mirror of
https://github.com/2234839/web-font.git
synced 2026-04-29 21:00:45 +08:00
- 前端UI重构:字体选择器、文本预览、CSS输出+复制、下载字体、上传区域 - 后端新增API:/api/fonts(列出字体)、/api/config(公开配置)、/api/upload(上传字体) - 字体查找支持模糊匹配(精确 > 前缀 > 包含) - 上传功能分两种模式:临时上传(ENV开关,FIFO上限10个)和管理员上传(API Key认证) - 新增multipart解析器、配置模块、文件系统抽象(writeFile/readdir/mkdir/unlink) - 后端启动时等待运行时初始化完成,确保接口可用 - dev-all.ts后端启用tsx watch模式自动重载 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
/**
|
|
* 轻量 multipart/form-data 解析器,不依赖外部库
|
|
*/
|
|
export interface MultipartFile {
|
|
/** 表单字段名 */
|
|
name: string;
|
|
/** 原始文件名 */
|
|
filename: string;
|
|
/** 文件二进制数据 */
|
|
data: Uint8Array;
|
|
}
|
|
|
|
export function parseMultipart(contentType: string, body: ArrayBuffer): MultipartFile {
|
|
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);
|
|
const endDelimiter = encoder.encode("--" + 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) {
|
|
// 检查是否到达结束边界
|
|
if (findBytes(bodyBytes, endDelimiter, pos - 2) !== -1) break;
|
|
|
|
// 查找 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 后的 "\r\n"
|
|
if (pos < bodyBytes.length && bodyBytes[pos] === 13 && bodyBytes[pos + 1] === 10) {
|
|
pos += 2;
|
|
}
|
|
}
|
|
|
|
return { ...files[0], files } as MultipartFile & { files: MultipartFile[] };
|
|
}
|