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 { 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 }); } }