diff options
| author | 2025-12-26 22:39:23 +0800 | |
|---|---|---|
| committer | 2025-12-26 22:39:23 +0800 | |
| commit | 32ca410f4edbff578d71781d943c41573912f476 (patch) | |
| tree | 49f7e1e5602657d23945082fe273fc4802959a40 /scripts/cache/s3.ts | |
Initial commitmain
Diffstat (limited to 'scripts/cache/s3.ts')
| -rw-r--r-- | scripts/cache/s3.ts | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/scripts/cache/s3.ts b/scripts/cache/s3.ts new file mode 100644 index 0000000..ffbb6ac --- /dev/null +++ b/scripts/cache/s3.ts @@ -0,0 +1,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'); + } +} |
