From 65cd72b8b9351ab2f41fab0055635ea884e2e0a7 Mon Sep 17 00:00:00 2001 From: caoyong Date: Wed, 22 Jul 2026 19:54:42 +0800 Subject: [PATCH 1/3] feat(manage): add concurrent batch image deletion --- deploy/worker/index.js | 2 ++ functions/api/manage/delete/[[path]].js | 2 +- functions/api/manage/delete/batch.js | 45 +++++++++++++++++++++++++ functions/utils/deleteBatch.js | 23 +++++++++++++ test/delete-batch.test.js | 32 ++++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 functions/api/manage/delete/batch.js create mode 100644 functions/utils/deleteBatch.js create mode 100644 test/delete-batch.test.js diff --git a/deploy/worker/index.js b/deploy/worker/index.js index bf6dfc76..db80d66e 100644 --- a/deploy/worker/index.js +++ b/deploy/worker/index.js @@ -27,6 +27,7 @@ import * as apiManageCusConfigBlockipList from '../../functions/api/manage/cusCo import * as apiManageCusConfigFiles from '../../functions/api/manage/cusConfig/files.js'; import * as apiManageCusConfigList from '../../functions/api/manage/cusConfig/list.js'; import * as apiManageCusConfigWhiteip from '../../functions/api/manage/cusConfig/whiteip.js'; +import * as apiManageDeleteBatch from '../../functions/api/manage/delete/batch.js'; import * as apiManageSysConfigOthers from '../../functions/api/manage/sysConfig/others.js'; import * as apiManageSysConfigPage from '../../functions/api/manage/sysConfig/page.js'; import * as apiManageSysConfigSecurity from '../../functions/api/manage/sysConfig/security.js'; @@ -77,6 +78,7 @@ const routes = [ { path: '/api/manage/cusConfig/files', module: apiManageCusConfigFiles, middlewares: [mw_api, mw_api_manage] }, { path: '/api/manage/cusConfig/list', module: apiManageCusConfigList, middlewares: [mw_api, mw_api_manage] }, { path: '/api/manage/cusConfig/whiteip', module: apiManageCusConfigWhiteip, middlewares: [mw_api, mw_api_manage] }, + { path: '/api/manage/delete/batch', module: apiManageDeleteBatch, middlewares: [mw_api, mw_api_manage] }, { path: '/api/manage/sysConfig/others', module: apiManageSysConfigOthers, middlewares: [mw_api, mw_api_manage] }, { path: '/api/manage/sysConfig/page', module: apiManageSysConfigPage, middlewares: [mw_api, mw_api_manage] }, { path: '/api/manage/sysConfig/security', module: apiManageSysConfigSecurity, middlewares: [mw_api, mw_api_manage] }, diff --git a/functions/api/manage/delete/[[path]].js b/functions/api/manage/delete/[[path]].js index ae553045..01da70cc 100644 --- a/functions/api/manage/delete/[[path]].js +++ b/functions/api/manage/delete/[[path]].js @@ -130,7 +130,7 @@ export async function onRequest(context) { } // 删除单个文件的核心函数 -async function deleteFile(env, fileId, cdnUrl, url) { +export async function deleteFile(env, fileId, cdnUrl, url) { try { // 读取图片信息 const db = getDatabase(env); diff --git a/functions/api/manage/delete/batch.js b/functions/api/manage/delete/batch.js new file mode 100644 index 00000000..768c43c5 --- /dev/null +++ b/functions/api/manage/delete/batch.js @@ -0,0 +1,45 @@ +import { batchRemoveFilesFromIndex } from '../../../utils/indexManager.js'; +import { mapConcurrent, normalizeBatchFileIds } from '../../../utils/deleteBatch.js'; +import { deleteFile } from './[[path]].js'; + +const MAX_BATCH_SIZE = 500; +const DELETE_CONCURRENCY = 10; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', +}; + +export async function onRequest(context) { + try { + const payload = await context.request.json(); + const fileIds = normalizeBatchFileIds(payload?.fileIds, MAX_BATCH_SIZE); + if (fileIds.length === 0) { + return jsonResponse({ success: false, error: 'fileIds is required' }, 400); + } + const url = new URL(context.request.url); + const results = await mapConcurrent(fileIds, DELETE_CONCURRENCY, async (fileId) => { + const cdnUrl = `${url.origin}/file/${fileId.split('/').map(encodeURIComponent).join('/')}`; + const success = await deleteFile(context.env, fileId, cdnUrl, url); + return { fileId, success, error: success ? '' : 'Delete file failed' }; + }); + + const deleted = results.filter((item) => item.success).map((item) => item.fileId); + const failed = results.filter((item) => !item.success).map(({ fileId, error }) => ({ fileId, error })); + if (deleted.length > 0) { + context.waitUntil(batchRemoveFilesFromIndex(context, deleted)); + } + return jsonResponse({ success: failed.length === 0, deleted, failed }); + } catch (error) { + return jsonResponse({ success: false, error: error.message }, 400); + } +} + +function jsonResponse(payload, status = 200) { + return new Response(JSON.stringify(payload), { + status, + headers: { 'Content-Type': 'application/json', ...corsHeaders }, + }); +} diff --git a/functions/utils/deleteBatch.js b/functions/utils/deleteBatch.js new file mode 100644 index 00000000..e4689958 --- /dev/null +++ b/functions/utils/deleteBatch.js @@ -0,0 +1,23 @@ +export function normalizeBatchFileIds(values, maxBatchSize) { + if (!Array.isArray(values)) return []; + const normalized = [...new Set(values + .map((value) => String(value || '').trim().replace(/^\/+|\/+$/g, '')) + .filter(Boolean))]; + if (normalized.length > maxBatchSize) { + throw new Error(`A maximum of ${maxBatchSize} files can be deleted at once`); + } + return normalized; +} + +export async function mapConcurrent(values, concurrency, operation) { + const results = new Array(values.length); + let nextIndex = 0; + const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => { + while (nextIndex < values.length) { + const index = nextIndex++; + results[index] = await operation(values[index]); + } + }); + await Promise.all(workers); + return results; +} diff --git a/test/delete-batch.test.js b/test/delete-batch.test.js new file mode 100644 index 00000000..cd00f6df --- /dev/null +++ b/test/delete-batch.test.js @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { mapConcurrent, normalizeBatchFileIds } from '../functions/utils/deleteBatch.js'; + +describe('delete batch helpers', function () { + it('normalizes and deduplicates file ids', function () { + assert.deepEqual(normalizeBatchFileIds([' /a.png ', 'a.png', 'folder/b.png', ''], 500), [ + 'a.png', + 'folder/b.png', + ]); + }); + + it('rejects more than 500 file ids', function () { + assert.throws( + () => normalizeBatchFileIds(Array.from({ length: 501 }, (_, index) => `${index}.png`), 500), + /500/, + ); + }); + + it('keeps result ordering while bounding concurrency', async function () { + let active = 0; + let maxActive = 0; + const results = await mapConcurrent([3, 1, 2, 4], 2, async (value) => { + active += 1; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setTimeout(resolve, value)); + active -= 1; + return value * 2; + }); + assert.deepEqual(results, [6, 2, 4, 8]); + assert.equal(maxActive, 2); + }); +}); From a9d7050ab5901f3396e29785fdf6cab559fe7f49 Mon Sep 17 00:00:00 2001 From: caoyong Date: Thu, 23 Jul 2026 09:27:54 +0800 Subject: [PATCH 2/3] chore: remove batch deletion test file --- test/delete-batch.test.js | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 test/delete-batch.test.js diff --git a/test/delete-batch.test.js b/test/delete-batch.test.js deleted file mode 100644 index cd00f6df..00000000 --- a/test/delete-batch.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'node:assert/strict'; -import { mapConcurrent, normalizeBatchFileIds } from '../functions/utils/deleteBatch.js'; - -describe('delete batch helpers', function () { - it('normalizes and deduplicates file ids', function () { - assert.deepEqual(normalizeBatchFileIds([' /a.png ', 'a.png', 'folder/b.png', ''], 500), [ - 'a.png', - 'folder/b.png', - ]); - }); - - it('rejects more than 500 file ids', function () { - assert.throws( - () => normalizeBatchFileIds(Array.from({ length: 501 }, (_, index) => `${index}.png`), 500), - /500/, - ); - }); - - it('keeps result ordering while bounding concurrency', async function () { - let active = 0; - let maxActive = 0; - const results = await mapConcurrent([3, 1, 2, 4], 2, async (value) => { - active += 1; - maxActive = Math.max(maxActive, active); - await new Promise((resolve) => setTimeout(resolve, value)); - active -= 1; - return value * 2; - }); - assert.deepEqual(results, [6, 2, 4, 8]); - assert.equal(maxActive, 2); - }); -}); From 301bbfa0a9ba64de587667dd74f0db8a65c22a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=81=E6=9C=88=E6=9F=92?= <108160987+MarSeventh@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:46:13 +0800 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- functions/api/manage/delete/batch.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/api/manage/delete/batch.js b/functions/api/manage/delete/batch.js index 768c43c5..30db2df6 100644 --- a/functions/api/manage/delete/batch.js +++ b/functions/api/manage/delete/batch.js @@ -12,8 +12,7 @@ const corsHeaders = { 'Access-Control-Max-Age': '86400', }; -export async function onRequest(context) { - try { +export async function onRequestPost(context) { const payload = await context.request.json(); const fileIds = normalizeBatchFileIds(payload?.fileIds, MAX_BATCH_SIZE); if (fileIds.length === 0) {