@mvp-factory/holy-upload
Version:
File upload processing system extracted from Holy Habit project with security validation and image optimization
408 lines • 14.2 kB
JavaScript
"use strict";
/**
* Express Upload Middleware
*
* Express.js integration with Multer for file uploads
* Extracted from Holy Habit Express integration
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressUploadMiddleware = void 0;
const multer_1 = __importDefault(require("multer"));
const UploadHandler_1 = require("./UploadHandler");
const Upload_1 = require("../types/Upload");
const path_1 = __importDefault(require("path"));
class ExpressUploadMiddleware {
constructor(config) {
this.uploadHandler = new UploadHandler_1.UploadHandler(config);
this.multerInstance = this.createMulterInstance();
}
/**
* Create Multer instance with configuration
*/
createMulterInstance() {
const config = this.uploadHandler.getConfig();
const storage = multer_1.default.memoryStorage(); // Use memory storage for better control
const fileFilter = (req, file, cb) => {
// Basic MIME type check
if (config.allowedMimeTypes && !config.allowedMimeTypes.includes(file.mimetype)) {
cb(new Error(`File type ${file.mimetype} is not allowed`));
return;
}
// Basic extension check
if (config.allowedExtensions) {
const ext = path_1.default.extname(file.originalname).toLowerCase().slice(1);
if (!config.allowedExtensions.includes(ext)) {
cb(new Error(`File extension .${ext} is not allowed`));
return;
}
}
cb(null, true);
};
return (0, multer_1.default)({
storage,
fileFilter,
limits: {
fileSize: config.maxSize,
files: 10 // Maximum 10 files per request
}
});
}
/**
* Single file upload middleware
*
* @param fieldName - Field name for file input
* @param options - Upload options
* @returns Express middleware
*/
single(fieldName = 'file', options = {}) {
const multerMiddleware = this.multerInstance.single(fieldName);
return async (req, res, next) => {
try {
// Apply Multer middleware
multerMiddleware(req, res, async (err) => {
if (err) {
return this.handleMulterError(err, res, next);
}
if (!req.file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
// Attach upload methods to request
this.attachUploadMethods(req, options);
next();
});
}
catch (error) {
this.handleError(error, res, next);
}
};
}
/**
* Multiple files upload middleware
*
* @param fieldName - Field name for file input
* @param maxCount - Maximum number of files
* @param options - Upload options
* @returns Express middleware
*/
multiple(fieldName = 'files', maxCount = 5, options = {}) {
const multerMiddleware = this.multerInstance.array(fieldName, maxCount);
return async (req, res, next) => {
try {
// Apply Multer middleware
multerMiddleware(req, res, async (err) => {
if (err) {
return this.handleMulterError(err, res, next);
}
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
return res.status(400).json({
success: false,
error: 'No files uploaded'
});
}
// Attach upload methods to request
this.attachUploadMethods(req, options);
next();
});
}
catch (error) {
this.handleError(error, res, next);
}
};
}
/**
* Fields upload middleware (mixed uploads)
*
* @param fields - Array of field configurations
* @param options - Upload options
* @returns Express middleware
*/
fields(fields, options = {}) {
const multerMiddleware = this.multerInstance.fields(fields);
return async (req, res, next) => {
try {
// Apply Multer middleware
multerMiddleware(req, res, async (err) => {
if (err) {
return this.handleMulterError(err, res, next);
}
// Attach upload methods to request
this.attachUploadMethods(req, options);
next();
});
}
catch (error) {
this.handleError(error, res, next);
}
};
}
/**
* Create upload route handler
*
* @param type - Upload type ('single' or 'multiple')
* @param fieldName - Field name
* @param options - Upload options
* @returns Express route handler
*/
createUploadRoute(type = 'single', fieldName = 'file', options = {}) {
return async (req, res) => {
try {
const userId = this.extractUserId(req);
if (type === 'single') {
if (!req.file) {
res.status(400).json({
success: false,
error: 'No file uploaded'
});
return;
}
const result = await this.uploadHandler.uploadSingle(req.file, userId, {
validation: options.config,
prefix: options.fieldName
});
res.json(result);
}
else {
const files = req.files;
if (!files || files.length === 0) {
res.status(400).json({
success: false,
error: 'No files uploaded'
});
return;
}
const result = await this.uploadHandler.uploadMultiple(files, userId, {
validation: options.config,
prefix: options.fieldName,
continueOnError: true
});
res.json(result);
}
}
catch (error) {
console.error('Upload route error:', error);
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
}
/**
* Create complete upload endpoint with middleware
*
* @param type - Upload type
* @param fieldName - Field name
* @param options - Upload options
* @returns Array of middleware functions
*/
createCompleteEndpoint(type = 'single', fieldName = 'file', options = {}) {
const middleware = type === 'single'
? this.single(fieldName, options)
: this.multiple(fieldName, options.maxFiles || 5, options);
const handler = this.createUploadRoute(type, fieldName, options);
return [middleware, handler];
}
/**
* Create file serving middleware
*
* @param urlPath - URL path for files (e.g., '/uploads')
* @returns Express middleware
*/
static createFileServing(urlPath = '/uploads') {
return (req, res, next) => {
const filename = req.params.filename || req.params[0];
if (!filename) {
res.status(400).json({ error: 'Filename required' });
return;
}
// Security: prevent path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
res.status(400).json({ error: 'Invalid filename' });
return;
}
// Let Express static middleware handle the actual file serving
next();
};
}
/**
* Create delete endpoint
*
* @returns Express route handler
*/
createDeleteEndpoint() {
return async (req, res) => {
try {
const filename = req.params.filename;
const userId = this.extractUserId(req);
if (!filename) {
res.status(400).json({
success: false,
error: 'Filename required'
});
return;
}
const deleted = await this.uploadHandler.deleteFile(filename, userId);
if (deleted) {
res.json({ success: true });
}
else {
res.status(404).json({
success: false,
error: 'File not found or permission denied'
});
}
}
catch (error) {
console.error('Delete endpoint error:', error);
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
}
/**
* Create storage info endpoint
*
* @returns Express route handler
*/
createStorageInfoEndpoint() {
return async (req, res) => {
try {
const userId = this.extractUserId(req);
const storageInfo = await this.uploadHandler.getStorageInfo(userId);
res.json({
success: true,
storage: storageInfo
});
}
catch (error) {
console.error('Storage info endpoint error:', error);
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
}
/**
* Attach upload methods to request object
*/
attachUploadMethods(req, options) {
req.uploadHandler = this.uploadHandler;
req.uploadSingle = async () => {
if (!req.file) {
throw new Upload_1.UploadError('No file available', 'NO_FILE');
}
const userId = this.extractUserId(req);
return await this.uploadHandler.uploadSingle(req.file, userId, {
validation: options.config,
prefix: options.fieldName
});
};
req.uploadMultiple = async () => {
const files = req.files;
if (!files || files.length === 0) {
throw new Upload_1.UploadError('No files available', 'NO_FILES');
}
const userId = this.extractUserId(req);
return await this.uploadHandler.uploadMultiple(files, userId, {
validation: options.config,
prefix: options.fieldName,
continueOnError: true
});
};
req.getUserId = () => {
return this.extractUserId(req);
};
}
/**
* Extract user ID from request
*/
extractUserId(req) {
// Try multiple common patterns for user ID
return req.user?.id ||
req.user?.userId ||
req.session?.userId ||
req.session?.user?.id ||
req.headers['x-user-id'] ||
undefined;
}
/**
* Handle Multer errors
*/
handleMulterError(error, res, next) {
if (error instanceof multer_1.default.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
res.status(413).json({
success: false,
error: 'File too large'
});
break;
case 'LIMIT_FILE_COUNT':
res.status(400).json({
success: false,
error: 'Too many files'
});
break;
case 'LIMIT_UNEXPECTED_FILE':
res.status(400).json({
success: false,
error: 'Unexpected file field'
});
break;
default:
res.status(400).json({
success: false,
error: error.message
});
}
}
else {
res.status(400).json({
success: false,
error: error.message
});
}
}
/**
* Handle general errors
*/
handleError(error, res, next) {
console.error('Upload middleware error:', error);
if (error instanceof Upload_1.UploadError) {
res.status(error.statusCode).json({
success: false,
error: error.message
});
}
else {
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
}
/**
* Get upload handler instance
*/
getUploadHandler() {
return this.uploadHandler;
}
/**
* Update configuration
*/
updateConfig(config) {
this.uploadHandler.updateConfig(config);
this.multerInstance = this.createMulterInstance(); // Recreate Multer instance
}
}
exports.ExpressUploadMiddleware = ExpressUploadMiddleware;
//# sourceMappingURL=ExpressUploadMiddleware.js.map