Files
cim_summary/backend/src/middleware/upload.ts

197 lines
5.5 KiB
TypeScript

import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { Request, Response, NextFunction } from 'express';
import { config } from '../config/env';
import { logger } from '../utils/logger';
// Ensure upload directory exists
const uploadDir = path.join(process.cwd(), config.upload.uploadDir);
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// File filter function
const fileFilter = (req: Request, file: any, cb: multer.FileFilterCallback) => {
// Check file type - allow PDF and text files for testing
const allowedTypes = ['application/pdf', 'text/plain', 'text/html'];
if (!allowedTypes.includes(file.mimetype)) {
const error = new Error(`File type ${file.mimetype} is not allowed. Only PDF and text files are accepted.`);
logger.warn(`File upload rejected - invalid type: ${file.mimetype}`, {
originalName: file.originalname,
size: file.size,
ip: req.ip,
});
return cb(error);
}
// Check file extension - allow PDF and text extensions for testing
const ext = path.extname(file.originalname).toLowerCase();
if (!['.pdf', '.txt', '.html'].includes(ext)) {
const error = new Error(`File extension ${ext} is not allowed. Only .pdf, .txt, and .html files are accepted.`);
logger.warn(`File upload rejected - invalid extension: ${ext}`, {
originalName: file.originalname,
size: file.size,
ip: req.ip,
});
return cb(error);
}
logger.info(`File upload accepted: ${file.originalname}`, {
originalName: file.originalname,
size: file.size,
mimetype: file.mimetype,
ip: req.ip,
});
cb(null, true);
};
// Storage configuration
const storage = multer.diskStorage({
destination: (req: Request, _file: any, cb) => {
// Create user-specific directory
const userId = (req as any).user?.userId || 'anonymous';
const userDir = path.join(uploadDir, userId);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
}
cb(null, userDir);
},
filename: (_req: Request, file: any, cb) => {
// Generate unique filename with timestamp
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 15);
const ext = path.extname(file.originalname);
const filename = `${timestamp}-${randomString}${ext}`;
cb(null, filename);
},
});
// Create multer instance
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: config.upload.maxFileSize, // 100MB default
files: 1, // Only allow 1 file per request
},
});
// Error handling middleware for multer
export const handleUploadError = (error: any, req: Request, res: Response, next: NextFunction): void => {
if (error instanceof multer.MulterError) {
logger.error('Multer error during file upload:', {
error: error.message,
code: error.code,
field: error.field,
originalName: req.file?.originalname,
ip: req.ip,
});
switch (error.code) {
case 'LIMIT_FILE_SIZE':
res.status(400).json({
success: false,
error: 'File too large',
message: `File size must be less than ${config.upload.maxFileSize / (1024 * 1024)}MB`,
});
return;
case 'LIMIT_FILE_COUNT':
res.status(400).json({
success: false,
error: 'Too many files',
message: 'Only one file can be uploaded at a time',
});
return;
case 'LIMIT_UNEXPECTED_FILE':
res.status(400).json({
success: false,
error: 'Unexpected file field',
message: 'File must be uploaded using the correct field name',
});
return;
default:
res.status(400).json({
success: false,
error: 'File upload error',
message: error.message,
});
return;
}
}
if (error) {
logger.error('File upload error:', {
error: error.message,
originalName: req.file?.originalname,
ip: req.ip,
});
res.status(400).json({
success: false,
error: 'File upload failed',
message: error.message,
});
return;
}
next();
};
// Main upload middleware with timeout handling
export const uploadMiddleware = (req: Request, res: Response, next: NextFunction) => {
// Set a timeout for the upload
const uploadTimeout = setTimeout(() => {
logger.error('Upload timeout for request:', {
ip: req.ip,
userAgent: req.get('User-Agent'),
});
res.status(408).json({
success: false,
error: 'Upload timeout',
message: 'Upload took too long to complete',
});
}, 300000); // 5 minutes timeout
// Clear timeout on successful upload
const originalNext = next;
next = (err?: any) => {
clearTimeout(uploadTimeout);
originalNext(err);
};
upload.single('document')(req, res, next);
};
// Combined middleware for file uploads
export const handleFileUpload = [
uploadMiddleware,
handleUploadError,
];
// Utility function to clean up uploaded files
export const cleanupUploadedFile = (filePath: string): void => {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info(`Cleaned up uploaded file: ${filePath}`);
}
} catch (error) {
logger.error(`Failed to cleanup uploaded file: ${filePath}`, error);
}
};
// Utility function to get file info
export const getFileInfo = (file: any) => {
return {
originalName: file.originalname,
filename: file.filename,
path: file.path,
size: file.size,
mimetype: file.mimetype,
uploadedAt: new Date(),
};
};