UNPKG

@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
"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