#!/usr/bin/env bun // s3-cleaner.js โ€” Delete old versioned cache folders from S3/R2 // Usage: bun s3-cleaner.js // Example: bun s3-cleaner.js 235 // Deletes all objects under api-cache/v-0/ through api-cache/v-234/ // ============================================================ // CREDENTIALS โ€” replace with your actual values // ============================================================ const S3_ACCESS_KEY_ID = 'YOUR_ACCESS_KEY_ID' const S3_SECRET_ACCESS_KEY = 'YOUR_SECRET_ACCESS_KEY' const S3_REGION = 'auto' // 'us-east-1' for AWS, 'auto' for R2 const S3_ENDPOINT = 'https://your-account.r2.cloudflarestorage.com' // S3 or R2 endpoint const S3_BUCKET = 'your-bucket-name' const API_CACHE_KEY = 'api-cache' // matches API_CACHE_KEY env var in backend // ============================================================ const threshold = parseInt(process.argv[2]) if (!threshold || isNaN(threshold) || threshold <= 0) { console.error('Usage: bun s3-cleaner.js ') console.error('Example: bun s3-cleaner.js 235') console.error(' Deletes all v-{n} folders where n < 235') process.exit(1) } const cachePrefix = API_CACHE_KEY.endsWith('/') ? API_CACHE_KEY : `${API_CACHE_KEY}/` const versionPattern = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}v-(\\d+)/`) console.log(`๐Ÿงน S3 Cache Cleaner โ€” keeping v-${threshold}+, deleting v-0 through v-${threshold - 1}`) console.log(` Bucket: ${S3_BUCKET}`) console.log(` Prefix: ${cachePrefix}`) const s3 = new Bun.S3Client({ accessKeyId: S3_ACCESS_KEY_ID, secretAccessKey: S3_SECRET_ACCESS_KEY, region: S3_REGION, endpoint: S3_ENDPOINT, bucket: S3_BUCKET, }) async function listAllObjects(prefix) { const allKeys = [] let continuationToken do { const options = { prefix, maxKeys: 1000 } if (continuationToken) options.continuationToken = continuationToken const result = await s3.list(options) for (const obj of result.contents) { allKeys.push(obj.key) } continuationToken = result.nextContinuationToken process.stdout.write(`\r Listed ${allKeys.length} objects...`) } while (continuationToken) console.log('') return allKeys } async function deleteObjects(keys) { let deleted = 0 const batchSize = 1000 for (let i = 0; i < keys.length; i += batchSize) { const batch = keys.slice(i, i + batchSize) const objects = batch.map((key) => ({ key })) const result = await s3.deleteObjects({ objects }) deleted += result.deleted?.length ?? 0 process.stdout.write(`\r Deleted ${deleted}/${keys.length}...`) } console.log('') return deleted } // ============================================================ // MAIN // ============================================================ try { console.log('\n๐Ÿ“‹ Listing objects...') const allObjects = await listAllObjects(cachePrefix) const toDelete = [] const versionsSeen = new Set() for (const key of allObjects) { const match = key.match(versionPattern) if (match) { const ver = parseInt(match[1]) if (ver < threshold) { toDelete.push(key) } versionsSeen.add(ver) } } const sortedVersions = [...versionsSeen].sort((a, b) => a - b) if (sortedVersions.length === 0) { console.log('\nโœ… No cache objects found.') process.exit(0) } console.log(`\n๐Ÿ“Š Found versions: v-${sortedVersions[0]} through v-${sortedVersions[sortedVersions.length - 1]}`) const oldVersionCount = sortedVersions.filter((v) => v < threshold).length console.log(` To delete: ${toDelete.length} objects across ${oldVersionCount} version(s)`) console.log(` To keep: v-${threshold}+`) if (toDelete.length === 0) { console.log('\nโœ… Nothing to delete.') process.exit(0) } console.log('\nโ— Proceeding with deletion...') const count = await deleteObjects(toDelete) console.log(`\nโœ… Done โ€” deleted ${count} objects.`) } catch (error) { console.error(`\nโŒ Error: ${error.message}`) process.exit(1) }