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.
393 lines • 15.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.netPipeStream = exports.netFiles = exports.netBody = exports.netAdaptRequestResponse = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const crypto_1 = __importDefault(require("crypto"));
const mime_types_1 = __importDefault(require("mime-types"));
const const_1 = require("../../const");
const createResponseObject = (socket) => {
const res = {
socket,
statusCode: 200,
headersSent: false,
writableEnded: false,
aborted: false,
writeHeaders: {
'content-type': 'text/plain',
'connection': 'keep-alive'
},
writeHead(status, context) {
this.statusCode = status;
if (context) {
for (const key in context)
this.setHeader(key, context[key]);
}
return this;
},
status(code) {
this.statusCode = code;
return this;
},
setHeader(key, value) {
this.writeHeaders[key.toLowerCase()] = value;
return this;
},
json(data) {
this.setHeader('content-type', 'application/json');
return this.send(JSON.stringify(data));
},
end(body = '') {
if (this.writableEnded)
return this;
socket.cork();
const content = Buffer.isBuffer(body) ? body : Buffer.from(String(body || ''));
if (!this.headersSent) {
this.setHeader('content-length', content.length);
}
//@ts-ignore
const statusMsg = const_1.HTTP_STATUS_MESSAGES[this.statusCode] || 'Unknown';
let head = `HTTP/1.1 ${this.statusCode} ${statusMsg}\r\n`;
for (const [key, value] of Object.entries(this.writeHeaders)) {
head += `${key}: ${value}\r\n`;
}
head += '\r\n';
const fullResponse = Buffer.concat([Buffer.from(head), content]);
if (socket.writable && !socket.destroyed) {
socket.write(fullResponse);
}
this.headersSent = true;
this.writableEnded = true;
socket.uncork();
return;
},
send(body) {
return this.end(body);
}
};
return res;
};
const CRLF = '\r\n';
const HEADER_END = '\r\n\r\n';
const netAdaptRequestResponse = (socket, callback) => {
socket.setNoDelay(true);
socket.setTimeout(60000);
if (socket._attached)
return;
socket._attached = true;
let buf = Buffer.allocUnsafe(64 * 1024);
let len = 0;
const ensure = (need) => {
if (buf.length >= need)
return;
let cap = buf.length;
while (cap < need)
cap *= 2;
const next = Buffer.allocUnsafe(cap);
buf.copy(next, 0, 0, len);
buf = next;
};
socket.on("error", (err) => {
if (err.code === "ECONNRESET" || err.code === "ECONNABORTED") {
return;
}
console.error("socket error:", err);
socket.destroy();
});
const onData = (chunk) => {
ensure(len + chunk.length);
chunk.copy(buf, len);
len += chunk.length;
while (true) {
const headerEnd = buf.indexOf(HEADER_END, 0);
if (headerEnd === -1)
break;
const headerStr = buf.toString("utf8", 0, headerEnd);
const lines = headerStr.split(CRLF);
const [method, path] = lines[0].split(" ");
if (!method || !path)
return socket.destroy();
let headers = {};
let contentLength = 0;
let keepAlive = true;
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
const idx = line.indexOf(":");
if (idx === -1)
continue;
const key = line.slice(0, idx).trim().toLowerCase();
const val = line.slice(idx + 1).trim();
headers[key] = val;
if (key === "content-length")
contentLength = parseInt(val) || 0;
if (key === "connection" && val.toLowerCase() === "close") {
keepAlive = false;
}
}
const bodyStart = headerEnd + 4;
const total = bodyStart + contentLength;
if (len < total)
break;
const body = contentLength > 0 ? buf.subarray(bodyStart, total) : null;
const req = {
socket,
method,
url: path,
path,
headers,
_body: body,
_bodyRead: false,
};
const res = createResponseObject(socket);
callback(req, res);
const remain = len - total;
if (remain > 0) {
buf.copy(buf, 0, total, len);
}
len = remain;
if (!keepAlive) {
socket.end();
return;
}
}
};
socket.on("data", onData);
};
exports.netAdaptRequestResponse = netAdaptRequestResponse;
const netBody = (req) => {
return new Promise((resolve, reject) => {
if (req._bodyRead)
return reject(new Error("Body already consumed"));
req._bodyRead = true;
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
if (contentLength === 0)
return resolve({});
let buffer = req._body || Buffer.alloc(0);
const socket = req.socket;
const onData = (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
if (buffer.length >= contentLength) {
cleanup();
try {
const bodyStr = buffer.subarray(0, contentLength).toString('utf8');
const contentType = req.headers['content-type'] || '';
if (contentType.includes('application/json')) {
resolve(JSON.parse(bodyStr));
}
else {
resolve(Object.fromEntries(new URLSearchParams(bodyStr)));
}
}
catch (e) {
reject(e);
}
}
};
const cleanup = () => {
socket.off('data', onData);
socket.off('error', reject);
};
if (buffer.length >= contentLength)
return onData(Buffer.alloc(0));
socket.on('data', onData);
socket.on('error', reject);
});
};
exports.netBody = netBody;
const netFiles = async (req, options) => {
const { socket } = req;
const temp = options.tempFileDir;
if (!fs_1.default.existsSync(temp)) {
fs_1.default.mkdirSync(temp, { recursive: true });
}
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(`--${boundary}`);
return new Promise((resolve, reject) => {
if (req._bodyRead)
return reject(new Error("Body already consumed"));
req._bodyRead = true;
let body = {};
let files = {};
let buffer = req._body || Buffer.alloc(0);
// let buffer: Buffer = Buffer.alloc(0);
let currentFileStream = null;
let file = null;
let headerParsed = false;
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
let totalBytesReceived = buffer.length;
const onData = (chunk) => {
if (chunk.length > 0) {
buffer = Buffer.concat([buffer, chunk]);
totalBytesReceived += chunk.length;
}
// console.log('onData loading');
// const data: Buffer = 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.subarray(0, headerEnd).toString();
buffer = buffer.subarray(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;
body[fieldName] = buffer.subarray(0, nextBoundary).toString().trim();
buffer = buffer.subarray(nextBoundary);
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);
headerParsed = true;
}
const boundaryIndex = buffer.indexOf(boundaryBuf);
if (boundaryIndex === -1) {
const safeLength = buffer.length - (boundaryBuf.length + 4);
if (safeLength > 0) {
const writeChunk = buffer.subarray(0, safeLength);
currentFileStream?.write(writeChunk);
file.size += writeChunk.length;
buffer = buffer.subarray(safeLength);
}
break;
}
const filePart = buffer.subarray(0, boundaryIndex);
currentFileStream?.write(filePart);
file.size += filePart.length;
currentFileStream?.end();
file.sizes = {
bytes: file.size,
kb: file.size / 1024,
mb: file.size / 1024 / 1024,
gb: file.size / 1024 / 1024 / 1024,
};
buffer = buffer.subarray(boundaryIndex + boundaryBuf.length);
headerParsed = false;
}
if (totalBytesReceived >= contentLength || buffer.toString().includes(boundary + "--")) {
socket.off('data', onData);
// if (currentFileStream) currentFileStream.end();
return resolve({ body, files });
}
}
catch (err) {
console.log(err);
socket.off('data', onData);
return reject(err);
}
};
socket.on('data', onData);
socket.on('error', (err) => {
console.log(err);
return reject(err);
});
if (buffer.length > 0)
onData(Buffer.alloc(0));
});
};
exports.netFiles = netFiles;
const netPipeStream = async ({ req, socket, filePath }) => {
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/");
let start = 0;
let end = fileSize - 1;
let statusCode = "200 OK";
let headers = [];
if (range && isVideo) {
const parts = range.replace(/bytes=/, "").split("-");
start = parseInt(parts[0], 10);
end = parts[1] ? parseInt(parts[1], 10) : end;
statusCode = "206 Partial Content";
headers.push(`Content-Range: bytes ${start}-${end}/${fileSize}`);
}
const chunkSize = end - start + 1;
headers.push(`Content-Type: ${contentType}`);
headers.push(`Accept-Ranges: bytes`);
headers.push(`Content-Length: ${chunkSize}`);
headers.push(`Connection: keep-alive`);
socket.write(`HTTP/1.1 ${statusCode}\r\n${headers.join('\r\n')}\r\n\r\n`);
const stream = fs_1.default.createReadStream(filePath, { start, end });
stream.on("data", (chunk) => {
const canWrite = socket.write(chunk);
if (!canWrite) {
stream.pause();
}
});
socket.on("drain", () => {
stream.resume();
});
stream.on("end", () => {
socket.end();
});
stream.on("error", (err) => {
console.error("Stream Error:", err);
socket.destroy();
});
socket.on("close", () => {
stream.destroy();
});
return stream;
};
exports.netPipeStream = netPipeStream;
//# sourceMappingURL=index.js.map