UNPKG

tspace-spear

Version:

tspace-spear is a lightweight, high-performance API framework for Node.js that leverages the native HTTP server and supports uWebSockets.js (C++) for maximum speed and efficiency.

357 lines 13.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.uWSPipeStream = exports.uWSfiles = exports.uWSBody = exports.uWSAdaptRequestResponse = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const mime_types_1 = __importDefault(require("mime-types")); const crypto_1 = __importDefault(require("crypto")); const const_1 = require("../../const"); const utils_1 = require("../../utils"); const uWSAdaptRequestResponse = (uwsReq, uwsRes) => { const headers = {}; uwsReq.forEach((key, value) => { headers[key.toLowerCase()] = value; }); const req = { uWS: uwsReq, method: String(uwsReq.getMethod()).toUpperCase(), url: uwsReq.getUrl() + (uwsReq.getQuery() ? `?${uwsReq.getQuery()}` : ""), headers: headers, }; const _writeHead = (status, context) => { const statusMessage = const_1.HTTP_STATUS_MESSAGES[status] || const_1.HTTP_STATUS_MESSAGES[500]; res.uWS.writeStatus(`${status} ${statusMessage}`); res.uWS.writeHeader(Object.keys(context)[0], Object.values(context)[0]); return res; }; const res = { writeHeader: (key, value) => { if (!res.aborted) { uwsRes.writeHeader(key, value); } return res; }, setHeader: (key, value) => { if (!res.aborted) { uwsRes.writeHeader(key, value); } return res; }, writeHead(status, context) { res.writeHeaders = { ...res.writeHeaders, [status]: context, }; res.headersSent = true; res.statusCode = status; return res; }, writeStatus: (status) => { if (!res.aborted) { res.uWS.writeStatus(status); } return res; }, end: (str) => { if (res.aborted) { return; } if (str === undefined) { return; } uwsRes.cork(() => { if (!res.aborted) { res.aborted = true; res.writableEnded = true; for (const h in res.writeHeaders) { _writeHead(+h, res.writeHeaders[h]); } uwsRes.end(str); return; } }); }, writableEnded: false, aborted: false, writeHeaders: Object.create(null), headersSent: false, statusCode: 200, uWS: uwsRes, }; uwsRes.onAborted(() => { res.aborted = true; }); return { req, res }; }; exports.uWSAdaptRequestResponse = uWSAdaptRequestResponse; const uWSBody = (req, res) => { return new Promise((resolve, reject) => { let buffer = []; res.uWS.onAborted(() => { reject(new Error("Request aborted")); }); res.uWS.onData(async (chunk, isLast) => { buffer.push(Buffer.from(chunk)); if (!isLast) return; const payload = Buffer.concat(buffer).toString("utf-8"); const contentType = req.headers["content-type"]?.toLowerCase() ?? null; try { const body = await (0, utils_1.normalizeRequestBody)({ contentType, payload }); return resolve(body); } catch (err) { return reject(err); } }); }); }; exports.uWSBody = uWSBody; const uWSfiles = async ({ req, res, options, }) => { const temp = options.tempFileDir; if (!fs_1.default.existsSync(temp)) { try { fs_1.default.mkdirSync(temp, { recursive: true }); } catch { } } const removeTemp = (fileTemp, ms) => { const remove = () => { try { fs_1.default.unlinkSync(fileTemp); } catch (err) { } }; setTimeout(remove, ms); }; const contentType = req.headers["content-type"] ?? ""; const boundary = contentType.split("boundary=")[1]; if (!boundary) { throw new Error("Invalid multipart/form-data (no boundary)"); } const boundaryBuf = Buffer.from(`\r\n--${boundary}`); return new Promise((resolve, reject) => { let body = {}; let files = {}; let buffer = Buffer.alloc(0); let currentFileStream = null; let file = null; let headerParsed = false; let aborted = false; const fail = (err) => { if (aborted) return; aborted = true; try { currentFileStream?.destroy(); } catch { } try { file?.tempFilePath && fs_1.default.unlinkSync(file.tempFilePath); } catch { } try { res.uWS.close(); } catch { } return reject(err); }; res.uWS.onData((chunk, isLast) => { if (aborted) return; const data = Buffer.from(new Uint8Array(chunk)); buffer = buffer.length === 0 ? data : Buffer.concat([buffer, data]); try { while (true) { if (!headerParsed) { const headerEnd = buffer.indexOf("\r\n\r\n"); if (headerEnd === -1) break; const header = buffer.slice(0, headerEnd).toString(); buffer = buffer.slice(headerEnd + 4); const disposition = header.match(/name="([^"]+)"(?:; filename="([^"]+)")?/); if (!disposition) continue; const fieldName = disposition[1]; const fileName = disposition[2]; if (!fileName) { const nextBoundary = buffer.indexOf(boundaryBuf); if (nextBoundary === -1) break; const value = buffer.slice(0, nextBoundary).toString().trim(); body[fieldName] = value; buffer = buffer.slice(nextBoundary + boundaryBuf.length); continue; } const contentTypeMatch = header.match(/Content-Type: ([^\r\n]+)/); const mimetype = contentTypeMatch ? contentTypeMatch[1] : "application/octet-stream"; const extension = mime_types_1.default.extension(mimetype) || path_1.default.extname(fileName).replace(".", "") || "bin"; const tempFilename = crypto_1.default.randomBytes(16).toString("hex"); const filePath = path_1.default.join(path_1.default.resolve(), `${temp}/${tempFilename}`); currentFileStream = fs_1.default.createWriteStream(filePath); file = { name: fileName, tempFilePath: filePath, tempFileName: tempFilename, mimetype: mimetype, extension: extension, size: 0, sizes: { bytes: 0, kb: 0, mb: 0, gb: 0, }, write: (to) => { return new Promise((resolve, reject) => { fs_1.default .createReadStream(filePath) .pipe(fs_1.default.createWriteStream(to)) .on("finish", () => { return resolve(null); }) .on("error", (err) => { return reject(err); }); }); }, remove: () => { return new Promise((resolve) => setTimeout(() => { fs_1.default.unlinkSync(filePath); return resolve(null); }, 100)); }, }; if (!files[fieldName]) files[fieldName] = []; files[fieldName].push(file); if (options.removeTempFile.remove) { removeTemp(filePath, options.removeTempFile.ms); } headerParsed = true; } const boundaryIndex = buffer.indexOf(boundaryBuf); if (boundaryIndex === -1) { const safeLength = buffer.length - boundaryBuf.length; if (safeLength > 0) { const writeChunk = buffer.slice(0, safeLength); currentFileStream.write(writeChunk); file.size += writeChunk.length; file.sizes = { bytes: file.size, kb: file.size / 1024, mb: file.size / 1024 / 1024, gb: file.size / 1024 / 1024 / 1024, }; if (file.size > options.limit) { return fail(new Error(`File too large (limit ${options.limit} bytes)`)); } buffer = buffer.slice(safeLength); } break; } const filePart = buffer.slice(0, boundaryIndex); currentFileStream.write(filePart); file.size += filePart.length; file.sizes = { bytes: file.size, kb: file.size / 1024, mb: file.size / 1024 / 1024, gb: file.size / 1024 / 1024 / 1024, }; if (file.size > options.limit) { return fail(new Error(`File too large (limit ${options.limit} bytes)`)); } currentFileStream.end(); currentFileStream = null; file = null; buffer = buffer.slice(boundaryIndex + boundaryBuf.length); headerParsed = false; } if (isLast && !aborted) { if (currentFileStream) currentFileStream.end(); return resolve({ body, files }); } } catch (err) { return fail(err); } }); }); }; exports.uWSfiles = uWSfiles; const uWSPipeStream = async ({ req, res, filePath }) => { //@ts-ignore const uwsRes = res.uWS; const stat = fs_1.default.statSync(filePath); const fileSize = stat.size; const range = req.headers["range"] ?? null; const contentType = mime_types_1.default.lookup(filePath) || "application/octet-stream"; const isVideo = contentType.startsWith("video/"); //@ts-ignore let aborted = res.aborted || false; let stream; let start = 0; let end = fileSize - 1; uwsRes.onAborted(() => { aborted = true; if (stream) stream.destroy(); }); if (range && isVideo) { const parts = range.replace(/bytes=/, "").split("-"); start = parseInt(parts[0], 10); end = parts[1] ? parseInt(parts[1], 10) : end; uwsRes.writeStatus("206 Partial Content"); uwsRes.writeHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`); } else { uwsRes.writeStatus("200 OK"); } const chunkSize = end - start + 1; uwsRes.writeHeader("Content-Type", contentType); uwsRes.writeHeader("Accept-Ranges", "bytes"); uwsRes.writeHeader("Content-Length", chunkSize.toString()); stream = fs_1.default.createReadStream(filePath, { start, end }); stream.pause(); stream.on("data", (chunk) => { if (aborted) return; const ok = uwsRes.cork(() => uwsRes.write(chunk)); if (!ok) stream.pause(); }); uwsRes.onWritable(() => { if (aborted) return false; stream.resume(); return true; }); stream.on("end", () => { if (!aborted) { uwsRes.cork(() => { uwsRes.end(); }); } }); stream.on("error", () => { if (!aborted) { uwsRes.cork(() => { uwsRes.writeStatus("500 Internal Server Error").end(); }); } }); stream.resume(); return stream; }; exports.uWSPipeStream = uWSPipeStream; //# sourceMappingURL=index.js.map