summaryrefslogtreecommitdiffstats
path: root/scripts/cache/s3.ts
blob: ffbb6ac3a3ac643ba64413e6a4830b26782e5854 (plain) (blame)
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
110
111
112
113
114
115
116
117
import { S3Client } from 'bun';
import { MAX_CACHE_AGE_DAYS, MAX_CACHE_FILES } from './utils';
import { findCacheByKey, findCacheByPrefix, loadManifest, removeCacheEntry } from './manifest';
import { deleteMetadata } from './metadata';

const s3Config = {
    ENDPOINT: process.env.S3_ENDPOINT,
    ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
    SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
    BUCKET_NAME: process.env.S3_BUCKET_NAME,
    REGION: process.env.S3_REGION
};

export const ENABLED =
    !!s3Config.ENDPOINT &&
    !!s3Config.ACCESS_KEY_ID &&
    !!s3Config.SECRET_ACCESS_KEY &&
    !!s3Config.BUCKET_NAME;

let client: S3Client | null = null;

export function getClient(): S3Client | null {
    if (!ENABLED || client) return client;

    client = new S3Client({
        endpoint: s3Config.ENDPOINT,
        accessKeyId: s3Config.ACCESS_KEY_ID,
        secretAccessKey: s3Config.SECRET_ACCESS_KEY,
        bucket: s3Config.BUCKET_NAME,
        region: s3Config.REGION
    });
    return client;
}

const cacheExists = async (s3: S3Client, key: string) =>
    await s3
        .file(key)
        .exists()
        .catch(() => false);

const cleanupStaleCache = async (s3: S3Client, key: string): Promise<void> => {
    console.log(`Cleaning stale cache from manifest (cache missing): ${key}`);
    await deleteMetadata(s3, key);
    await removeCacheEntry(s3, key);
};

export async function resolveCacheKey(
    s3: S3Client,
    key: string,
    restoreKeys?: string[]
): Promise<string | null> {
    const manifest = await loadManifest(s3);

    // Try exact match first
    const exactMatch = findCacheByKey(manifest, key);
    if (exactMatch) {
        if (await cacheExists(s3, exactMatch.key)) {
            return exactMatch.key;
        }
        await cleanupStaleCache(s3, exactMatch.key);
    }

    // Try restore keys in order (prefix matching), preferring most recent
    if (restoreKeys && restoreKeys.length > 0) {
        for (const prefix of restoreKeys) {
            const match = findCacheByPrefix(manifest, prefix);
            if (match) {
                if (await cacheExists(s3, match.key)) {
                    return match.key;
                }
                await cleanupStaleCache(s3, match.key);
            }
        }
    }

    return null;
}

export async function cleanupOldCaches(s3: S3Client, prefix: string): Promise<void> {
    const manifest = await loadManifest(s3);

    // Filter caches by prefix
    const filesWithMetadata = manifest.caches
        .filter((entry) => entry.key.startsWith(prefix))
        .map((entry) => ({
            key: entry.key,
            lastAccessed: entry.lastAccessed,
            timestamp: entry.timestamp
        }));

    // Sort by lastAccessed (most recently accessed first)
    const files = filesWithMetadata.sort((a, b) => b.lastAccessed - a.lastAccessed);

    const now = Date.now();
    const maxAge = MAX_CACHE_AGE_DAYS * 24 * 60 * 60 * 1000;
    let manifestUpdated = false;

    for (let i = 0; i < files.length; i++) {
        const entry = files[i];
        const age = now - entry.lastAccessed;
        const shouldDelete = i >= MAX_CACHE_FILES || age > maxAge;

        if (shouldDelete) {
            console.log(
                `Deleting cache: ${entry.key} (age: ${Math.floor(age / (24 * 60 * 60 * 1000))} days, position: ${i + 1})`
            );
            await s3.file(entry.key).delete();
            await deleteMetadata(s3, entry.key);
            await removeCacheEntry(s3, entry.key);
            manifestUpdated = true;
        }
    }

    if (manifestUpdated) {
        console.log('Manifest updated after cleanup');
    }
}