Files
qingdao-employment-service/utils/fileValidator.js
2025-12-19 12:06:35 +08:00

210 lines
7.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.

const KNOWN_SIGNATURES = {
png: "89504E470D0A1A0A",
jpg: "FFD8FF",
jpeg: "FFD8FF",
gif: "47494638",
webp: "52494646",
docx: "504B0304",
xlsx: "504B0304",
pptx: "504B0304",
doc: "D0CF11E0",
xls: "D0CF11E0",
ppt: "D0CF11E0",
pdf: "25504446",
txt: "TYPE_TEXT",
csv: "TYPE_TEXT",
md: "TYPE_TEXT",
json: "TYPE_TEXT"
};
export class FileValidator {
version = "1.0.0";
signs = Object.keys(KNOWN_SIGNATURES);
constructor(options = {}) {
this.maxSizeMB = options.maxSizeMB || 10;
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
this.allowedConfig = {};
options.allowedExtensions.forEach(ext => {
const key = ext.toLowerCase();
if (KNOWN_SIGNATURES[key]) {
this.allowedConfig[key] = KNOWN_SIGNATURES[key]
} else {
console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`)
}
})
} else {
this.allowedConfig = {
...KNOWN_SIGNATURES
}
}
}
_isValidUTF8(buffer) {
try {
const decoder = new TextDecoder("utf-8", {
fatal: true
});
decoder.decode(buffer);
return true
} catch (e) {
return false
}
}
_bufferToHex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), x => ("00" + x.toString(16)).slice(-2)).join("")
.toUpperCase()
}
_countCSVRows(buffer) {
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(buffer);
let rowCount = 0;
let inQuote = false;
let len = text.length;
for (let i = 0; i < len; i++) {
const char = text[i];
if (char === '"') {
inQuote = !inQuote
} else if (char === "\n" && !inQuote) {
rowCount++
}
}
if (len > 0 && text[len - 1] !== "\n") {
rowCount++
}
return rowCount
}
_validateTextContent(buffer, extension) {
let contentStr = "";
try {
const decoder = new TextDecoder("utf-8", {
fatal: true
});
contentStr = decoder.decode(buffer)
} catch (e) {
console.warn("UTF-8 解码失败", e);
return false
}
if (contentStr.includes("\0")) {
return false
}
if (extension === "json") {
try {
JSON.parse(contentStr)
} catch (e) {
console.warn("无效的 JSON 格式");
return false
}
}
return true
}
validate(file) {
return new Promise((resolve, reject) => {
if (!file || !file.name) return reject("无效的文件对象");
if (file.size > this.maxSizeMB * 1024 * 1024) {
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`)
}
const fileName = file.name.toLowerCase();
const extension = fileName.substring(fileName.lastIndexOf(".") + 1);
const expectedMagic = this.allowedConfig[extension];
if (!expectedMagic) {
return reject(`不支持的文件格式: .${extension}`)
}
const reader = new FileReader;
reader.onload = e => {
const buffer = e.target.result;
let isSafe = false;
if (expectedMagic === "TYPE_TEXT") {
if (this._validateTextContent(buffer, extension)) {
isSafe = true
} else {
if (extension === "json") {
return reject(`文件异常:不是有效的 JSON 文件`)
}
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`)
}
if (extension === "csv" && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer);
if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`)
}
}
} else {
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
if (fileHeader.startsWith(expectedMagic)) {
isSafe = true
} else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`)
}
}
if (isSafe) resolve(true)
};
reader.onerror = () => reject("文件读取失败,无法校验");
if (expectedMagic === "TYPE_TEXT" && extension === "json") {
reader.readAsArrayBuffer(file)
} else {
reader.readAsArrayBuffer(file.slice(0, 2048))
}
})
}
}
// FileValidator 使用文档
// FileValidator 是一个用于浏览器端的 JavaScript 文件校验类。它提供了比简单的后缀名检查更安全的文件验证机制。
// 主要特性
// 真实类型检测:通过读取文件二进制头部的“魔数”来验证文件类型,防止后缀名伪造。
// 文本内容安全:检测文本文件是否为有效的 UTF-8 编码,防止乱码或二进制文件伪装。
// JSON 语法校验:针对 JSON 文件,会自动尝试解析以确保格式正确。
// CSV 行数限制:支持限制 CSV 文件的最大行数(需手动配置)。
// 大小限制内置文件大小检查MB
// 按需配置:支持自定义允许的文件扩展名列表。
// 使用方法
// import { FileValidator } from './fileValidator.js';
// 1. 初始化校验器
// const validator = new FileValidator({
// maxSizeMB: 5, // 限制最大 5MB
// allowedExtensions: ['jpg', 'png', 'pdf', 'docx'] // 仅允许这些格式
// });
// // 2. 获取文件对象 (通常来自 input[type="file"])
// const fileInput = document.getElementById('file-upload');
// fileInput.addEventListener('change', async (event) => {
// const file = event.target.files[0];
// if (!file) return;
// try {
// // 3. 执行校验
// await validator.validate(file);
// console.log('✅ 文件校验通过,可以上传');
// // 在这里执行你的上传逻辑...
// } catch (errorMessage) {
// console.error('❌ 校验失败:', errorMessage);
// alert(errorMessage);
// }
// });
// 配置项:
// maxSizeMB: 允许的最大文件大小,单位 MB。
// allowedExtensions: 允许的文件后缀列表(不区分大小写)。如果不传,则允许所有内置支持的格式。
// // 允许所有支持的格式,限制 20MB
// const v1 = new FileValidator({ maxSizeMB: 20 });
// // 仅允许图片
// const v2 = new FileValidator({
// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp']
// });
// 设置 CSV 最大允许 1000 行
// validator.csvMaxRows = 1000;
// 分类,扩展名,检测方式
// 图片,"png, jpg, jpeg, gif, webp",二进制头签名 (Magic Number)
// 文档,"pdf, docx, xlsx, pptx, doc, xls, ppt",二进制头签名 (Magic Number)
// 文本,"txt, csv, md",UTF-8 编码检测 + 无 Null 字节检测
// 数据,json,UTF-8 编码检测 + JSON.parse 语法校验