fix(canvas): defer expired archive cleanup after ZIP admission
This commit is contained in:
parent
226a85d339
commit
85aa711633
@ -169,7 +169,6 @@ export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
try {
|
||||
await cleanupZipTasks();
|
||||
const body = await request.json();
|
||||
let id = typeof body.taskId === "string" ? body.taskId : randomUUID();
|
||||
let task = await loadZipTask(id, session.user.id);
|
||||
@ -199,6 +198,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveZipTask(task);
|
||||
}
|
||||
if (task.status === "pending" || task.status === "running") schedule(task, request);
|
||||
after(() => cleanupZipTasks().catch(() => { console.warn("[CanvasDownloadZipTask] Expired file cleanup deferred"); }));
|
||||
return NextResponse.json({ success: true, task: serializeTask(task) }, { status: 202, headers: { "Cache-Control": "no-store" } });
|
||||
} catch (error) {
|
||||
console.warn("[CanvasDownloadZipTask] Create rejected", { message: (error as Error).message });
|
||||
|
||||
@ -35,7 +35,7 @@ test("multi-select ZIP admission is immediate; complete archives survive restart
|
||||
const response=await route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId,folderName:'作品',files})}));
|
||||
expect(response.status).toBe(202);expect(fetches).toBe(0);
|
||||
expect((await route.GET(request(`taskId=${taskId}&download=1`))).status).toBe(409);
|
||||
await callbacks.shift()!();
|
||||
while (callbacks.length) await callbacks.shift()!();
|
||||
const state=await (await route.GET(request(`taskId=${taskId}`))).json();
|
||||
expect(state.task).toMatchObject({status:'completed',successCount:2,failedCount:0});
|
||||
const complete=await route.GET(request(`taskId=${taskId}&download=1`));
|
||||
@ -63,10 +63,10 @@ test("all-source failures never produce an empty successful ZIP; retries reuse t
|
||||
},{CANVAS_DOWNLOAD_ZIP_DIR:dir})('app/api/canvas/download-zip-task/route.ts');
|
||||
const id=randomUUID();const post=()=>new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId:id,retry:true,files:[{url:'https://provider.example/expired.png'}]})});
|
||||
try {
|
||||
await route.POST(post());await callbacks.shift()!();
|
||||
await route.POST(post());while (callbacks.length) await callbacks.shift()!();
|
||||
let state=await(await route.GET(new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`))).json();
|
||||
expect(state.task.status).toBe('failed');expect(state.task.downloadUrl).toBe('');
|
||||
available=true;await route.POST(post());await callbacks.shift()!();
|
||||
available=true;await route.POST(post());while (callbacks.length) await callbacks.shift()!();
|
||||
state=await(await route.GET(new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`))).json();
|
||||
expect(state.task).toMatchObject({id,status:'completed',successCount:1});
|
||||
} finally {await rm(dir,{recursive:true,force:true});}
|
||||
@ -116,7 +116,7 @@ test("partial source failures are explicit; CDN redirects retain an authenticate
|
||||
const id=randomUUID();const request=(extra='')=>new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}${extra}`);
|
||||
try{
|
||||
await route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId:id,files:[{url:'https://provider.example/1'},{url:'https://provider.example/2'}]})}));
|
||||
await callbacks.shift()!();
|
||||
while (callbacks.length) await callbacks.shift()!();
|
||||
const state=await(await route.GET(request())).json();expect(state.task).toMatchObject({status:'completed',successCount:1,failedCount:1});
|
||||
const cdn=await route.GET(request('&download=1'));expect(cdn.status).toBe(307);expect(cdn.headers.get('location')).toBe('https://cdn.example/complete.zip');
|
||||
const local=await route.GET(request('&download=1&local=1'));const zip=await JSZip.loadAsync(await local.arrayBuffer());
|
||||
@ -137,7 +137,7 @@ test("recovery validates staged files instead of treating missing bytes as succe
|
||||
const store=load('lib/canvas/download-zip-task-store.ts');
|
||||
await store.saveZipTask({id,userId:'owner',folderName:'test',files:[{url:'https://provider.example/a',prepared:'0.image',filename:'a.png'}],totalCount:1,processedCount:1,successCount:1,failedCount:0,compressPercent:0,status:'running',stage:'preparing',createdAt:Date.now(),updatedAt:Date.now(),expiresAt:Date.now()+60000});
|
||||
const route=load('app/api/canvas/download-zip-task/route.ts');const req=()=>new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`);
|
||||
await route.GET(req());await callbacks.shift()!();
|
||||
await route.GET(req());while (callbacks.length) await callbacks.shift()!();
|
||||
const state=await(await route.GET(req())).json();expect(state.task).toMatchObject({status:'failed',successCount:0,failedCount:1,downloadUrl:''});
|
||||
}finally{await rm(dir,{recursive:true,force:true});}
|
||||
});
|
||||
@ -153,3 +153,19 @@ test("large ZIP writing checks current free space as staged input bytes are rele
|
||||
expect(result.bytes).toBeGreaterThan(48*1024**2);expect(checks).toBeGreaterThan(4);
|
||||
}finally{await rm(dir,{recursive:true,force:true});}
|
||||
});
|
||||
|
||||
test("slow expired-object cleanup cannot keep the ZIP admission request open",async()=>{
|
||||
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-cleanup-'));const callbacks:Array<()=>Promise<void>>=[];
|
||||
const mocks:Record<string,unknown>={
|
||||
'@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})},
|
||||
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
|
||||
'@/lib/canvas/photoshop-safe-download':{},'@/lib/storage/canvas-zip-delivery':{},
|
||||
};
|
||||
const load=serverLoader(mocks,{CANVAS_DOWNLOAD_ZIP_DIR:dir});const store=load('lib/canvas/download-zip-task-store.ts');
|
||||
mocks['@/lib/canvas/download-zip-task-store']={...store,cleanupZipTasks:()=>new Promise(()=>{})};
|
||||
try{
|
||||
const route=load('app/api/canvas/download-zip-task/route.ts');
|
||||
const result=await Promise.race([route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({files:[{url:'https://cdn.example/a.png'}]})})),new Promise(resolve=>setTimeout(()=>resolve('blocked'),1000))]);
|
||||
expect(result).not.toBe('blocked');expect((result as Response).status).toBe(202);
|
||||
}finally{await rm(dir,{recursive:true,force:true});}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user