140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import fs from 'fs-extra';
|
|
import path from 'path';
|
|
import { getSpectrogramsPath } from '@/lib/utils/paths';
|
|
|
|
/**
|
|
* Robustly removes a directory with retries, exponential backoff, and manual file-by-file cleanup fallback
|
|
* to handle transient file locks or pending OS handle releases (e.g. ENOTEMPTY / EBUSY).
|
|
*/
|
|
async function removeDirectoryWithRetry(dirPath: string, retries = 5, delayMs = 300): Promise<void> {
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
if (!(await fs.pathExists(dirPath))) return;
|
|
|
|
// Standard recursive force removal with internal retries
|
|
await fs.rm(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
|
|
if (!(await fs.pathExists(dirPath))) {
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Attempt ${attempt}/${retries} to remove directory ${dirPath} failed:`, err);
|
|
}
|
|
|
|
// Fallback: If directory still exists, manually remove contents individually
|
|
try {
|
|
if (await fs.pathExists(dirPath)) {
|
|
const files = await fs.readdir(dirPath);
|
|
for (const file of files) {
|
|
const fullPath = path.join(dirPath, file);
|
|
try {
|
|
await fs.remove(fullPath);
|
|
} catch (e) {
|
|
console.warn(`Failed to remove item inside directory ${fullPath}:`, e);
|
|
}
|
|
}
|
|
// Try rmdir on the directory
|
|
await fs.rmdir(dirPath);
|
|
if (!(await fs.pathExists(dirPath))) return;
|
|
}
|
|
} catch (err) {
|
|
// Ignore intermediate loop errors; retry will continue
|
|
}
|
|
|
|
if (attempt < retries) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
|
|
}
|
|
}
|
|
|
|
// Final attempt
|
|
if (await fs.pathExists(dirPath)) {
|
|
await fs.remove(dirPath);
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const { albumPath, upc } = await request.json();
|
|
|
|
if (!albumPath) {
|
|
return NextResponse.json({ error: 'Invalid album path' }, { status: 400 });
|
|
}
|
|
|
|
const albumName = path.basename(albumPath);
|
|
const parentDir = path.dirname(albumPath);
|
|
const errors: string[] = [];
|
|
|
|
// 1. Delete generated torrent file(s) FIRST (independent of directory removal)
|
|
if (process.env.DELETE_GENERATED_TORRENTS === 'true') {
|
|
try {
|
|
const primaryTorrentPath = path.join(parentDir, `${albumName}.torrent`);
|
|
if (await fs.pathExists(primaryTorrentPath)) {
|
|
await fs.remove(primaryTorrentPath);
|
|
console.log(`Deleted generated torrent: ${primaryTorrentPath}`);
|
|
}
|
|
|
|
// Also check for matching torrent files in parent directory
|
|
if (await fs.pathExists(parentDir)) {
|
|
const files = await fs.readdir(parentDir);
|
|
const matchingTorrents = files.filter(f =>
|
|
f.endsWith('.torrent') && f.toLowerCase().includes(albumName.toLowerCase())
|
|
);
|
|
for (const torrentFile of matchingTorrents) {
|
|
const tPath = path.join(parentDir, torrentFile);
|
|
if (await fs.pathExists(tPath)) {
|
|
await fs.remove(tPath);
|
|
console.log(`Deleted matching torrent: ${tPath}`);
|
|
}
|
|
}
|
|
}
|
|
} catch (tErr) {
|
|
console.error('Error deleting generated torrent:', tErr);
|
|
errors.push(`Torrent deletion warning: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
|
|
}
|
|
}
|
|
|
|
// 2. Delete spectrograms directory for this album (independent task)
|
|
try {
|
|
const specRoot = getSpectrogramsPath();
|
|
const specDir = path.join(specRoot, upc || albumName);
|
|
if (await fs.pathExists(specDir)) {
|
|
await fs.remove(specDir);
|
|
}
|
|
if (upc && albumName !== upc) {
|
|
const altSpecDir = path.join(specRoot, albumName);
|
|
if (await fs.pathExists(altSpecDir)) {
|
|
await fs.remove(altSpecDir);
|
|
}
|
|
}
|
|
} catch (sErr) {
|
|
console.error('Error deleting spectrograms:', sErr);
|
|
errors.push(`Spectrogram deletion warning: ${sErr instanceof Error ? sErr.message : String(sErr)}`);
|
|
}
|
|
|
|
// 3. Delete the album directory with retries
|
|
if (await fs.pathExists(albumPath)) {
|
|
try {
|
|
await removeDirectoryWithRetry(albumPath, 5, 300);
|
|
} catch (dirErr) {
|
|
console.error('Error deleting album directory:', dirErr);
|
|
errors.push(`Album directory deletion error: ${dirErr instanceof Error ? dirErr.message : String(dirErr)}`);
|
|
}
|
|
}
|
|
|
|
// Verify result
|
|
const directoryStillExists = await fs.pathExists(albumPath);
|
|
if (directoryStillExists) {
|
|
return NextResponse.json({
|
|
error: `Failed to completely remove directory: ${path.basename(albumPath)}`,
|
|
details: errors
|
|
}, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true, warnings: errors.length > 0 ? errors : undefined });
|
|
} catch (error) {
|
|
console.error('Error deleting album:', error);
|
|
return NextResponse.json({ error: 'Failed to delete album' }, { status: 500 });
|
|
}
|
|
}
|