// 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 FileLinker — sube y comparte

FileLinker

Arrastra archivos o haz click para seleccionar. Se crearán enlaces para ver/descargar.
Nota: esta app guarda archivos localmente en el servidor que ejecutes. No la pongas en un servidor público sin medidas de seguridad.
// FILE: public/app.js (() => { const drop = document.getElementById('drop'); const input = document.getElementById('fileinput'); const uploadBtn = document.getElementById('uploadBtn'); const linksDiv = document.getElementById('links'); let files = []; drop.addEventListener('click', () => input.click()); drop.addEventListener('dragover', (e) => { e.preventDefault(); drop.style.opacity = 0.8; }); drop.addEventListener('dragleave', () => { drop.style.opacity = 1; }); drop.addEventListener('drop', (e) => { e.preventDefault(); drop.style.opacity = 1; const dt = e.dataTransfer; if (dt && dt.files) { files = Array.from(dt.files); drop.querySelector('.small').innerText = `${files.length} archivo(s) listo(s)`; } }); input.addEventListener('change', (e) => { files = Array.from(e.target.files); drop.querySelector('.small').innerText = `${files.length} archivo(s) seleccionado(s)`; }); uploadBtn.addEventListener('click', async () => { if (!files.length) return alert('Selecciona archivos primero'); const form = new FormData(); files.forEach(f => form.append('files', f)); uploadBtn.disabled = true; uploadBtn.innerText = 'Subiendo...'; try { const res = await fetch('/upload', { method: 'POST', body: form }); const data = await res.json(); uploadBtn.disabled = false; uploadBtn.innerText = 'Subir y generar enlaces'; if (!data.success) throw new Error(data.error || 'Error'); linksDiv.innerHTML = ''; data.files.forEach(f => { const el = document.createElement('div'); el.innerHTML = `${escapeHtml(f.name)}
${f.url}`; linksDiv.appendChild(el); }); } catch (err) { alert('Error al subir: ' + (err.message || err)); uploadBtn.disabled = false; uploadBtn.innerText = 'Subir y generar enlaces'; } }); function escapeHtml(s){ return s.replace(/&/g,'&').replace(//g,'>'); } })(); // FILE: README.md # FileLinker App local simple para subir archivos y generar enlaces para ver o descargar. ## Uso 1. Instala dependencias: `npm install` 2. Ejecuta: `npm start` (por defecto escucha en http://localhost:3000) 3. Abre el navegador y usa la interfaz para subir archivos. ## Nota de seguridad - Esta versión guarda archivos en el disco local y no tiene autenticación. - No expongas este servidor a Internet sin añadir autenticación, límites de tamaño, escaneo antivirus y limpieza automática de archivos. // FILE: .gitignore node_modules/ uploads/ db.json