web-font/backend/multipart.ts
崮生(子虚) e0f34c36b6 修复上传功能和服务器健壮性问题
- 修复 multipart 解析器:endDelimiter 提前检查导致循环退出
- 修复 Node.js v24 兼容:改为先读取 body 再构造 Request
- 修复 HTTP keep-alive 死锁:按 Content-Length 精确读取 body
- 修复服务器崩溃:connectionHandle 加 try-catch,单连接错误不影响全局
- 修复 URL 写死:webfont.shenzilong.com 改为 localhost
- 复制按钮加"已复制/复制失败"反馈提示
- TEMP_MAX_FILES 改为环境变量可配置
- dev 环境默认启用临时上传和管理员上传
- CSS 代码默认展示,使用技巧不再折叠

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:11:55 +08:00

99 lines
3.0 KiB
TypeScript

/**
* 轻量 multipart/form-data 解析器,不依赖外部库
*/
export interface MultipartFile {
/** 表单字段名 */
name: string;
/** 原始文件名 */
filename: string;
/** 文件二进制数据 */
data: Uint8Array;
}
export interface MultipartParseResult {
files: MultipartFile[];
}
export function parseMultipart(contentType: string, body: ArrayBuffer): MultipartParseResult {
if (!body || body.byteLength === 0) {
return { files: [] };
}
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
if (!boundaryMatch) throw new Error("No boundary found");
const boundary = boundaryMatch[1].replace(/^"(.*)"$/, "$1");
const encoder = new TextEncoder();
const decoder = new TextDecoder("utf-8");
const bodyBytes = new Uint8Array(body);
const delimiter = encoder.encode("\r\n--" + boundary);
/** 在字节数组中查找子串位置 */
function findBytes(haystack: Uint8Array, needle: Uint8Array, offset: number): number {
for (let i = offset; i <= haystack.length - needle.length; i++) {
let match = true;
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
match = false;
break;
}
}
if (match) return i;
}
return -1;
}
const files: MultipartFile[] = [];
// 跳过起始边界 "--boundary\r\n"
const startBoundary = encoder.encode("--" + boundary + "\r\n");
let pos = 0;
const sbPos = findBytes(bodyBytes, startBoundary, pos);
if (sbPos === -1) throw new Error("Invalid multipart: no start boundary");
pos = sbPos + startBoundary.length;
while (pos < bodyBytes.length) {
// 查找 headers 和 body 的分界 "\r\n\r\n"
const headerEnd = findBytes(bodyBytes, encoder.encode("\r\n\r\n"), pos);
if (headerEnd === -1) break;
const headerText = decoder.decode(bodyBytes.slice(pos, headerEnd));
const bodyStart = headerEnd + 4;
// 查找下一个 boundary
const nextBoundary = findBytes(bodyBytes, delimiter, bodyStart);
if (nextBoundary === -1) break;
// part body 去掉末尾的 "\r\n"
const partBody = bodyBytes.slice(bodyStart, nextBoundary);
const actualBody = partBody.length >= 2 && partBody[partBody.length - 1] === 10 && partBody[partBody.length - 2] === 13
? partBody.slice(0, partBody.length - 2)
: partBody;
// 从 Content-Disposition 中解析字段名和文件名
const nameMatch = headerText.match(/name="([^"]*)"/);
const filenameMatch = headerText.match(/filename="([^"]*)"/);
if (nameMatch) {
files.push({
name: nameMatch[1],
filename: filenameMatch?.[1] ?? "",
data: actualBody,
});
}
pos = nextBoundary + delimiter.length;
// 检查 boundary 后面是否紧跟 "--"(结束标记)
if (pos + 2 <= bodyBytes.length && bodyBytes[pos] === 45 && bodyBytes[pos + 1] === 45) {
break;
}
// 跳过 boundary 后的 "\r\n"
if (pos < bodyBytes.length && bodyBytes[pos] === 13 && bodyBytes[pos + 1] === 10) {
pos += 2;
}
}
return { files };
}