D2: Widen retry to upload-source jobs

- Remove http-only guard in retry endpoint (src/routes/api/jobs/[id]/retry/+server.ts)
- Extend pipeline.ts retryJob() to handle upload-source jobs:
  read file from persistent storage, pass buffer to runJob
- Change saveUploadedFile() to save to DATA_DIR/uploads/{jobId}/
  (survives cleanupJobTmp, available for retry on failure)
- Add getUploadPath(), cleanupUploadDir() to downloader.ts
- Remove cleanupFiles(rawAudioPath) from catch block in runJob()
  — cleanupJobTmp handles TMP_DIR for YouTube, uploads survive
- Add cleanupUploadDir() on webhook success path
- Add retry-endpoint.test.ts with 9 tests covering:
  - 404 for unknown job
  - 409 for non-retryable status (done, pending)
  - 200 for cancelled (retryable)
  - 200 for failed YouTube job (existing behavior)
  - 200 for failed upload-source job (new behavior, .webm and .mp3)
  - Negative: retryJob not called on 404/409
- Add downloader.test.ts tests for saveUploadedFile persistent path,
  getUploadPath, cleanupUploadDir, isolation, and non-existent cleanup
- Fix webhook.test.ts mock to include cleanupUploadDir

All 189 tests pass across 12 test files.
This commit is contained in:
Giancarmine Salucci
2026-07-08 01:25:49 +02:00
parent d633f74689
commit f03b757174
7 changed files with 217 additions and 17 deletions
+25 -5
View File
@@ -6,12 +6,34 @@ import { join } from 'path';
import { fetchTranscript, type TranscriptResponse } from 'youtube-transcript';
const execFileAsync = promisify(execFile);
const TMP_DIR = join(process.env.DATA_DIR ?? '/tmp/.whisper-pwa', 'downloads');
const DATA_DIR = process.env.DATA_DIR ?? '/tmp/.whisper-pwa';
const TMP_DIR = join(DATA_DIR, 'downloads');
const UPLOADS_DIR = join(DATA_DIR, 'uploads');
export async function ensureTmpDir() {
if (!existsSync(TMP_DIR)) await mkdir(TMP_DIR, { recursive: true });
}
async function ensureUploadsDir(jobId: string): Promise<string> {
const dir = join(UPLOADS_DIR, jobId);
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
return dir;
}
/** Path to a job's persistent upload file. Survives cleanupJobTmp. */
export function getUploadPath(jobId: string, filename: string): string {
return join(UPLOADS_DIR, jobId, filename);
}
/** Delete a job's persistent upload dir (call on success). */
export async function cleanupUploadDir(jobId: string): Promise<void> {
const dir = join(UPLOADS_DIR, jobId);
try {
const { rm } = await import('fs/promises');
await rm(dir, { recursive: true, force: true });
} catch { /* ignore */ }
}
export interface CaptionResult {
type: 'captions';
segments: Array<{ index: number; start: number; end: number; text: string; words: [] }>;
@@ -93,15 +115,13 @@ export async function downloadYouTube(url: string, jobId: string): Promise<Downl
return { type: 'audio', audioPath, title };
}
/** Save an uploaded file to tmp dir and return its path. */
/** Save an uploaded file to persistent storage (survives cleanupJobTmp). */
export async function saveUploadedFile(
buffer: Buffer,
filename: string,
jobId: string
): Promise<string> {
await ensureTmpDir();
const outDir = join(TMP_DIR, jobId);
await mkdir(outDir, { recursive: true });
const outDir = await ensureUploadsDir(jobId);
const dest = join(outDir, filename);
await writeFile(dest, buffer);
return dest;