KnowledgeBridge / server /file-upload.ts
fazeel007's picture
Fix file upload
353a9b9
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<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);
}
}
// 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;