freshyo/scripts/s3-cleaner.js
2026-08-03 20:39:40 +05:30

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