improve audio stream teardown and enhance robust directory deletion with retries
This commit is contained in:
@@ -34,6 +34,12 @@ export async function GET(request: Request) {
|
||||
// Convert stream to ReadableStream for NextResponse
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
if (request.signal.aborted) {
|
||||
file.destroy();
|
||||
return;
|
||||
}
|
||||
const onAbort = () => file.destroy();
|
||||
request.signal.addEventListener('abort', onAbort);
|
||||
file.on('data', (chunk) => {
|
||||
try {
|
||||
controller.enqueue(chunk);
|
||||
@@ -43,11 +49,13 @@ export async function GET(request: Request) {
|
||||
}
|
||||
});
|
||||
file.on('end', () => {
|
||||
request.signal.removeEventListener('abort', onAbort);
|
||||
try {
|
||||
controller.close();
|
||||
} catch (e) { }
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
request.signal.removeEventListener('abort', onAbort);
|
||||
try {
|
||||
controller.error(err);
|
||||
} catch (e) { }
|
||||
@@ -73,6 +81,12 @@ export async function GET(request: Request) {
|
||||
// Convert stream to ReadableStream for NextResponse
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
if (request.signal.aborted) {
|
||||
file.destroy();
|
||||
return;
|
||||
}
|
||||
const onAbort = () => file.destroy();
|
||||
request.signal.addEventListener('abort', onAbort);
|
||||
file.on('data', (chunk) => {
|
||||
try {
|
||||
controller.enqueue(chunk);
|
||||
@@ -81,11 +95,13 @@ export async function GET(request: Request) {
|
||||
}
|
||||
});
|
||||
file.on('end', () => {
|
||||
request.signal.removeEventListener('abort', onAbort);
|
||||
try {
|
||||
controller.close();
|
||||
} catch (e) { }
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
request.signal.removeEventListener('abort', onAbort);
|
||||
try {
|
||||
controller.error(err);
|
||||
} catch (e) { }
|
||||
|
||||
+116
-22
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user