edtech / apps /api /src /services /cleanup.ts
CognxSafeTrack
refactor(debt): resolve all 10 technical debt items from audit
a966957
import { logger } from '../logger';
import fs from 'fs/promises';
import path from 'path';
import cron from 'node-cron';
import { redis } from '../lib/redis';
const TEMP_DIR = '/tmp';
const GLOBAL_MAX_AGE = 30 * 60 * 1000; // 30 minutes
const PER_TENANT_QUOTA_MB = 100; // 100MB per tenant limit
const CLEANUP_LOCK_KEY = 'lock:cleanup:temp_files';
const LOCK_TTL = 1800; // 30 minutes in seconds
/**
* Cleanup Service
*
* Manages /tmp storage by enforcing age limits and per-tenant quotas.
* Uses a Redis lock to ensure only one instance runs maintenance at a time.
*/
export async function cleanTempFiles() {
// 0. Acquire distributed lock
const lock = await redis.set(CLEANUP_LOCK_KEY, 'locked', 'EX', LOCK_TTL, 'NX');
if (!lock) {
logger.info('[CLEANUP] Maintenance already in progress on another instance. Skipping.');
return;
}
const now = Date.now();
try {
const folders = ['', 'audio', 'images', 'documents'];
const tenantUsage: Record<string, number> = {};
const tenantFiles: Record<string, { path: string, size: number, mtime: number }[]> = {};
for (const folder of folders) {
const dirPath = path.join(TEMP_DIR, folder);
try {
await fs.access(dirPath);
} catch {
continue;
}
const files = await fs.readdir(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) continue;
// 1. Check Age (Global cleanup)
const age = now - stats.mtimeMs;
if (age > GLOBAL_MAX_AGE) {
await fs.unlink(filePath);
logger.info(`[CLEANUP] Deleted old file (Age): ${filePath}`);
continue;
}
// 2. Track usage by tenant (Assumes files are named orgId_filename)
const orgId = file.split('_')[0];
if (orgId && orgId.length > 10) { // Basic orgId check
tenantUsage[orgId] = (tenantUsage[orgId] || 0) + stats.size;
if (!tenantFiles[orgId]) tenantFiles[orgId] = [];
tenantFiles[orgId].push({ path: filePath, size: stats.size, mtime: stats.mtimeMs });
}
}
}
// 3. Enforce Quotas
for (const orgId in tenantUsage) {
const usageMB = tenantUsage[orgId] / (1024 * 1024);
if (usageMB > PER_TENANT_QUOTA_MB) {
logger.warn(`[CLEANUP] Quota exceeded for Org ${orgId}: ${usageMB.toFixed(2)}MB. Cleaning oldest files...`);
// Sort by mtime (oldest first)
const files = tenantFiles[orgId].sort((a, b) => a.mtime - b.mtime);
let freed = 0;
const targetToFree = (usageMB - (PER_TENANT_QUOTA_MB * 0.8)) * 1024 * 1024; // Free up to 80% of quota
for (const file of files) {
await fs.unlink(file.path);
freed += file.size;
if (freed >= targetToFree) break;
}
logger.info(`[CLEANUP] Freed ${(freed / (1024 * 1024)).toFixed(2)}MB for Org ${orgId}`);
}
}
} catch (err: any) {
logger.error({ err }, '[CLEANUP] Error during maintenance');
} finally {
// 4. Release lock
await redis.del(CLEANUP_LOCK_KEY);
}
}
/**
* Starts a cron job that runs every 30 minutes.
*/
export function startCleanupCron() {
cron.schedule('*/30 * * * *', async () => {
logger.info('[CLEANUP] 🧹 Starting scheduled maintenance...');
await cleanTempFiles();
});
// Also run once at startup
cleanTempFiles();
}