Files
CloudFlare-ImgBed/functions/api/manage/delete/[[path]].js
axibayuit 3d77585ec5 feat: add storage quota limit for S3/R2 channels
- Add quota counter tracking (usedMB, fileCount) per channel
- Filter channels by quota threshold in fetchUploadConfig()
- Update quota on upload (endUpload) and delete
- Add /api/manage/quota API for stats and recalculation
- Fix null check in delete when file record not found
- Fix R2 channel name from hardcoded to actual channel.name
2025-12-29 19:55:20 +08:00

202 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
import { purgeCFCache } from "../../../utils/purgeCache";
import { removeFileFromIndex, batchRemoveFilesFromIndex } from "../../../utils/indexManager.js";
import { getDatabase } from '../../../utils/databaseAdapter.js';
import { updateQuotaCounter } from '../../../upload/uploadTools.js';
// CORS 跨域响应头
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export async function onRequest(context) {
const { request, env, params, waitUntil } = context;
const url = new URL(request.url);
// 读取folder参数判断是否为文件夹删除请求
const folder = url.searchParams.get('folder');
if (folder === 'true') {
try {
params.path = decodeURIComponent(params.path);
// 使用队列存储需要处理的文件夹
const folderQueue = [{
path: params.path.split(',').join('/')
}];
const deletedFiles = [];
const failedFiles = [];
while (folderQueue.length > 0) {
const currentFolder = folderQueue.shift();
// 获取指定目录下的所有文件
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${currentFolder.path}`);
const listRequest = new Request(listUrl, request);
const listResponse = await fetch(listRequest);
const listData = await listResponse.json();
const files = listData.files;
// 处理当前文件夹下的所有文件
for (const file of files) {
const fileId = file.name;
const cdnUrl = `https://${url.hostname}/file/${fileId}`;
const success = await deleteFile(env, fileId, cdnUrl, url);
if (success) {
deletedFiles.push(fileId);
} else {
failedFiles.push(fileId);
}
}
// 将子文件夹添加到队列
const directories = listData.directories;
for (const dir of directories) {
folderQueue.push({
path: dir
});
}
}
// 批量从索引中删除文件
if (deletedFiles.length > 0) {
waitUntil(batchRemoveFilesFromIndex(context, deletedFiles));
}
return new Response(JSON.stringify({
success: true,
deleted: deletedFiles,
failed: failedFiles
}), {
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
} catch (e) {
return new Response(JSON.stringify({
success: false,
error: e.message
}), {
status: 400,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
}
}
// 单个文件删除处理
try {
// 解码params.path
params.path = decodeURIComponent(params.path);
const fileId = params.path.split(',').join('/');
const cdnUrl = `https://${url.hostname}/file/${fileId}`;
const success = await deleteFile(env, fileId, cdnUrl, url);
if (!success) {
throw new Error('Delete file failed');
} else {
// 从索引中删除文件
waitUntil(removeFileFromIndex(context, fileId));
}
return new Response(JSON.stringify({
success: true,
fileId: fileId
}), {
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
} catch (e) {
return new Response(JSON.stringify({
success: false,
error: e.message
}), {
status: 400,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
}
}
// 删除单个文件的核心函数
async function deleteFile(env, fileId, cdnUrl, url) {
try {
// 读取图片信息
const db = getDatabase(env);
const img = await db.getWithMetadata(fileId);
// 如果文件记录不存在,直接返回成功(幂等删除)
if (!img) {
console.warn(`File ${fileId} not found in database, skipping delete`);
return true;
}
// 如果是R2渠道的图片需要删除R2中对应的图片
if (img.metadata?.Channel === 'CloudflareR2') {
const R2DataBase = env.img_r2;
await R2DataBase.delete(fileId);
}
// S3 渠道的图片需要删除S3中对应的图片
if (img.metadata?.Channel === 'S3') {
await deleteS3File(img);
}
// 更新容量计数器(在删除记录之前)
if (img.metadata?.ChannelName && img.metadata?.FileSize) {
await updateQuotaCounter(env, img.metadata.ChannelName, img.metadata.FileSize, 'subtract');
}
// 删除数据库中的记录
await db.delete(fileId);
// 清除CDN缓存
await purgeCFCache(env, cdnUrl);
// 清除randomFileList API缓存
try {
const cache = caches.default;
const nullResponse = new Response(null, {
headers: { 'Cache-Control': 'max-age=0' },
});
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedFolder}`, nullResponse);
} catch (error) {
console.error('Failed to clear cache:', error);
}
return true;
} catch (e) {
console.error('Delete file failed:', e);
return false;
}
}
// 删除 S3 渠道的图片
async function deleteS3File(img) {
const s3Client = new S3Client({
region: img.metadata?.S3Region || "auto",
endpoint: img.metadata?.S3Endpoint,
credentials: {
accessKeyId: img.metadata?.S3AccessKeyId,
secretAccessKey: img.metadata?.S3SecretAccessKey
},
forcePathStyle: img.metadata?.S3PathStyle || false // 是否启用路径风格
});
const bucketName = img.metadata?.S3BucketName;
const key = img.metadata?.S3FileKey;
try {
await s3Client.send(new DeleteObjectCommand({
Bucket: bucketName,
Key: key,
}));
return true;
} catch (error) {
console.error("S3 Delete Failed:", error);
return false;
}
}