fix: avoid duplicate names in task detail zips

This commit is contained in:
chenpei 2026-05-25 11:45:49 +08:00
parent 91aad7bb5a
commit 8dd8ebac7e
2 changed files with 63 additions and 6 deletions

View File

@ -1,6 +1,6 @@
/** @jest-environment node */
import { generatePreviewImages } from '@/lib/task-material-processor'
import { generatePreviewImages, resolveUniqueZipEntryName } from '@/lib/task-material-processor'
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
import { uploadToS3 } from '@/lib/s3'
import { compressImage } from '@/lib/image-processor'
@ -80,4 +80,14 @@ describe('task-material-processor', () => {
)
expect(uploadToS3).not.toHaveBeenCalled()
})
test('resolveUniqueZipEntryName 应避免同名文件在解压时互相覆盖', () => {
const usedNames = new Set<string>()
expect(resolveUniqueZipEntryName('01.jpg', 0, usedNames)).toBe('01.jpg')
expect(resolveUniqueZipEntryName('01.jpg', 1, usedNames)).toBe('01-2.jpg')
expect(resolveUniqueZipEntryName('01.JPG', 2, usedNames)).toBe('01-3.JPG')
expect(resolveUniqueZipEntryName('主图/01.jpg', 3, usedNames)).toBe('主图_01.jpg')
expect(resolveUniqueZipEntryName('', 4, usedNames)).toBe('file-005')
})
})

View File

@ -68,6 +68,46 @@ function normalizeMaterialAssetFileUrl(value: unknown): string {
return String(value || '');
}
export function resolveUniqueZipEntryName(
originalName: string | null | undefined,
index: number,
usedNames: Set<string>,
): string {
const fallbackName = `file-${String(index + 1).padStart(3, '0')}`;
const safeName = String(originalName || fallbackName)
.replace(/[\0\r\n]/g, ' ')
.replace(/[\/\\]+/g, '_')
.trim() || fallbackName;
const addIfUnique = (candidate: string) => {
const key = candidate.toLowerCase();
if (usedNames.has(key)) {
return false;
}
usedNames.add(key);
return true;
};
if (addIfUnique(safeName)) {
return safeName;
}
const ext = path.extname(safeName);
const baseName = (ext ? safeName.slice(0, -ext.length) : safeName) || fallbackName;
for (let duplicateIndex = 2; duplicateIndex < 10000; duplicateIndex += 1) {
const candidate = `${baseName}-${duplicateIndex}${ext}`;
if (addIfUnique(candidate)) {
return candidate;
}
}
const lastResortName = `${baseName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}${ext}`;
usedNames.add(lastResortName.toLowerCase());
return lastResortName;
}
/**
* 从chunk临时目录打包文件成zip
*/
@ -205,11 +245,18 @@ export async function createZipFromFiles(
// 连接archive到输出流
archive.pipe(output);
// 添加每个文件到zip
for (const file of files) {
console.log(`[Zip] Adding file: ${file.name} (${file.size} bytes)`);
archive.file(file.path, { name: file.name });
}
// 添加每个文件到zip。上传文件可能来自多个文件夹,文件名会重复;
// ZIP 内同名条目在很多解压软件中会互相覆盖,因此这里必须保证条目名唯一。
const usedZipEntryNames = new Set<string>();
files.forEach((file, index) => {
const zipEntryName = resolveUniqueZipEntryName(file.name, index, usedZipEntryNames);
if (zipEntryName !== file.name) {
console.warn(`[Zip] Duplicate or unsafe file name adjusted: ${file.name} -> ${zipEntryName}`);
}
console.log(`[Zip] Adding file: ${zipEntryName} (${file.size} bytes)`);
archive.file(file.path, { name: zipEntryName });
});
// 完成压缩
archive.finalize();