improve audio stream teardown and enhance robust directory deletion with retries

This commit is contained in:
2026-07-25 21:12:38 +01:00
parent 7806483a86
commit 5185d4dc0b
3 changed files with 140 additions and 22 deletions
+116 -22
View File
@@ -3,41 +3,135 @@ 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 || !(await fs.pathExists(albumPath))) {
if (!albumPath) {
return NextResponse.json({ error: 'Invalid album path' }, { status: 400 });
}
// 1. Delete the album directory
await fs.remove(albumPath);
// 2. Delete the spectrograms directory for this album
const specRoot = getSpectrogramsPath();
const albumName = path.basename(albumPath);
const parentDir = path.dirname(albumPath);
const errors: string[] = [];
// Try UPC first, then album name
let specDir = path.join(specRoot, upc || albumName);
if (await fs.pathExists(specDir)) {
await fs.remove(specDir);
} else if (upc && await fs.pathExists(path.join(specRoot, albumName))) {
// Fallback: check album name if UPC dir didn't exist but UPC was provided
await fs.remove(path.join(specRoot, albumName));
}
// 3. Conditionally delete the generated torrent file
// 1. Delete generated torrent file(s) FIRST (independent of directory removal)
if (process.env.DELETE_GENERATED_TORRENTS === 'true') {
const parentDir = path.dirname(albumPath);
const torrentPath = path.join(parentDir, `${albumName}.torrent`);
if (await fs.pathExists(torrentPath)) {
await fs.remove(torrentPath);
console.log(`Deleted generated torrent: ${torrentPath}`);
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)}`);
}
}
return NextResponse.json({ success: true });
// 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 });