Complete app features and relay cleanup
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
PORT=3000
|
||||
MAX_FILE_SIZE_MB=25
|
||||
UPLOADS_DIR=/data/files
|
||||
FILE_TTL_DAYS=30
|
||||
ADMIN_TOKEN=8545851a901067a271e8e941297acb4c3d8d68c83eddb2809a8184071e8bc46f
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user