File size: 3,934 Bytes
f4e46e8 a966957 f4e46e8 aec4d7f cfbb685 aec4d7f f4e46e8 aec4d7f cfbb685 f4e46e8 aec4d7f cfbb685 f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f f4e46e8 aec4d7f 0a6555d cfbb685 f4e46e8 aec4d7f f4e46e8 cfbb685 aec4d7f cfbb685 f4e46e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 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();
}
|