Skip to content

Instantly share code, notes, and snippets.

@regenrek
Created August 14, 2025 11:37
Show Gist options
  • Save regenrek/e1dafab39fbca6ddde7e500df9f47d59 to your computer and use it in GitHub Desktop.
Save regenrek/e1dafab39fbca6ddde7e500df9f47d59 to your computer and use it in GitHub Desktop.
R2 Empty all data
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