// src/uxio.js
const busboy = require("busboy");
const path = require("path");
const os = require("os");
const fs = require("fs");
const files = require("./files");
/**
* Uxio captures file(s) for a specific route, creates file object(s), places them in the req.uxio.files array
*
* @typedef {object} UxioFile
* @property {string} fieldname - The name of the form field.
* @property {string} filename - The original name of the file.
* @property {string} encoding - The encoding of the file.
* @property {string} mimeType - The MIME type of the file.
* @property {string} tempFilePath - The full path to the temporary file on the disk.
* @property {number} size - The size of the file in bytes.
*/
/**
* This is the uxio file object passed in the req.uxio, It give yiu control of the uploaded file in the tempCache.
*
* @typedef {object} UxioObject
* @property {function(): boolean} hasFile - Checks if any file was uploaded in the request.
*
* @property {function} hasFiles - Checks if files with specific field names exist.
* @param {string|string[]} fieldNames - The name(s) of the file field(s) to check for.
* @returns {boolean} True if any of the specified file fields exist, otherwise false.
*
* @property {UxioFile[]} files - An array of objects, each representing an uploaded file.
* @property {function(): void} cleanup - Manually cleans up the temporary cache directory.
*/
/**
* Express/Connect-compatible middleware for handling multipart/form-data uploads.
* This middleware parses uploaded files and form fields, making them available on
* `req.uxio` and `req.body` objects.
*
* It automatically cleans up temporary files once the response is finished or closed.
*
* @param {Object} [options] - Optional configuration for the middleware.
* @returns {Function} Express/Connect-compatible middleware function.
*/
function Uxio(options = {}) {
return (req, res, next) => {
if (
req.method === "POST" &&
req.headers["content-type"] &&
req.headers["content-type"].startsWith("multipart/form-data")
) {
const requestId = Date.now().toString(36) + Math.random().toString(36).substring(2);
const tempCacheDir = path.join(os.tmpdir(), `.uxio-cache-${requestId}`);
fs.mkdirSync(tempCacheDir, { recursive: true });
req.uxio = {
get hasFile() {
return this.files.length > 0;
},
hasFiles: (fieldNames) => {
if (Array.isArray(fieldNames)) {
return this.files.some((file) => fieldNames.includes(file.fieldname));
} else if (typeof fieldNames === "string") {
return this.files.some((file) => file.fieldname === fieldNames);
}
return false;
},
files: [],
cleanup: () => {
if (fs.existsSync(tempCacheDir)) {
fs.rmSync(tempCacheDir, { recursive: true, force: true });
console.log(`Cleaned up temp directory: ${tempCacheDir}`);
}
},
};
res.on("finish", () => {
req.uxio.cleanup();
});
res.on("close", () => {
req.uxio.cleanup();
});
const bb = busboy({ headers: req.headers });
bb.on("file", (fieldname, file, info) => {
const { filename, encoding, mimeType } = info;
const tempFilePath = path.join(
tempCacheDir,
`${fieldname}-${filename}`,
);
const writeStream = fs.createWriteStream(tempFilePath);
req.uxio.files.push({
fieldname,
filename,
encoding,
mimeType,
tempFilePath,
size: 0,
});
file.on("data", (data) => {
const fileObj = req.uxio.files.find((f) => f.fieldname === fieldname);
if (fileObj) {
fileObj.size += data.length;
}
});
file.pipe(writeStream);
});
bb.on("field", (fieldname, val, info) => {
req.body = req.body || {};
req.body[fieldname] = val;
});
bb.on("close", () => {
next();
});
req.pipe(bb);
} else {
next();
}
};
};
module.exports = Object.assign(Uxio, { files });