Files
competition-website/app/utils/filename.js
WorkBuddy a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00

63 lines
2.7 KiB
JavaScript
Raw Permalink 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.
'use strict'
/**
* 上传文件名归一化。
*
* 背景(生产事故复盘 2026-08-10):
* 备赛资料上传曾出现「列表读取 ENOENT」——磁盘上的文件名是乱码(GBK 字节被按
* UTF-8 误解码)+ 前导空格,导致 admin.getMaterials 在 readdirSync().map(statSync)
* 时 stat 不到该文件而整列抛错。根因是 uploadMaterial 直接拿 stream.filename 当
* 磁盘名、未做编码/空白归一化。
*
* 本函数对传入的「客户端原始文件名」做最小但充分的清洗,保证落盘名:
* - 是合法 UTF-8(NFC 归一化,消除组合字符/变体差异)
* - 不含控制字符与路径分隔符(防目录穿越 + 防 stat 失败)
* - 不含前导/多余空白(折叠为单空格并 trim —— 直接修复「前导空格 + 乱码」类 ENOENT)
* - 长度受限(保留扩展名)
* 若清洗后为空(极端情况),调用方应回退到时间戳名,避免写出空名文件。
*
* 注意:本函数不负责「还原」客户端用错误编码(如 GBK)发来的文件名——那是客户端
* 规范问题;它的职责是「让落盘名干净、可往返、不会让读盘崩溃」。已落盘的旧乱码文件
* 需运维侧手动 rm(见 diag_orphan + 人工清理)。
*/
function sanitizeUploadFileName(rawName, fallbackExt = '') {
if (!rawName || typeof rawName !== 'string') return ''
let name = rawName
// 1) 客户端可能预编码(如 %E5%89%8D...),先尝试解码
if (name.includes('%') && /%[0-9A-Fa-f]{2}/.test(name)) {
try { name = decodeURIComponent(name) } catch (e) { /* 解码失败保留原串 */ }
}
// 2) Unicode 归一化(NFC),消除同一字符的不同编码形态
try {
if (typeof name.normalize === 'function') name = name.normalize('NFC')
} catch (e) { /* 兜底保留 */ }
// 3) 去除控制字符(C0: 0x00-0x1F,DEL/0x7F)与路径分隔符/危险字符
// 控制字符在正则里用 \x00-\x1f 与 \x7f 表示,避免源码里写裸控制字节
name = name.replace(/[\x00-\x1f\x7f]/g, '')
name = name.replace(/[\/\\:*?"<>|]/g, '_')
// 4) 折叠所有空白(含全角/不间断空格等 \s 覆盖的字符)为单空格并 trim
name = name.replace(/\s+/g, ' ').trim()
// 5) 去除首尾点(避免 ".name" / "name." / ".." 类)
name = name.replace(/^[. ]+/, '').replace(/[. ]+$/, '')
// 6) 长度限制(保留扩展名)
const MAX = 120
if (name.length > MAX) {
const dot = name.lastIndexOf('.')
const ext = dot > 0 ? name.slice(dot) : ''
const safeExt = ext || fallbackExt || ''
name = name.slice(0, MAX - safeExt.length) + safeExt
}
return name
}
module.exports = { sanitizeUploadFileName }