Created
August 14, 2025 11:37
-
-
Save regenrek/e1dafab39fbca6ddde7e500df9f47d59 to your computer and use it in GitHub Desktop.
R2 Empty all data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| S3Client, | |
| ListObjectsV2Command, | |
| DeleteObjectsCommand, | |
| ListObjectVersionsCommand, | |
| } from '@aws-sdk/client-s3'; | |
| import { config as loadEnv } from 'dotenv'; | |
| loadEnv(); | |
| type EmptyParams = { | |
| bucket: string; | |
| prefix?: string; | |
| dryRun?: boolean; | |
| }; | |
| const getArg = (name: string): string | undefined => { | |
| const argv = process.argv.slice(2); | |
| const index = argv.indexOf(`--${name}`); | |
| if (index !== -1 && index + 1 < argv.length) return argv[index + 1]; | |
| return undefined; | |
| }; | |
| const hasFlag = (name: string): boolean => process.argv.slice(2).includes(`--${name}`); | |
| const requiredEnv = (key: string): string => { | |
| const value = process.env[key]; | |
| if (!value) throw new Error(`Missing required env var: ${key}`); | |
| return value; | |
| }; | |
| const STAGE = getArg('stage') || process.env.STAGE || 'prod2'; | |
| const BUCKET = getArg('bucket') || process.env.R2_BUCKET || `codefetch-scraped-${STAGE}`; | |
| const PREFIX = getArg('prefix') || process.env.R2_PREFIX || undefined; | |
| const DRY_RUN = hasFlag('dry-run') || process.env.DRY_RUN === '1'; | |
| const R2_ACCESS_KEY_ID = requiredEnv('CLOUDFLARE_R2_ACCESS_KEY_ID'); | |
| const R2_SECRET_ACCESS_KEY = requiredEnv('CLOUDFLARE_R2_SECRET_ACCESS_KEY'); | |
| const R2_ENDPOINT = requiredEnv('CLOUDFLARE_R2_URL'); | |
| const s3 = new S3Client({ | |
| region: 'auto', | |
| endpoint: R2_ENDPOINT, | |
| forcePathStyle: true, | |
| credentials: { | |
| accessKeyId: R2_ACCESS_KEY_ID, | |
| secretAccessKey: R2_SECRET_ACCESS_KEY, | |
| }, | |
| }); | |
| async function deleteAllObjectVersions({ bucket, prefix, dryRun }: EmptyParams): Promise<number> { | |
| let totalDeleted = 0; | |
| let keyMarker: string | undefined; | |
| let versionIdMarker: string | undefined; | |
| for (;;) { | |
| const resp = await s3.send( | |
| new ListObjectVersionsCommand({ | |
| Bucket: bucket, | |
| Prefix: prefix, | |
| KeyMarker: keyMarker, | |
| VersionIdMarker: versionIdMarker, | |
| MaxKeys: 1000, | |
| }) | |
| ); | |
| const versions = resp.Versions ?? []; | |
| const deleteMarkers = resp.DeleteMarkers ?? []; | |
| const objects = [ | |
| ...versions.map((v) => ({ Key: v.Key!, VersionId: v.VersionId })), | |
| ...deleteMarkers.map((m) => ({ Key: m.Key!, VersionId: m.VersionId })), | |
| ]; | |
| if (objects.length === 0) { | |
| if (!resp.IsTruncated) break; | |
| } else { | |
| if (dryRun) { | |
| console.log(`[DRY RUN] Would delete ${objects.length} versioned objects`); | |
| } else { | |
| const delResp = await s3.send( | |
| new DeleteObjectsCommand({ | |
| Bucket: bucket, | |
| Delete: { Objects: objects, Quiet: true }, | |
| }) | |
| ); | |
| const deletedCount = (delResp.Deleted?.length ?? 0) + (delResp.Errors?.length ? 0 : 0); | |
| totalDeleted += deletedCount || objects.length; // fallback | |
| console.log(`Deleted ${objects.length} versioned entries (versions + delete-markers)`); | |
| } | |
| } | |
| if (!resp.IsTruncated) break; | |
| keyMarker = resp.NextKeyMarker; | |
| versionIdMarker = resp.NextVersionIdMarker; | |
| } | |
| return totalDeleted; | |
| } | |
| async function deleteAllCurrentObjects({ bucket, prefix, dryRun }: EmptyParams): Promise<number> { | |
| let totalDeleted = 0; | |
| let continuationToken: string | undefined; | |
| for (;;) { | |
| const resp = await s3.send( | |
| new ListObjectsV2Command({ | |
| Bucket: bucket, | |
| Prefix: prefix, | |
| ContinuationToken: continuationToken, | |
| MaxKeys: 1000, | |
| }) | |
| ); | |
| const keys = (resp.Contents ?? []).map((c) => c.Key!).filter(Boolean); | |
| if (keys.length === 0) { | |
| if (!resp.IsTruncated) break; | |
| } else { | |
| const objects = keys.map((Key) => ({ Key })); | |
| if (dryRun) { | |
| console.log(`[DRY RUN] Would delete ${objects.length} objects`); | |
| } else { | |
| const delResp = await s3.send( | |
| new DeleteObjectsCommand({ | |
| Bucket: bucket, | |
| Delete: { Objects: objects, Quiet: true }, | |
| }) | |
| ); | |
| totalDeleted += delResp.Deleted?.length ?? objects.length; | |
| console.log(`Deleted ${objects.length} objects`); | |
| } | |
| } | |
| if (!resp.IsTruncated) break; | |
| continuationToken = resp.NextContinuationToken; | |
| } | |
| return totalDeleted; | |
| } | |
| async function main() { | |
| console.log(` | |
| Emptying R2 bucket: ${BUCKET} | |
| Stage: ${STAGE} | |
| Prefix: ${PREFIX ?? '(none)'} | |
| Endpoint: ${R2_ENDPOINT} | |
| Dry run: ${DRY_RUN ? 'yes' : 'no'} | |
| `); | |
| try { | |
| // Delete versions first (if versioning is enabled). Some R2 accounts/endpoints | |
| // do not implement ListObjectVersions and will return 501 NotImplemented. | |
| try { | |
| const versionsDeleted = await deleteAllObjectVersions({ | |
| bucket: BUCKET, | |
| prefix: PREFIX, | |
| dryRun: DRY_RUN, | |
| }); | |
| if (versionsDeleted > 0) { | |
| console.log(`Deleted ${versionsDeleted} versioned entries`); | |
| } | |
| } catch (e: any) { | |
| const status = e?.$metadata?.httpStatusCode; | |
| const code = e?.Code || e?.code; | |
| if (status === 501 || code === 'NotImplemented') { | |
| console.log( | |
| 'ListObjectVersions not implemented on this endpoint. Skipping version cleanup.' | |
| ); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| // Then delete current objects | |
| const deleted = await deleteAllCurrentObjects({ | |
| bucket: BUCKET, | |
| prefix: PREFIX, | |
| dryRun: DRY_RUN, | |
| }); | |
| console.log( | |
| `\n✅ Done. ${DRY_RUN ? 'Planned to delete' : 'Deleted'} ${deleted}${DRY_RUN ? ' (current objects only)' : ''}.` | |
| ); | |
| } catch (err) { | |
| console.error('❌ Error emptying bucket:', err); | |
| process.exit(1); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment