// FILE: package.json { "name": "filelinker", "version": "1.0.0", "description": "Simple local app: sube archivos y genera links para ver/compartir", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", "fs-extra": "^11.1.1", "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", "uuid": "^9.0.0" } } // FILE: server.js const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs-extra'); const { v4: uuidv4 } = require('uuid'); const mime = require('mime-types'); const cors = require('cors'); const UPLOADS_DIR = path.join(__dirname, 'uploads'); const DB_FILE = path.join(__dirname, 'db.json'); fs.ensureDirSync(UPLOADS_DIR); if (!fs.existsSync(DB_FILE)) fs.writeJsonSync(DB_FILE, {}); const db = () => fs.readJsonSync(DB_FILE); const saveDb = (obj) => fs.writeJsonSync(DB_FILE, obj, { spaces: 2 }); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOADS_DIR), filename: (req, file, cb) => { // keep original filename but prefix with timestamp for safety const name = `${Date.now()}-${file.originalname}`.replace(/[^a-zA-Z0-9.\-_]/g, '_'); cb(null, name); } }); const upload = multer({ storage }); const app = express(); app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Upload endpoint (accept multiple files) app.post('/upload', upload.array('files'), (req, res) => { try { const files = req.files || []; const mapping = db(); const baseUrl = (req.protocol + '://' + req.get('host')); const links = files.map(f => { const id = uuidv4(); mapping[id] = { storedName: f.filename, originalName: f.originalname, mime: f.mimetype, size: f.size, uploadedAt: new Date().toISOString() }; return { id, url: `${baseUrl}/f/${id}`, name: f.originalname }; }); saveDb(mapping); return res.json({ success: true, files: links }); } catch (err) { console.error(err); return res.status(500).json({ success: false, error: 'Error interno' }); } }); // Serve file by id (preview when possible) app.get('/f/:id', (req, res) => { const id = req.params.id; const mapping = db(); if (!mapping[id]) return res.status(404).send('No encontrado'); const meta = mapping[id]; const filePath = path.join(UPLOADS_DIR, meta.storedName); if (!fs.existsSync(filePath)) return res.status(410).send('Archivo no disponible'); const mimeType = meta.mime || mime.lookup(meta.originalName) || 'application/octet-stream'; // For common previewable types, serve inline. For others, force download. const previewable = [ 'image/', 'video/', 'audio/', 'text/', 'application/pdf', 'application/json', 'application/xml' ]; const inline = previewable.some(p => mimeType.startsWith(p) || mimeType === p); res.setHeader('Content-Type', mimeType); if (inline) res.setHeader('Content-Disposition', `inline; filename="${meta.originalName.replace(/\"/g,'') }"`); else res.setHeader('Content-Disposition', `attachment; filename="${meta.originalName.replace(/\"/g,'') }"`); const stream = fs.createReadStream(filePath); stream.pipe(res); }); // API: list all files (simple) app.get('/api/list', (req, res) => { const mapping = db(); const baseUrl = (req.protocol + '://' + req.get('host')); const list = Object.entries(mapping).map(([id, meta]) => ({ id, url: `${baseUrl}/f/${id}`, ...meta })); res.json(list); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`FileLinker app listening on http://localhost:${PORT}`)); // FILE: public/index.html