126 lines
4 KiB
JavaScript
126 lines
4 KiB
JavaScript
#!/usr/bin/env bun
|
|
// s3-cleaner.js — Delete old versioned cache folders from S3/R2
|
|
// Usage: bun s3-cleaner.js <version>
|
|
// 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 = '8fab47503efb9547b50e4fb317e35cc7'
|
|
const S3_SECRET_ACCESS_KEY = '47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950'
|
|
const S3_REGION = 'apac' // 'us-east-1' for AWS, 'auto' for R2
|
|
const S3_ENDPOINT = 'https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com' // S3 or R2 endpoint
|
|
const S3_BUCKET = 'meatfarmer'
|
|
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 <version>')
|
|
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 concurrency = 20
|
|
|
|
for (let i = 0; i < keys.length; i += concurrency) {
|
|
const batch = keys.slice(i, i + concurrency)
|
|
await Promise.all(
|
|
batch.map(async (key) => {
|
|
await s3.delete(key)
|
|
deleted++
|
|
})
|
|
)
|
|
|
|
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)
|
|
}
|