/** * Web Uploader - Backend Server * Express.js server for handling file uploads with multer. */ import express from 'express'; import multer from 'multer'; import cors from 'cors'; import path from 'path'; import os from 'os'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; import { existsSync, mkdirSync } from 'fs'; import dotenv from 'dotenv'; import rateLimit from 'express-rate-limit'; import { v4 as uuidv4 } from 'uuid'; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const CONFIG = { port: process.env.PORT || 7860, uploadDir: process.env.UPLOAD_DIR || path.join(os.tmpdir(), 'web-uploader'), maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 500 * 1024 * 1024, rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX) || 100, cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL_MS) || 60 * 60 * 1000 // 1 hour }; const METADATA_FILE = path.join(CONFIG.uploadDir, 'metadata.json'); const app = express(); app.set('trust proxy', true); app.use(cors()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/uploads', express.static(CONFIG.uploadDir)); // Rate Limiting const limiter = rateLimit({ windowMs: CONFIG.rateLimitWindow, max: CONFIG.rateLimitMax, standardHeaders: true, legacyHeaders: false, validate: { trustProxy: false } }); const storage = multer.diskStorage({ destination: (req, file, cb) => { if (!existsSync(CONFIG.uploadDir)) { mkdirSync(CONFIG.uploadDir, { recursive: true }); } cb(null, CONFIG.uploadDir); }, filename: (req, file, cb) => { const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`; cb(null, `${uniqueSuffix}-${file.originalname}`); } }); const upload = multer({ storage, limits: { fileSize: CONFIG.maxFileSize } }); // Metadata Manager class MetadataManager { static async ensureMetadataFile() { if (!existsSync(CONFIG.uploadDir)) { mkdirSync(CONFIG.uploadDir, { recursive: true }); } if (!existsSync(METADATA_FILE)) { await fs.writeFile(METADATA_FILE, JSON.stringify({}, null, 2)); } } static async load() { await this.ensureMetadataFile(); try { const data = await fs.readFile(METADATA_FILE, 'utf8'); return JSON.parse(data); } catch (error) { console.error('Error reading metadata:', error); return {}; } } static async save(data) { await fs.writeFile(METADATA_FILE, JSON.stringify(data, null, 2)); } static async addFile(filename, expiration) { const metadata = await this.load(); const expiresAt = expiration === 'permanent' ? null : Date.now() + this.parseExpiration(expiration); metadata[filename] = { filename, uploadedAt: Date.now(), expiresAt }; await this.save(metadata); return metadata[filename]; } static parseExpiration(exp) { const units = { 'h': 60 * 60 * 1000, 'd': 24 * 60 * 60 * 1000, 'w': 7 * 24 * 60 * 60 * 1000 }; const match = exp.match(/^(\d+)([hdw])$/); if (!match) return 24 * 60 * 60 * 1000; // Default 24h return parseInt(match[1]) * units[match[2]]; } static async cleanup() { const metadata = await this.load(); const now = Date.now(); let changed = false; for (const [filename, info] of Object.entries(metadata)) { if (info.expiresAt && info.expiresAt < now) { try { const filePath = path.join(CONFIG.uploadDir, filename); if (existsSync(filePath)) { await fs.unlink(filePath); console.log(`Expired file deleted: ${filename}`); } delete metadata[filename]; changed = true; } catch (err) { console.error(`Failed to delete expired file ${filename}:`, err); } } } if (changed) { await this.save(metadata); } } } // Start cleanup task setInterval(() => { MetadataManager.cleanup(); }, CONFIG.cleanupInterval); // Initial cleanup on startup MetadataManager.cleanup(); function isValidFilePath(filePath) { const safeUploadDir = path.normalize(CONFIG.uploadDir); const resolvedPath = path.normalize(filePath); return resolvedPath.startsWith(safeUploadDir + path.sep); } function buildFileUrl(req, filename) { return `${req.protocol}://${req.get('host')}/uploads/${filename}`; } app.post('/api/upload', limiter, upload.array('files'), async (req, res) => { if (!req.files || req.files.length === 0) { return res.status(400).json({ status: 'error', message: 'No files uploaded' }); } const expiration = req.body.expiration || '24h'; const uploadedFiles = await Promise.all(req.files.map(async file => { const metadata = await MetadataManager.addFile(file.filename, expiration); return { originalName: file.originalname, filename: file.filename, url: buildFileUrl(req, file.filename), expiresAt: metadata.expiresAt }; })); res.status(200).json({ status: 'success', message: 'Files uploaded successfully', files: uploadedFiles }); }); app.delete('/api/delete/:filename', async (req, res) => { const { filename } = req.params; const filePath = path.join(CONFIG.uploadDir, filename); if (!isValidFilePath(filePath)) { return res.status(403).json({ status: 'error', message: 'Access denied: Invalid file path' }); } try { // Remove from metadata const metadata = await MetadataManager.load(); if (metadata[filename]) { delete metadata[filename]; await MetadataManager.save(metadata); } await fs.access(filePath); } catch { return res.status(404).json({ status: 'error', message: 'File not found' }); } try { await fs.unlink(filePath); res.status(200).json({ status: 'success', message: 'File deleted successfully' }); } catch (err) { console.error('Delete error:', err); res.status(500).json({ status: 'error', message: 'Failed to delete file' }); } }); app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ status: 'error', message: `File too large. Maximum size is ${CONFIG.maxFileSize / (1024 * 1024)} MB` }); } return res.status(400).json({ status: 'error', message: err.message }); } next(err); }); app.listen(CONFIG.port, () => { console.log(`Server running on http://localhost:${CONFIG.port}`); });