Complete app features and relay cleanup

This commit is contained in:
diyaa
2026-07-26 17:36:46 +02:00
parent 53fa92d222
commit 2bd9c1b3ae
25 changed files with 2204 additions and 188 deletions
+2
View File
@@ -1,3 +1,5 @@
PORT=3000
MAX_FILE_SIZE_MB=25
UPLOADS_DIR=/data/files
FILE_TTL_DAYS=30
ADMIN_TOKEN=8545851a901067a271e8e941297acb4c3d8d68c83eddb2809a8184071e8bc46f
+74
View File
@@ -16,6 +16,9 @@ const PAIRING_DIR = process.env.PAIRING_DIR ?? '/data/pairing';
const PAIRING_TTL_MS = 5 * 60 * 1000;
const QUEUE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const QUEUE_MAX = 500;
const FILE_TTL_DAYS = positiveInteger(process.env.FILE_TTL_DAYS, 30);
const FILE_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN?.trim() ?? '';
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
fs.mkdirSync(QUEUE_DIR, { recursive: true });
@@ -38,6 +41,49 @@ const pairingSessions = new Map(); // code -> session
const connections = new Map(); // peerID -> { ws, name }
const fileRegistry = new Map(); // fileID -> { diskPath, originalName, size }
function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? '', 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function cleanupFilesOlderThan(days = FILE_TTL_DAYS) {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
let deletedFiles = 0;
let deletedBytes = 0;
for (const entry of fs.readdirSync(UPLOADS_DIR, { withFileTypes: true })) {
if (!entry.isFile()) continue;
const diskPath = path.join(UPLOADS_DIR, entry.name);
try {
const stats = fs.statSync(diskPath);
if (stats.mtimeMs >= cutoff) continue;
fs.unlinkSync(diskPath);
fileRegistry.delete(entry.name);
deletedFiles += 1;
deletedBytes += stats.size;
} catch (error) {
console.error(`[cleanup] failed to remove ${entry.name}: ${error.message}`);
}
}
return { days, deletedFiles, deletedBytes };
}
function logCleanupResult(result, source) {
if (result.deletedFiles > 0) {
console.log(
`[cleanup] ${source}: removed ${result.deletedFiles} file(s), ${result.deletedBytes} byte(s), older than ${result.days} day(s)`
);
}
}
logCleanupResult(cleanupFilesOlderThan(), 'startup');
setInterval(() => {
logCleanupResult(cleanupFilesOlderThan(), 'scheduled');
}, FILE_CLEANUP_INTERVAL_MS);
// ─── Persistent queue (disk-backed) ──────────────────────────────────────────
//
// Layout on disk:
@@ -219,6 +265,19 @@ function requireToken(req, res, next) {
next();
}
function requireAdmin(req, res, next) {
if (!ADMIN_TOKEN) {
return res.status(503).json({ error: 'Admin cleanup is not configured' });
}
const auth = req.headers.authorization ?? '';
if (auth !== `Bearer ${ADMIN_TOKEN}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// ─── Pairing routes ───────────────────────────────────────────────────────────
app.post('/pairing/create', (req, res) => {
@@ -298,6 +357,21 @@ app.get('/files/:id', requireToken, (req, res) => {
res.sendFile(meta.diskPath);
});
// ─── Admin routes ───────────────────────────────────────────────────────────
app.post('/admin/cleanup', requireAdmin, (req, res) => {
const requestedDays = req.body?.days;
const days = requestedDays === undefined ? FILE_TTL_DAYS : Number(requestedDays);
if (!Number.isInteger(days) || days < 1 || days > 3650) {
return res.status(400).json({ error: 'days must be an integer between 1 and 3650' });
}
const result = cleanupFilesOlderThan(days);
logCleanupResult(result, 'manual');
res.json(result);
});
// ─── Health check ─────────────────────────────────────────────────────────────
app.get('/health', (_req, res) => res.json({