import { appLogger } from '@/lib/app-logger'; import { isValidImageFilename } from '@/lib/image-request-utils'; import { resolveImageOutputDir, verifyPasswordHash } from '@/lib/server-runtime'; import { withWebuiImageFilenameLock } from '@/lib/webui-image-retention-lock'; import { getWebuiImageRetentionStore } from '@/lib/webui-image-retention-store'; import fs from 'fs/promises'; import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; type DeleteRequestBody = { filenames: string[]; passwordHash?: string; }; type FileDeletionResult = { filename: string; success: boolean; fileDeleted?: boolean; fileAbsent?: boolean; markerRemoved?: boolean; error?: string; }; export async function POST(request: NextRequest) { let requestBody: DeleteRequestBody; try { requestBody = await request.json(); const appPassword = process.env.APP_PASSWORD?.trim(); if (appPassword) { const clientPasswordHash = requestBody.passwordHash; if (!clientPasswordHash) { appLogger.error('删除操作缺少访问码哈希。'); return NextResponse.json({ error: '未授权:缺少访问码哈希。' }, { status: 401 }); } if (!verifyPasswordHash(clientPasswordHash, appPassword)) { appLogger.error('删除操作的访问码哈希无效。'); return NextResponse.json({ error: '未授权:访问码无效。' }, { status: 401 }); } } } catch (e) { appLogger.error('解析 /api/image-delete 请求正文失败:', e); return NextResponse.json({ error: '请求正文无效:必须是 JSON。' }, { status: 400 }); } const { filenames } = requestBody; if (!Array.isArray(filenames) || filenames.some((fn) => typeof fn !== 'string')) { return NextResponse.json({ error: 'filenames 无效:必须是字符串数组。' }, { status: 400 }); } if (filenames.length === 0) { return NextResponse.json({ message: '未提供要删除的文件名。', results: [] }, { status: 200 }); } const outputDir = resolveImageOutputDir(); const deletionResults: FileDeletionResult[] = []; for (const filename of filenames) { if (!isValidImageFilename(filename)) { appLogger.warn(`删除操作收到无效文件名:${filename}`); deletionResults.push({ filename, success: false, error: '文件名格式无效。' }); continue; } deletionResults.push( await withWebuiImageFilenameLock( filename, async () => await deleteImageAndRetentionMarker(filename, outputDir) ) ); } const allSucceeded = deletionResults.every((r) => r.success); return NextResponse.json( { message: allSucceeded ? '所有文件已删除。' : '部分文件未能完整删除或清理自动清理保护。', results: deletionResults }, { status: allSucceeded ? 200 : 207 } // 部分失败时返回 207 Multi-Status。 ); } async function deleteImageAndRetentionMarker(filename: string, outputDir: string): Promise { const filepath = path.join(outputDir, filename); let fileDeleted = false; try { await fs.unlink(filepath); fileDeleted = true; } catch (error) { if (!isMissingFileError(error)) { appLogger.error(`删除图片失败 ${filepath}:`, error); return { filename, success: false, error: '删除文件失败。' }; } } try { await (await getWebuiImageRetentionStore()).remove([filename]); } catch (error) { appLogger.error('删除图片后清理自动清理保护失败。', error); return { filename, success: false, ...(fileDeleted ? { fileDeleted: true } : { fileAbsent: true }), markerRemoved: false, error: fileDeleted ? '图片已删除,但自动清理保护未能清理。' : '图片已不存在,但自动清理保护未能清理。' }; } if (fileDeleted) return { filename, success: true }; return { filename, success: false, fileAbsent: true, markerRemoved: true, error: '文件不存在。' }; } function isMissingFileError(error: unknown): boolean { return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'; }