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();
}