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