197 lines
5.5 KiB
TypeScript
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(),
|
|
};
|
|
};
|