import multer from 'multer'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; import { Request } from 'express'; // Use /tmp for uploads in production environments (like Hugging Face Spaces) // This ensures we have write permissions const uploadsDir = process.env.NODE_ENV === 'production' ? path.join('/tmp', 'uploads') : path.join(process.cwd(), 'uploads'); // Safely create uploads directory with error handling try { if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); console.log(`✅ Created uploads directory: ${uploadsDir}`); } else { console.log(`✅ Uploads directory exists: ${uploadsDir}`); } // Test write permissions const testFile = path.join(uploadsDir, 'test-permissions.txt'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); console.log(`✅ Upload directory is writable: ${uploadsDir}`); } catch (error) { console.warn(`❌ Failed to create or write to uploads directory at ${uploadsDir}:`, error); console.log('File uploads may not work properly'); } // Configure multer for file uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { try { // Create subdirectories by date for organization const dateDir = new Date().toISOString().split('T')[0]; const fullPath = path.join(uploadsDir, dateDir); console.log(`Creating upload destination: ${fullPath} for file: ${file.originalname}`); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); console.log(`Created upload subdirectory: ${fullPath}`); } cb(null, fullPath); } catch (error) { console.error('Failed to create upload directory:', error); console.log(`Falling back to base directory: ${uploadsDir}`); // Fallback to base uploads directory cb(null, uploadsDir); } }, filename: (req, file, cb) => { // Generate unique filename with timestamp and random string const timestamp = Date.now(); const randomString = crypto.randomBytes(8).toString('hex'); const ext = path.extname(file.originalname); const baseName = path.basename(file.originalname, ext); // Sanitize filename const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '_'); const filename = `${timestamp}_${randomString}_${sanitizedBaseName}${ext}`; cb(null, filename); } }); // File filter to accept specific file types const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => { const allowedMimeTypes = [ 'application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'text/plain', 'text/markdown', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/json' ]; if (allowedMimeTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed types: PDF, images (JPEG, PNG, GIF, WebP), text files, Word documents, JSON`)); } }; // Configure multer with size limits export const upload = multer({ storage, fileFilter, limits: { fileSize: 50 * 1024 * 1024, // 50MB limit files: 10 // Maximum 10 files per upload } }); // File processing utilities export class FileProcessor { static async getFileInfo(filePath: string): Promise<{ size: number; mimeType: string; exists: boolean; }> { try { const stats = await fs.promises.stat(filePath); return { size: stats.size, mimeType: await this.getMimeType(filePath), exists: true }; } catch (error) { return { size: 0, mimeType: '', exists: false }; } } static async getMimeType(filePath: string): Promise { const ext = path.extname(filePath).toLowerCase(); const mimeTypes: Record = { '.pdf': 'application/pdf', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.txt': 'text/plain', '.md': 'text/markdown', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.json': 'application/json' }; return mimeTypes[ext] || 'application/octet-stream'; } static async readTextFile(filePath: string): Promise { try { const content = await fs.promises.readFile(filePath, 'utf-8'); return content; } catch (error) { throw new Error(`Failed to read text file: ${error}`); } } static async deleteFile(filePath: string): Promise { try { await fs.promises.unlink(filePath); return true; } catch (error) { console.error(`Failed to delete file ${filePath}:`, error); return false; } } static isTextFile(mimeType: string): boolean { return mimeType.startsWith('text/') || mimeType === 'application/json' || mimeType.includes('document'); } static isImageFile(mimeType: string): boolean { return mimeType.startsWith('image/'); } static isPdfFile(mimeType: string): boolean { return mimeType === 'application/pdf'; } static requiresOCR(mimeType: string): boolean { return this.isImageFile(mimeType) || this.isPdfFile(mimeType); } } // Upload validation middleware export const validateUpload = (req: Request, res: any, next: any) => { if (!req.files || (Array.isArray(req.files) && req.files.length === 0)) { return res.status(400).json({ error: 'No files uploaded', message: 'Please select at least one file to upload' }); } next(); }; export default upload;