Visual Journal deploy
Deploy 5554830 to Docker Space
e4e0afe
Raw
History Blame Contribute Delete
4.52 kB
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<FileDeletionResult> {
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';
}