const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const bcrypt = require('bcryptjs'); const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); const express = require('express'); const jwt = require('jsonwebtoken'); const multer = require('multer'); const sqlite3 = require('sqlite3').verbose(); dotenv.config({ path: path.join(__dirname, '..', '..', '.env') }); const basePath = (process.env.BASE_PATH || '/manage').replace(/\/+$/, '') || '/manage'; const port = parseInt(process.env.PORT || '3000', 10); const dataDir = process.env.DATA_DIR || path.join(__dirname, '..', 'data'); const dbPath = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'uploads.sqlite'); const loginFile = process.env.LOGIN_FILE || path.join(__dirname, '..', '..', '.logins'); const uploadTtlSeconds = parseInt(process.env.UPLOAD_TTL_SECONDS || '604800', 10); const maxUploadBytes = parseInt(process.env.UPLOAD_MAX_BYTES || '0', 10); const shareDir = path.join(dataDir, '_share'); const jwtSecret = crypto.randomBytes(32).toString('hex'); const jwtMaxAgeMs = 2 * 60 * 60 * 1000; fs.mkdirSync(shareDir, { recursive: true }); const tempDir = path.join(os.tmpdir(), 'uploads'); fs.mkdirSync(tempDir, { recursive: true }); const upload = multer({ dest: tempDir, limits: maxUploadBytes > 0 ? { fileSize: maxUploadBytes } : undefined, }); const app = express(); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); const db = new sqlite3.Database(dbPath); db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS uploads ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner TEXT NOT NULL, original_name TEXT NOT NULL, stored_name TEXT NOT NULL, stored_path TEXT NOT NULL, size_bytes INTEGER NOT NULL, uploaded_at INTEGER NOT NULL, expires_at INTEGER NOT NULL )`); db.run('CREATE INDEX IF NOT EXISTS uploads_owner_idx ON uploads(owner)'); db.run('CREATE INDEX IF NOT EXISTS uploads_expires_idx ON uploads(expires_at)'); }); function run(sql, params = []) { return new Promise((resolve, reject) => { db.run(sql, params, function (err) { if (err) { reject(err); return; } resolve(this); }); }); } function get(sql, params = []) { return new Promise((resolve, reject) => { db.get(sql, params, (err, row) => { if (err) { reject(err); return; } resolve(row); }); }); } function all(sql, params = []) { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) { reject(err); return; } resolve(rows); }); }); } function parseLogins(contents) { const entries = new Map(); const lines = contents.split(/\r?\n/); for (const line of lines) { if (!line || line.startsWith('#')) { continue; } const parts = line.split(';;'); if (parts.length !== 2) { continue; } const username = parts[0].trim(); const hash = parts[1].trim(); if (!username || !hash) { continue; } entries.set(username, hash); } return entries; } async function loadLogins() { try { const contents = await fs.promises.readFile(loginFile, 'utf8'); return parseLogins(contents); } catch (err) { return new Map(); } } function toBase32(buffer) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; let output = ''; for (const byte of buffer) { value = (value << 8) | byte; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; } function createToken(timestampMs) { const tsBuffer = Buffer.alloc(8); tsBuffer.writeBigUInt64BE(BigInt(timestampMs)); const randomPart = crypto.randomBytes(12); return toBase32(Buffer.concat([tsBuffer, randomPart])); } function sanitizeExtension(originalName) { const ext = path.extname(originalName || '').toLowerCase(); if (!ext) { return ''; } if (!/^\.[a-z0-9]{1,10}$/.test(ext)) { return ''; } return ext; } function formatBytes(bytes) { if (bytes < 1024) { return `${bytes} B`; } const units = ['KB', 'MB', 'GB', 'TB']; let value = bytes / 1024; let idx = 0; while (value >= 1024 && idx < units.length - 1) { value /= 1024; idx += 1; } return `${value.toFixed(value < 10 ? 1 : 0)} ${units[idx]}`; } function formatTimestamp(ts) { const date = new Date(ts); return date.toLocaleString(); } function formatCountdown(ts) { const delta = Math.max(0, ts - Date.now()); const minutes = Math.floor(delta / 60000); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } if (hours > 0) { return `${hours}h ${minutes % 60}m`; } return `${minutes}m`; } function renderPage(title, body) { return `
| File | Size | Expires | Actions |
|---|
Upload not found.
')); return; } try { await fs.promises.unlink(uploadEntry.stored_path); } catch (err) { // Ignore missing files. } await run('DELETE FROM uploads WHERE id = ?', [uploadEntry.id]); res.redirect(baseUrl('/dashboard')); }); app.post(`${basePath}/files/:id/extend`, requireAuthPage, async (req, res) => { const uploadEntry = await get('SELECT id, expires_at FROM uploads WHERE id = ? AND owner = ?', [ req.params.id, req.user.username, ]); if (!uploadEntry) { res.status(404).send(renderPage('Not found', 'Upload not found.
')); return; } const override = parseInt(req.body.extendSeconds || '', 10); const extensionSeconds = Number.isFinite(override) && override > 0 ? override : uploadTtlSeconds; const base = Math.max(uploadEntry.expires_at, Date.now()); const nextExpiry = base + extensionSeconds * 1000; await run('UPDATE uploads SET expires_at = ? WHERE id = ?', [nextExpiry, uploadEntry.id]); res.redirect(baseUrl('/dashboard')); }); app.use((req, res) => { res.status(404).send(renderPage('Not found', 'Not found.
')); }); app.listen(port, () => { console.log(`Express server listening on ${port} with base path ${basePath}`); });