|
import multer from 'multer'; |
|
import path from 'path'; |
|
import fs from 'fs'; |
|
import crypto from 'crypto'; |
|
import { Request } from 'express'; |
|
|
|
|
|
|
|
const uploadsDir = process.env.NODE_ENV === 'production' |
|
? path.join('/tmp', 'uploads') |
|
: path.join(process.cwd(), 'uploads'); |
|
|
|
|
|
try { |
|
if (!fs.existsSync(uploadsDir)) { |
|
fs.mkdirSync(uploadsDir, { recursive: true }); |
|
console.log(`β
Created uploads directory: ${uploadsDir}`); |
|
} else { |
|
console.log(`β
Uploads directory exists: ${uploadsDir}`); |
|
} |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
const storage = multer.diskStorage({ |
|
destination: (req, file, cb) => { |
|
try { |
|
|
|
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}`); |
|
|
|
cb(null, uploadsDir); |
|
} |
|
}, |
|
filename: (req, file, cb) => { |
|
|
|
const timestamp = Date.now(); |
|
const randomString = crypto.randomBytes(8).toString('hex'); |
|
const ext = path.extname(file.originalname); |
|
const baseName = path.basename(file.originalname, ext); |
|
|
|
|
|
const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '_'); |
|
const filename = `${timestamp}_${randomString}_${sanitizedBaseName}${ext}`; |
|
|
|
cb(null, filename); |
|
} |
|
}); |
|
|
|
|
|
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`)); |
|
} |
|
}; |
|
|
|
|
|
export const upload = multer({ |
|
storage, |
|
fileFilter, |
|
limits: { |
|
fileSize: 50 * 1024 * 1024, |
|
files: 10 |
|
} |
|
}); |
|
|
|
|
|
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<string> { |
|
const ext = path.extname(filePath).toLowerCase(); |
|
const mimeTypes: Record<string, string> = { |
|
'.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<string> { |
|
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<boolean> { |
|
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); |
|
} |
|
} |
|
|
|
|
|
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; |