import { IncomingMessage, ServerResponse } from 'node:http'; type JsonValue = Record | Array; export function sendJson(response: ServerResponse, statusCode: number, payload: JsonValue): void { const body = JSON.stringify(payload); response.writeHead(statusCode, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body), }); response.end(body); } export function readBody(request: IncomingMessage): Promise> { return new Promise((resolve) => { let body = ''; request.on('data', (chunk) => { body += chunk; }); request.on('end', () => { if (!body) { resolve({}); return; } try { resolve(JSON.parse(body)); } catch { resolve({}); } }); }); } /** * 从 multipart/form-data 请求中读取上传文件 * 简易实现,支持单文件上传 */ export function readMultipartFile(request: IncomingMessage): Promise<{ fileName: string; buffer: Buffer }> { return new Promise((resolve, reject) => { const contentType = request.headers['content-type'] || ''; const chunks: Buffer[] = []; request.on('data', (chunk: Buffer) => { chunks.push(chunk); }); request.on('end', () => { const rawBuffer = Buffer.concat(chunks); if (contentType.includes('multipart/form-data')) { try { const result = parseMultipart(rawBuffer, contentType); resolve(result); } catch (error) { reject(error); } } else { // Treat as raw file upload with filename from header const disposition = request.headers['content-disposition'] || ''; const fileNameMatch = disposition.match(/filename="?([^";\n]+)"?/); const fileName = fileNameMatch ? fileNameMatch[1] : 'upload.bin'; resolve({ fileName, buffer: rawBuffer }); } }); request.on('error', reject); }); } function parseMultipart(buffer: Buffer, contentType: string): { fileName: string; buffer: Buffer } { // 提取 boundary(可能带引号) const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/); if (!boundaryMatch) { throw new Error('Missing multipart boundary'); } const boundary = `--${boundaryMatch[1]}`; const boundaryBytes = Buffer.from(boundary, 'ascii'); // 在 buffer 中按 boundary 定位各 part let pos = bufferIndexOf(buffer, boundaryBytes, 0); if (pos === -1) { throw new Error('No file found in multipart data'); } // 跳过第一个 boundary + CRLF pos += boundaryBytes.length + 2; while (pos < buffer.length) { // 找到 header/body 分隔的 \r\n\r\n const headerEndMarker = Buffer.from('\r\n\r\n', 'ascii'); const headerEnd = bufferIndexOf(buffer, headerEndMarker, pos); if (headerEnd === -1) break; const headersStr = buffer.subarray(pos, headerEnd).toString('utf-8'); const bodyStart = headerEnd + 4; // 找下一个 boundary const nextBoundary = bufferIndexOf(buffer, boundaryBytes, bodyStart); // body 结束于下一个 boundary 前的 \r\n const bodyEnd = nextBoundary === -1 ? buffer.length : nextBoundary - 2; // 检查是否有 filename const fileNameMatch = headersStr.match(/filename="?([^";\r\n]+)"?/); if (fileNameMatch) { const fileName = decodeFileName(fileNameMatch[1].trim()); const fileBuffer = buffer.subarray(bodyStart, bodyEnd); return { fileName, buffer: fileBuffer }; } // 跳到下一个 part if (nextBoundary === -1) break; pos = nextBoundary + boundaryBytes.length; // 跳过 CRLF 或 -- (结束标记) if (buffer[pos] === 0x2d && buffer[pos + 1] === 0x2d) break; // '--' = end pos += 2; // skip \r\n } throw new Error('No file found in multipart data'); } function bufferIndexOf(buf: Buffer, search: Buffer, fromIndex: number): number { for (let i = fromIndex; i <= buf.length - search.length; i++) { let found = true; for (let j = 0; j < search.length; j++) { if (buf[i + j] !== search[j]) { found = false; break; } } if (found) return i; } return -1; } function decodeFileName(name: string): string { try { return decodeURIComponent(name); } catch { return name; } } /** * 从 URL 中提取路径参数 * 例如: extractPathParam('/api/tasks/abc123', '/api/tasks/') => 'abc123' */ export function extractPathParam(pathname: string, prefix: string): string { if (!pathname.startsWith(prefix)) return ''; const rest = pathname.substring(prefix.length); const slashIndex = rest.indexOf('/'); return slashIndex === -1 ? rest : rest.substring(0, slashIndex); } /** * 提取路径尾部 * 例如: extractPathTail('/api/tasks/abc123/export', '/api/tasks/') => 'abc123/export' */ export function extractPathTail(pathname: string, prefix: string): string { if (!pathname.startsWith(prefix)) return ''; return pathname.substring(prefix.length); }