import { _Object, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, ListObjectsV2CommandOutput, S3Client, HeadObjectCommand, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3' import { writeFileSync, existsSync, readFileSync } from 'fs' import { S3Client as S3ClientLite } from '@bradenmacdonald/s3-lite-client/' import { Pool } from 'pg' import { Context } from 'vm' const S3_BUCKET = 'capgo' const CHECKPOINT_FILE = './objects_checkpoint.json' const OUTPUT_FILE = './objects.json' const BATCH_SIZE = 1000 // Write to disk every N objects interface Checkpoint { continuationToken: string | null objectCount: number lastUpdate: string } async function main() { const args = process.argv.slice(2) const command = args[0] if (!command) { console.error('❌ No command specified') console.error('Usage: ts-node check_r2_big_files.ts ') console.error('Commands:') console.error(' fetch_objects - Fetch all objects from R2') console.error(' json_big_files - Process big files from JSON') console.error(' folder_size - Calculate total size of a folder') console.error(' total_size - Calculate total size of all objects') console.error(' object_size - Get size of a specific object') console.error(' export_files_folder_to_csv - Export all files in a folder to CSV') console.error(' get_app_versions - Export all app_versions from database to JSON') console.error(' get_big_orgs - Analyze organization storage usage from app versions and objects data') console.error(' export_supabase_csv - Export bundle information for an organization to CSV') console.error(' prepare_cleanup_zip - Find orphaned zip files in R2 with no database records') console.error(' copy_cleanup_candidates_to_backup_bucket - Copy cleanup candidates to backup bucket') console.error(' copy_cleanup_candidates_direct - Copy cleanup candidates using direct S3 copy (faster but may not work on R2)') console.error(' delete_cleanup_candidates - Delete orphaned files from main bucket (USE WITH CAUTION!)') process.exit(1) } switch (command) { case 'fetch_objects': await fetch_objects() break case 'json_big_files': await json_big_files() break case 'folder_size': const folderPath = args[1] if (!folderPath) { console.error('❌ No folder path specified') console.error('Usage: ts-node check_r2_big_files.ts folder_size ') process.exit(1) } await folder_size(folderPath) break case 'total_size': await total_size() break case 'object_size': const objectPath = args[1] if (!objectPath) { console.error('❌ No object path specified') console.error('Usage: ts-node check_r2_big_files.ts object_size ') console.error('Example: ts-node check_r2_big_files.ts object_size orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') process.exit(1) } await object_size(objectPath) break case 'export_files_folder_to_csv': const csvFolderPath = args[1] if (!csvFolderPath) { console.error('❌ No folder path specified') console.error('Usage: ts-node check_r2_big_files.ts export_files_folder_to_csv ') console.error('Example: ts-node check_r2_big_files.ts export_files_folder_to_csv orgs/my-org/apps/') process.exit(1) } await export_files_folder_to_csv(csvFolderPath) break case 'get_app_versions': await get_app_versions() break case 'get_big_orgs': await get_big_orgs() break case 'export_supabase_csv': const orgId = args[1] if (!orgId) { console.error('❌ No organization ID specified') console.error('Usage: ts-node check_r2_big_files.ts export_supabase_csv ') process.exit(1) } await export_supabase_csv(orgId) break case 'prepare_cleanup_zip': await prepare_cleanup_zip() break case 'copy_cleanup_candidates_to_backup_bucket': await copy_cleanup_candidates_to_backup_bucket() break case 'copy_cleanup_candidates_direct': await copy_cleanup_candidates_direct() break case 'delete_cleanup_candidates': await delete_cleanup_candidates() break default: console.error(`❌ Unknown command: ${command}`) console.error('Available commands: fetch_objects, json_big_files, folder_size, total_size, object_size, export_files_folder_to_csv, get_app_versions, get_big_orgs, export_supabase_csv, prepare_cleanup_zip, copy_cleanup_candidates_to_backup_bucket, copy_cleanup_candidates_direct, delete_cleanup_candidates') process.exit(1) } } async function fetch_objects() { const s3 = await initS3() // Check if we have a previous checkpoint let checkpoint: Checkpoint | null = null let existingObjects: _Object[] = [] if (existsSync(CHECKPOINT_FILE)) { console.log('🔄 Found checkpoint file, resuming from last position...') checkpoint = JSON.parse(readFileSync(CHECKPOINT_FILE, 'utf-8')) // Load existing objects if output file exists if (existsSync(OUTPUT_FILE)) { existingObjects = JSON.parse(readFileSync(OUTPUT_FILE, 'utf-8')) console.log(`📋 Loaded ${existingObjects.length} existing objects`) } } const objects = await listAllObjectsInFolder(s3, 'orgs/', checkpoint, existingObjects) // Final save writeFileSync(OUTPUT_FILE, JSON.stringify(objects, null, 2)) // Clean up checkpoint file on successful completion if (existsSync(CHECKPOINT_FILE)) { const fs = await import('fs/promises') await fs.unlink(CHECKPOINT_FILE) console.log('✅ Completed! Checkpoint file removed.') } console.log(`\n🎉 Done! Total objects: ${objects.length}`) // const bigFiles = objects.filter(obj => (obj.Size ?? 0) > 100 * 1024 * 1024) // 100MB } async function json_big_files() { if (!existsSync(OUTPUT_FILE)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading objects from JSON...') const objects: _Object[] = JSON.parse(readFileSync(OUTPUT_FILE, 'utf-8')) console.log(`📊 Total objects loaded: ${objects.length}`) // Sort by size (descending) and take top 50 const topFiles = objects .filter(obj => obj.Size !== undefined && obj.Size > 0) .sort((a, b) => (b.Size ?? 0) - (a.Size ?? 0)) .slice(0, 50) console.log('\n🏆 Top 50 Biggest Files:') console.log('========================') topFiles.forEach((file, index) => { const sizeMB = ((file.Size ?? 0) / (1024 * 1024)).toFixed(2) console.log(`${(index + 1).toString().padStart(2, '0')}. ${file.Key} - ${sizeMB} MB`) }) // Calculate total size of top 50 const totalSize = topFiles.reduce((sum, file) => sum + (file.Size ?? 0), 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) console.log('\n📈 Summary:') console.log(`Total size of top 50 files: ${totalSizeGB} GB`) // Save to separate file const outputPath = './top_50_biggest_files.json' writeFileSync(outputPath, JSON.stringify(topFiles, null, 2)) console.log(`\n✅ Saved top 50 biggest files to: ${outputPath}`) } async function folder_size(folderPath: string) { if (!existsSync(OUTPUT_FILE)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading objects from JSON...') const objects: _Object[] = JSON.parse(readFileSync(OUTPUT_FILE, 'utf-8')) console.log(`📊 Total objects loaded: ${objects.length}`) // Ensure folder path ends with / const normalizedPath = folderPath.endsWith('/') ? folderPath : folderPath + '/' // Filter objects in the specified folder const folderObjects = objects.filter(obj => obj.Key && obj.Key.startsWith(normalizedPath) ) if (folderObjects.length === 0) { console.log(`❌ No files found in folder: ${normalizedPath}`) return } // Calculate total size const totalSize = folderObjects.reduce((sum, obj) => sum + (obj.Size ?? 0), 0) // Calculate size in different units const sizeKB = totalSize / 1024 const sizeMB = sizeKB / 1024 const sizeGB = sizeMB / 1024 const sizeTB = sizeGB / 1024 console.log(`\n📁 Folder: ${normalizedPath}`) console.log('========================') console.log(`📊 Total files: ${folderObjects.length}`) console.log(`💾 Total size:`) console.log(` - ${totalSize.toLocaleString()} bytes`) console.log(` - ${sizeKB.toFixed(2)} KB`) console.log(` - ${sizeMB.toFixed(2)} MB`) console.log(` - ${sizeGB.toFixed(2)} GB`) if (sizeTB >= 0.01) { console.log(` - ${sizeTB.toFixed(2)} TB`) } // Find biggest files in this folder const topFiles = folderObjects .filter(obj => obj.Size && obj.Size > 0) .sort((a, b) => (b.Size ?? 0) - (a.Size ?? 0)) .slice(0, 10) if (topFiles.length > 0) { console.log(`\n🏆 Top ${Math.min(10, topFiles.length)} biggest files in this folder:`) topFiles.forEach((file, index) => { const sizeMB = ((file.Size ?? 0) / (1024 * 1024)).toFixed(2) const relativePath = file.Key?.substring(normalizedPath.length) ?? '' console.log(`${(index + 1).toString().padStart(2, '0')}. ${relativePath} - ${sizeMB} MB`) }) } } async function total_size() { if (!existsSync(OUTPUT_FILE)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading objects from JSON...') const objects: _Object[] = JSON.parse(readFileSync(OUTPUT_FILE, 'utf-8')) console.log(`📊 Total objects loaded: ${objects.length}`) // Calculate total size const totalSize = objects.reduce((sum, obj) => sum + (obj.Size ?? 0), 0) // Calculate size in different units const sizeKB = totalSize / 1024 const sizeMB = sizeKB / 1024 const sizeGB = sizeMB / 1024 const sizeTB = sizeGB / 1024 const sizePB = sizeTB / 1024 console.log('\n📊 Total Storage Summary') console.log('========================') console.log(`📁 Total objects: ${objects.length.toLocaleString()}`) console.log(`💾 Total size:`) console.log(` - ${totalSize.toLocaleString()} bytes`) console.log(` - ${sizeKB.toFixed(2)} KB`) console.log(` - ${sizeMB.toFixed(2)} MB`) console.log(` - ${sizeGB.toFixed(2)} GB`) console.log(` - ${sizeTB.toFixed(2)} TB`) if (sizePB >= 0.01) { console.log(` - ${sizePB.toFixed(2)} PB`) } // Calculate average file size const avgSize = totalSize / objects.length const avgSizeMB = avgSize / (1024 * 1024) console.log(`\n📈 Average file size: ${avgSizeMB.toFixed(2)} MB`) // Group by size ranges const sizeRanges = { '< 1MB': 0, '1MB - 10MB': 0, '10MB - 100MB': 0, '100MB - 1GB': 0, '> 1GB': 0 } objects.forEach(obj => { const size = obj.Size ?? 0 const sizeMB = size / (1024 * 1024) const sizeGB = sizeMB / 1024 if (sizeMB < 1) sizeRanges['< 1MB']++ else if (sizeMB < 10) sizeRanges['1MB - 10MB']++ else if (sizeMB < 100) sizeRanges['10MB - 100MB']++ else if (sizeGB < 1) sizeRanges['100MB - 1GB']++ else sizeRanges['> 1GB']++ }) console.log('\n📊 File Size Distribution:') Object.entries(sizeRanges).forEach(([range, count]) => { const percentage = (count / objects.length * 100).toFixed(1) console.log(` ${range}: ${count.toLocaleString()} files (${percentage}%)`) }) } function convertToValidS3Path(path: string) { return path // path.replaceAll('+', '%2B') } async function object_size(objectPath: string) { console.log('🔄 Checking if object exists in R2...') const s3 = await initS3() try { // First check if object exists (similar to checkIfExist in s3.ts) let objectExists = false let headResponse try { const response = await s3.send(new HeadObjectCommand({ Bucket: S3_BUCKET, Key: objectPath })) const s3Lite = await initS3Lite() const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) } catch (error: any) { if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { objectExists = false } else { throw error // Re-throw if it's not a "not found" error } } if (!objectExists) { console.error(`❌ Object not exist in R2: ${objectPath}`) console.error('\n💡 Tips:') console.error(' - Make sure the path is exact (case-sensitive)') console.error(' - Check if the object exists in R2') console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') return } console.log('✅ Object exists, fetching details...') return // Calculate size in different units const size = headResponse!.ContentLength ?? 0 const sizeKB = size / 1024 const sizeMB = sizeKB / 1024 const sizeGB = sizeMB / 1024 console.log('\n📄 Object Details (Live from R2)') console.log('===================================') console.log(`🔑 Key: ${objectPath}`) console.log(`💾 Size:`) console.log(` - ${size.toLocaleString()} bytes`) console.log(` - ${sizeKB.toFixed(2)} KB`) console.log(` - ${sizeMB.toFixed(2)} MB`) if (sizeGB >= 0.01) { console.log(` - ${sizeGB.toFixed(2)} GB`) } if (headResponse!.LastModified) { console.log(`📅 Last Modified: ${headResponse!.LastModified.toISOString()}`) } if (headResponse!.ETag) { console.log(`🏷️ ETag: ${headResponse!.ETag}`) } if (headResponse!.ContentType) { console.log(`📝 Content Type: ${headResponse!.ContentType}`) } if (headResponse!.StorageClass) { console.log(`💼 Storage Class: ${headResponse!.StorageClass}`) } // Show file extension info const extension = objectPath.split('.').pop() if (extension) { console.log(`📝 File Extension: .${extension}`) } // Show folder path const folderPath = objectPath.substring(0, objectPath.lastIndexOf('/')) if (folderPath) { console.log(`📁 Folder: ${folderPath}/`) } } catch (error: any) { console.error('❌ Error checking object:', error.message) if (error.$metadata?.httpStatusCode) { console.error(` HTTP Status: ${error.$metadata.httpStatusCode}`) } } } async function listAllObjectsInFolder( s3: S3Client, path: string, checkpoint: Checkpoint | null = null, existingObjects: _Object[] = [] ) { const folderPrefix = path let continuationToken: string | null = checkpoint?.continuationToken || null let objects = [...existingObjects] let batchCount = 0 const startTime = Date.now() try { while (true) { const data = await s3.send(new ListObjectsV2Command({ Bucket: S3_BUCKET, Prefix: folderPrefix, ContinuationToken: continuationToken || undefined, MaxKeys: 1000 // Request max items per batch })) as ListObjectsV2CommandOutput const newObjects = data.Contents ?? [] objects = objects.concat(newObjects) batchCount++ // Progress update const elapsed = (Date.now() - startTime) / 1000 const rate = objects.length / elapsed console.log(`📊 Progress: ${objects.length} objects | Batch #${batchCount} | ${newObjects.length} new | Rate: ${rate.toFixed(0)} obj/s`) // Save checkpoint and data every BATCH_SIZE objects if (objects.length % BATCH_SIZE === 0 || !data.IsTruncated) { // Save current progress writeFileSync(OUTPUT_FILE, JSON.stringify(objects, null, 2)) // Save checkpoint (only if not finished) if (data.IsTruncated) { const checkpoint: Checkpoint = { continuationToken: data.NextContinuationToken ?? null, objectCount: objects.length, lastUpdate: new Date().toISOString() } writeFileSync(CHECKPOINT_FILE, JSON.stringify(checkpoint, null, 2)) console.log(`💾 Checkpoint saved at ${objects.length} objects`) } } continuationToken = data.NextContinuationToken ?? null if (!data.IsTruncated) { break } } } catch (err) { console.error('❌ Error listing objects:', err) console.error('💡 Progress has been saved. You can re-run the script to resume.') process.exit(1) } return objects } function getEnv(s: string) { return process.env[s] ?? '' } export function initS3() { const c = null const access_key_id = getEnv('S3_ACCESS_KEY_ID') const access_key_secret = getEnv('S3_SECRET_ACCESS_KEY') const storageEndpoint = getEnv('S3_ENDPOINT') const useSsl = getEnv('S3_SSL') !== 'false' const storageRegion = getEnv('S3_REGION') const params = { credentials: { accessKeyId: access_key_id, secretAccessKey: access_key_secret, }, endpoint: `${useSsl ? 'https' : 'http'}://${storageEndpoint}`, region: storageRegion ?? 'us-east-1', // not apply in supabase local forcePathStyle: true, // storageEndpoint !== '127.0.0.1:54321/storage/v1/s3', signingEscapePath: false, // signingEscapePath: storageEndpoint !== '127.0.0.1:54321/storage/v1/s3', } console.log({ message: 'initS3', params }) return new S3Client({ ...params }) } async function initS3Lite() { const access_key_id = getEnv('S3_ACCESS_KEY_ID') const access_key_secret = getEnv('S3_SECRET_ACCESS_KEY') const storageEndpoint = getEnv('S3_ENDPOINT') const storageRegion = getEnv('S3_REGION') || 'us-east-1' const useSSL = getEnv('S3_SSL') === 'true' const bucket = getEnv('S3_BUCKET') const endPoint = useSSL ? `https://${storageEndpoint}` : `http://${storageEndpoint}` const options = { endPoint, accessKey: access_key_id, pathStyle: true, secretKey: access_key_secret, region: storageRegion, bucket, } console.log({ message: 'initS3Lite', options }) const client = new S3ClientLite(options) return client } async function export_files_folder_to_csv(folderPath: string) { if (!existsSync(OUTPUT_FILE)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading objects from JSON...') const objects: _Object[] = JSON.parse(readFileSync(OUTPUT_FILE, 'utf-8')) console.log(`📊 Total objects loaded: ${objects.length}`) // Ensure folder path ends with / const normalizedPath = folderPath.endsWith('/') ? folderPath : folderPath + '/' // Filter objects in the specified folder const folderObjects = objects.filter(obj => obj.Key && obj.Key.startsWith(normalizedPath) && obj.Key.endsWith('.zip') ) if (folderObjects.length === 0) { console.log(`❌ No files found in folder: ${normalizedPath}`) return } console.log(`📁 Found ${folderObjects.length} files in folder: ${normalizedPath}`) // Create CSV content const csvHeaders = [ 'app_id', 'version_name', 'version_id', 'size_bytes', 'size_gb', 'link' ] const csvRows = folderObjects.map(obj => { const size = obj.Size ?? 0 const sizeGB = (size / (1024 * 1024 * 1024)).toFixed(6) const webLink = `https://console.capgo.app/app/${obj.Key?.split('/').pop() ?? ''}` return [ `"${obj.Key?.split('/').pop() ?? ''}"`, `"${obj.Key?.split('/').slice(-2).pop() ?? ''}"`, obj.Key?.split('/').pop() ?? '', size.toString(), sizeGB, `"${webLink}"` ].join(',') }) const csvContent = [csvHeaders.join(','), ...csvRows].join('\n') // Generate output filename const sanitizedPath = normalizedPath.replace(/[^\w]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '') const outputPath = `./folder_export_${sanitizedPath}.csv` // Write CSV file writeFileSync(outputPath, csvContent) // Calculate some statistics const totalSize = folderObjects.reduce((sum, obj) => sum + (obj.Size ?? 0), 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) console.log('\n📊 Export Summary:') console.log('==================') console.log(`📁 Folder: ${normalizedPath}`) console.log(`📄 Files exported: ${folderObjects.length}`) console.log(`💾 Total size: ${totalSizeGB} GB`) console.log(`📝 CSV file: ${outputPath}`) // Show top 10 biggest files const topFiles = folderObjects .filter(obj => obj.Size && obj.Size > 0) .sort((a, b) => (b.Size ?? 0) - (a.Size ?? 0)) .slice(0, 10) if (topFiles.length > 0) { console.log(`\n🏆 Top ${Math.min(10, topFiles.length)} biggest files:`) topFiles.forEach((file, index) => { const sizeMB = ((file.Size ?? 0) / (1024 * 1024)).toFixed(2) const relativePath = file.Key?.substring(normalizedPath.length) ?? '' console.log(`${(index + 1).toString().padStart(2, '0')}. ${relativePath} - ${sizeMB} MB`) }) } console.log(`\n✅ Successfully exported to: ${outputPath}`) } function existInEnv(s: string) { return process.env[s] !== undefined } export function getDatabaseURL(): string { // TODO: uncomment when we enable back replicate // const clientContinent = (c.req.raw as any)?.cf?.continent // cloudlog({ requestId: c.get('requestId'), message: 'clientContinent', clientContinent }) let DEFAULT_DB_URL = getEnv('SUPABASE_DB_URL') if (existInEnv('MAIN_SUPABASE_DB_URL')) DEFAULT_DB_URL = getEnv('MAIN_SUPABASE_DB_URL') // if (!clientContinent) // return DEFAULT_DB_URL // Hyperdrive test if (existInEnv('HYPERDRIVE_CAPGO_DIRECT_EU')) return (getEnv('HYPERDRIVE_CAPGO_DIRECT_EU') as any).connectionString // // Default to Germany for any other cases return DEFAULT_DB_URL } export function getPgClient(c: Context) { const dbUrl = getDatabaseURL() console.log({ message: 'getPgClient', dbUrl }) return new Pool({ connectionString: dbUrl, max: 1, idleTimeoutMillis: 2000, }) } async function get_app_versions() { console.log('🔄 Connecting to database...') // Create a mock context for getPgClient const mockContext = {} as Context const pool = getPgClient(mockContext) try { console.log('📊 Fetching all app versions...') // Execute the query to get all app_versions as JSON const result = await pool.query('SELECT json_agg(app_versions.*) as data FROM app_versions') const appVersions = result.rows[0].data ?? [] console.log(`✅ Found ${appVersions.length} app versions`) // Write to JSON file - data is already in JSON format const outputFile = './app_versions.json' writeFileSync(outputFile, JSON.stringify(appVersions, null, 2)) console.log(`💾 Saved app versions to: ${outputFile}`) } catch (error) { console.error('❌ Error fetching app versions:', error) process.exit(1) } finally { // Close the database connection console.log('🔒 Closing database connection...') await pool.end() console.log('✅ Database connection closed') } } async function get_big_orgs() { console.log('🔄 Analyzing organization storage usage...') // Check if required files exist const appVersionsFile = './app_versions.json' const objectsFile = './objects.json' if (!existsSync(appVersionsFile)) { console.error('❌ No app_versions.json file found. Run "get_app_versions" first.') process.exit(1) } if (!existsSync(objectsFile)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading app versions and objects data...') // Load both files const appVersions = JSON.parse(readFileSync(appVersionsFile, 'utf-8')) const objects: _Object[] = JSON.parse(readFileSync(objectsFile, 'utf-8')) console.log(`📊 Loaded ${appVersions.length} app versions and ${objects.length} objects`) // Create a map of r2_path -> size for fast lookup console.log('🗺️ Creating object size lookup map...') const objectSizeMap = new Map() objects.forEach(obj => { if (obj.Key && obj.Size) { objectSizeMap.set(obj.Key, obj.Size) } }) // Process app versions to calculate org sizes console.log('🔍 Processing app versions to calculate org storage usage...') const orgSizes = new Map() let foundVersions = 0 let missingVersions = 0 appVersions.forEach((version: any) => { if (version.r2_path && version.owner_org) { const size = objectSizeMap.get(version.r2_path) if (size) { const currentSize = orgSizes.get(version.owner_org)?? 0 orgSizes.set(version.owner_org, currentSize + size) foundVersions++ } else { missingVersions++ } } }) console.log(`✅ Found sizes for ${foundVersions} versions, ${missingVersions} missing from R2`) // Sort organizations by total size (descending) const sortedOrgs = Array.from(orgSizes.entries()) .map(([orgId, size]) => ({ orgId, size })) .sort((a, b) => b.size - a.size) console.log(`📊 Found ${sortedOrgs.length} organizations with storage usage`) // Filter orgs over 100 MB and fetch their plan information const orgsOver100MB = sortedOrgs.filter(org => org.size > 100 * 1024 * 1024) // 100 MB in bytes console.log(`💳 Fetching plan information for ${orgsOver100MB.length} organizations over 100 MB...`) let planInfoMap = new Map() if (orgsOver100MB.length > 0) { // Create a mock context for getPgClient const mockContext = {} as Context const pool = getPgClient(mockContext) try { // Create all the plan queries const planQueries = orgsOver100MB.map(org => pool.query( `select name, storage from plans where (select price_id from stripe_info where customer_id=(select customer_id from orgs where id = $1)) = any(array[price_m_id, price_y_id, price_m_id_us, price_y_id_us]::text[] || alt_price_m_ids || alt_price_y_ids)`, [org.orgId] ) ) // Execute all queries in parallel console.log('⚡ Running plan queries in parallel...') const planResults = await Promise.all(planQueries) // Map results back to org IDs orgsOver100MB.forEach((org, index) => { const planResult = planResults[index] if (planResult && planResult.rows.length > 0) { planInfoMap.set(org.orgId, planResult.rows[0]) } }) console.log(`✅ Found plan information for ${planInfoMap.size}/${orgsOver100MB.length} organizations`) } catch (error) { console.error('❌ Error fetching plan information:', error) } finally { // Close the database connection await pool.end() } } // Generate report content const reportLines: string[] = [] reportLines.push('Organization Storage Usage Report') reportLines.push('=====================================') reportLines.push(`Generated: ${new Date().toISOString()}`) reportLines.push(`Total Organizations: ${sortedOrgs.length}`) reportLines.push(`App Versions Processed: ${foundVersions}`) reportLines.push(`Organizations with Plan Info: ${planInfoMap.size}`) reportLines.push('') sortedOrgs.forEach((org, index) => { const sizeGB = (org.size / (1024 * 1024 * 1024)).toFixed(2) const sizeMB = (org.size / (1024 * 1024)).toFixed(2) const displaySize = parseFloat(sizeGB) >= 0.01 ? `${sizeGB} GB` : `${sizeMB} MB` let reportLine = `${(index + 1).toString().padStart(3, '0')}. org: ${org.orgId} || size: ${displaySize}` // Add plan information if available const planInfo = planInfoMap.get(org.orgId) if (planInfo) { const planStorageGB = planInfo.storage ? (planInfo.storage / (1024 * 1024 * 1024)).toFixed(0) : 'N/A' reportLine += ` || plan: ${planInfo.name || 'Unknown'}, storage: ${planStorageGB} GB` } reportLines.push(reportLine) }) // Calculate total storage const totalSize = sortedOrgs.reduce((sum, org) => sum + org.size, 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) reportLines.push('') reportLines.push('Summary:') reportLines.push(`Total Storage Used: ${totalSizeGB} GB`) reportLines.push(`Average per Org: ${(parseFloat(totalSizeGB) / sortedOrgs.length).toFixed(2)} GB`) // Write report to file const outputFile = './big_orgs_report.txt' writeFileSync(outputFile, reportLines.join('\n')) console.log('\n📈 Top 10 Organizations by Storage:') console.log('====================================') sortedOrgs.slice(0, 10).forEach((org, index) => { const sizeGB = (org.size / (1024 * 1024 * 1024)).toFixed(2) let line = `${(index + 1).toString().padStart(2, '0')}. ${org.orgId} - ${sizeGB} GB` // Add plan info if available const planInfo = planInfoMap.get(org.orgId) if (planInfo) { const planStorageGB = planInfo.storage ? (planInfo.storage / (1024 * 1024 * 1024)).toFixed(0) : 'N/A' line += ` (Plan: ${planInfo.name || 'Unknown'}, ${planStorageGB} GB)` } console.log(line) }) console.log(`\n💾 Total Storage: ${totalSizeGB} GB`) console.log(`✅ Full report saved to: ${outputFile}`) } async function export_supabase_csv(orgId: string) { console.log(`🔄 Exporting bundle information for organization: ${orgId}`) // Check if required files exist const appVersionsFile = './app_versions.json' const objectsFile = './objects.json' if (!existsSync(appVersionsFile)) { console.error('❌ No app_versions.json file found. Run "get_app_versions" first.') process.exit(1) } if (!existsSync(objectsFile)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading app versions and objects data...') // Load both files const appVersions = JSON.parse(readFileSync(appVersionsFile, 'utf-8')) const objects: _Object[] = JSON.parse(readFileSync(objectsFile, 'utf-8')) console.log(`📊 Loaded ${appVersions.length} app versions and ${objects.length} objects`) // Filter app versions for the specified organization const orgVersions = appVersions.filter((version: any) => version.owner_org === orgId) if (orgVersions.length === 0) { console.error(`❌ No app versions found for organization: ${orgId}`) console.error('💡 Make sure the organization ID is correct') return } console.log(`🎯 Found ${orgVersions.length} app versions for organization: ${orgId}`) // Create a set of r2_path from local objects to check existence console.log('🗺️ Creating object existence lookup from local data...') const localObjectsSet = new Set() objects.forEach(obj => { if (obj.Key) { localObjectsSet.add(obj.Key) } }) // Initialize S3 client for R2 console.log('🔗 Connecting to R2...') const s3 = await initS3() // Filter versions that have r2_path AND exist in local objects const versionsWithR2 = orgVersions.filter((version: any) => version.r2_path && localObjectsSet.has(version.r2_path) ) console.log(`📦 Found ${versionsWithR2.length} versions with R2 paths that exist in local objects`) // Fetch actual sizes from R2 in parallel console.log('⚡ Fetching actual bundle sizes from R2 in parallel...') const sizeQueries = versionsWithR2.map(async (version: any) => { try { const response = await s3.send(new HeadObjectCommand({ Bucket: S3_BUCKET, Key: version.r2_path })) return { version, size: response.ContentLength?? 0, found: true } } catch (error: any) { if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { return { version, size: 0, found: false } } else { console.error(`❌ Error fetching ${version.r2_path}:`, error.message) return { version, size: 0, found: false } } } }) // Execute all queries in parallel const sizeResults = await Promise.all(sizeQueries) // Process versions and create CSV data console.log('📊 Processing versions and generating CSV data...') const csvData: any[] = [] let foundSizes = 0 let missingSizes = 0 // Process all versions (including those without r2_path) orgVersions.forEach((version: any) => { let sizeBytes = 0 let found = false if (version.r2_path) { const sizeResult = sizeResults.find(result => result.version.id === version.id) if (sizeResult) { sizeBytes = sizeResult.size found = sizeResult.found if (found) { foundSizes++ } else { missingSizes++ } } } else { missingSizes++ } const sizeGB = (sizeBytes / (1024 * 1024 * 1024)).toFixed(6) const webLink = `https://console.capgo.app/app/${version.app_id}/bundle/${version.id}` csvData.push({ app_id: version.app_id ?? '', version_name: version.name ?? '', version_id: version.id ?? '', size_bytes: sizeBytes, size_gb: sizeGB, link: webLink }) }) console.log(`✅ Found sizes for ${foundSizes} versions, ${missingSizes} missing/no R2 path`) // Sort by size (descending) csvData.sort((a, b) => b.size_bytes - a.size_bytes) // Create CSV content const csvHeaders = [ 'app_id', 'version_name', 'version_id', 'size_bytes', 'size_gb', 'link' ] const csvRows = csvData.filter(row => row.size_bytes > 0).map(row => [ `"${row.app_id}"`, `"${row.version_name}"`, row.version_id, row.size_bytes, row.size_gb, `"${row.link}"` ].join(',')) const csvContent = [csvHeaders.join(','), ...csvRows].join('\n') // Generate output filename const sanitizedOrgId = orgId.replace(/[^\w-]/g, '_') const outputPath = `./org_${sanitizedOrgId}_bundles.csv` // Write CSV file writeFileSync(outputPath, csvContent) // Calculate statistics const totalSize = csvData.reduce((sum, row) => sum + row.size_bytes, 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) const avgSizeGB = csvData.length > 0 ? (totalSize / csvData.length / (1024 * 1024 * 1024)).toFixed(2) : '0' // Get unique apps const uniqueApps = new Set(csvData.map(row => row.app_id)) console.log('\n📊 Export Summary:') console.log('==================') console.log(`🏢 Organization: ${orgId}`) console.log(`📱 Unique Apps: ${uniqueApps.size}`) console.log(`📦 Total Bundles: ${csvData.length}`) console.log(`💾 Total Size: ${totalSizeGB} GB`) console.log(`📈 Average Bundle Size: ${avgSizeGB} GB`) console.log(`📝 CSV file: ${outputPath}`) // Show top 10 biggest bundles const topBundles = csvData.slice(0, 10) if (topBundles.length > 0) { console.log(`\n🏆 Top ${Math.min(10, topBundles.length)} biggest bundles:`) topBundles.forEach((bundle, index) => { const sizeMB = (bundle.size_bytes / (1024 * 1024)).toFixed(2) console.log(`${(index + 1).toString().padStart(2, '0')}. ${bundle.app_id}@${bundle.version_name} - ${sizeMB} MB`) }) } console.log(`\n✅ Successfully exported to: ${outputPath}`) } async function prepare_cleanup_zip() { console.log('🔄 Analyzing zip files for cleanup candidates...') // Check if required files exist const objectsFile = './objects.json' if (!existsSync(objectsFile)) { console.error('❌ No objects.json file found. Run "fetch_objects" first.') process.exit(1) } console.log('📂 Loading objects data...') // Load objects file const objects: _Object[] = JSON.parse(readFileSync(objectsFile, 'utf-8')) console.log(`📊 Loaded ${objects.length} objects`) // Filter for zip files that are direct children of apps folders const zipFiles = objects.filter(obj => { if (!obj.Key || !obj.Key.endsWith('.zip')) { return false } // Check if the file is a direct child of an apps folder // Expected pattern: orgs/{org-id}/apps/{app-id}/{version}.zip const pathParts = obj.Key.split('/') // Find the index of 'apps' in the path const appsIndex = pathParts.findIndex(part => part === 'apps') if (appsIndex === -1) { return false // No 'apps' folder in path } // Check if the zip file is exactly 2 levels deep from the apps folder // apps/{app-id}/{version}.zip means appsIndex + 2 should be the zip file const isDirectChild = appsIndex + 2 === pathParts.length - 1 return isDirectChild }) console.log(`📦 Found ${zipFiles.length} zip files`) if (zipFiles.length === 0) { console.log('✅ No zip files found to analyze') return } // Connect to database console.log('🔗 Connecting to database...') const mockContext = {} as Context const pool = getPgClient(mockContext) // Define types for cleanup data interface CleanupCandidate { key: string size: number lastModified: Date | null reason: string error: string | null } try { // Check all zip files with a single database query for better performance console.log('⚡ Checking database records for all zip files with single optimized query...') // Extract all zip file keys const zipFileKeys = zipFiles.map(zipFile => zipFile.Key).filter(key => key !== undefined) as string[] // Single query to check all zip files at once const result = await pool.query( 'SELECT r2_path FROM app_versions WHERE r2_path = ANY($1)', [zipFileKeys] ) // Create a set of existing r2_paths for fast lookup const existingPaths = new Set(result.rows.map(row => row.r2_path)) console.log(`✅ Found ${existingPaths.size} zip files with database records out of ${zipFiles.length} total`) // Process results const toDelete: CleanupCandidate[] = [] let hasRecords = 0 let orphaned = 0 zipFiles.forEach(zipFile => { if (zipFile.Key && existingPaths.has(zipFile.Key)) { hasRecords++ } else { orphaned++ toDelete.push({ key: zipFile.Key ?? '', size: zipFile.Size?? 0, lastModified: zipFile.LastModified || null, reason: 'No matching app_versions record found', error: null }) } }) console.log(`✅ Analysis complete:`) console.log(` 📦 Total zip files: ${zipFiles.length}`) console.log(` ✅ With database records: ${hasRecords}`) console.log(` 🗑️ Orphaned (to delete): ${orphaned}`) // Calculate total size of files to delete const totalDeleteSize = toDelete.reduce((sum, file) => sum + file.size, 0) const totalDeleteSizeGB = (totalDeleteSize / (1024 * 1024 * 1024)).toFixed(2) console.log(` 💾 Total size to delete: ${totalDeleteSizeGB} GB`) // Save to JSON file const outputFile = './cleanup_candidates.json' const cleanupData = { generated: new Date().toISOString(), summary: { totalZipFiles: zipFiles.length, withDatabaseRecords: hasRecords, orphanedFiles: orphaned, totalSizeToDelete: totalDeleteSize, totalSizeToDeleteGB: parseFloat(totalDeleteSizeGB) }, toDelete: toDelete.sort((a, b) => b.size - a.size) // Sort by size descending } writeFileSync(outputFile, JSON.stringify(cleanupData, null, 2)) // Show top 10 biggest files to delete if (toDelete.length > 0) { console.log(`\n🏆 Top ${Math.min(10, toDelete.length)} biggest orphaned files:`) toDelete.slice(0, 10).forEach((file, index) => { const sizeMB = (file.size / (1024 * 1024)).toFixed(2) console.log(`${(index + 1).toString().padStart(2, '0')}. ${file.key} - ${sizeMB} MB`) }) } console.log(`\n✅ Cleanup candidates saved to: ${outputFile}`) if (orphaned > 0) { console.log(`\n⚠️ Found ${orphaned} orphaned zip files totaling ${totalDeleteSizeGB} GB`) console.log('💡 Review the cleanup_candidates.json file before proceeding with deletion') } else { console.log('\n🎉 No orphaned files found! All zip files have corresponding database records.') } } catch (error) { console.error('❌ Error during cleanup analysis:', error) process.exit(1) } finally { // Close the database connection console.log('🔒 Closing database connection...') await pool.end() console.log('✅ Database connection closed') } } async function copy_cleanup_candidates_to_backup_bucket() { console.log('🔄 Copying cleanup candidates to backup bucket...') const BACKUP_BUCKET = 'backup-cleanup-r2' // Check if cleanup_candidates.json exists const cleanupFile = './cleanup_candidates.json' if (!existsSync(cleanupFile)) { console.error('❌ No cleanup_candidates.json file found. Run "prepare_cleanup_zip" first.') process.exit(1) } console.log('📂 Loading cleanup candidates...') // Load cleanup candidates file const cleanupData = JSON.parse(readFileSync(cleanupFile, 'utf-8')) const toDelete = cleanupData.toDelete ?? [] if (toDelete.length === 0) { console.log('✅ No files to copy - cleanup candidates is empty') return } console.log(`📦 Found ${toDelete.length} files to copy to backup bucket`) // Initialize S3 client console.log('🔗 Connecting to R2...') const s3 = await initS3() // Calculate total size const totalSize = toDelete.reduce((sum: number, file: any) => sum + (file.size?? 0), 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) console.log(`💾 Total size to copy: ${totalSizeGB} GB`) console.log(`📁 From bucket: ${S3_BUCKET}`) console.log(`📁 To backup bucket: ${BACKUP_BUCKET}`) // Copy files in batches based on memory limit (3GB) console.log('⚡ Downloading and re-uploading files to backup bucket in memory-safe batches...') const MAX_MEMORY_BYTES = 3 * 1024 * 1024 * 1024 // 3GB const allResults: any[] = [] let currentBatch: any[] = [] let currentBatchSize = 0 let batchNumber = 1 // Group files into memory-safe batches const batches: any[][] = [] for (const file of toDelete) { const fileSize = file.size?? 0 // If adding this file would exceed memory limit, start a new batch if (currentBatchSize + fileSize > MAX_MEMORY_BYTES && currentBatch.length > 0) { batches.push(currentBatch) currentBatch = [] currentBatchSize = 0 } currentBatch.push(file) currentBatchSize += fileSize } // Add the final batch if it has files if (currentBatch.length > 0) { batches.push(currentBatch) } console.log(`📦 Processing ${toDelete.length} files in ${batches.length} memory-safe batches`) // Process each batch sequentially for (const batch of batches) { const batchSizeGB = (batch.reduce((sum, file) => sum + (file.size?? 0), 0) / (1024 * 1024 * 1024)).toFixed(2) console.log(`🔄 Processing batch ${batchNumber}/${batches.length}: ${batch.length} files (${batchSizeGB} GB)`) const copyOperations = batch.map(async (file: any) => { try { // Step 1: Download the object from source bucket const getCommand = new GetObjectCommand({ Bucket: S3_BUCKET, Key: file.key }) const getResponse = await s3.send(getCommand) if (!getResponse.Body) { throw new Error('No body in response') } // Convert the stream to buffer const chunks: Uint8Array[] = [] const reader = getResponse.Body.transformToWebStream().getReader() while (true) { const { done, value } = await reader.read() if (done) break chunks.push(value) } const buffer = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)) let offset = 0 for (const chunk of chunks) { buffer.set(chunk, offset) offset += chunk.length } // Step 2: Upload the object to backup bucket const putCommand = new PutObjectCommand({ Bucket: BACKUP_BUCKET, Key: file.key, Body: buffer, ContentType: getResponse.ContentType, ContentLength: getResponse.ContentLength, Metadata: getResponse.Metadata }) await s3.send(putCommand) return { key: file.key, success: true, error: null } } catch (error: any) { console.error(`❌ Error downloading/uploading ${file.key}:`, error.message) return { key: file.key, success: false, error: error.message } } }) // Execute current batch in parallel const batchResults = await Promise.all(copyOperations) allResults.push(...batchResults) const batchSuccessful = batchResults.filter(r => r.success).length const batchFailed = batchResults.filter(r => !r.success).length console.log(`✅ Batch ${batchNumber} complete: ${batchSuccessful} successful, ${batchFailed} failed`) console.log(`📊 Overall progress: ${allResults.length}/${toDelete.length} files processed`) batchNumber++ } // Execute all copy operations const results = allResults // Analyze results const successful = results.filter(r => r.success) const failed = results.filter(r => !r.success) console.log('\n📊 Copy Results:') console.log('================') console.log(`✅ Successfully downloaded and re-uploaded: ${successful.length} files`) console.log(`❌ Failed to download/upload: ${failed.length} files`) if (failed.length > 0) { console.log('\n💥 Failed downloads/uploads:') failed.forEach((failure, index) => { console.log(`${(index + 1).toString().padStart(2, '0')}. ${failure.key} - ${failure.error}`) }) } // Save copy results const copyReport = { generated: new Date().toISOString(), summary: { totalFiles: toDelete.length, successfulCopies: successful.length, failedCopies: failed.length, totalSizeCopied: totalSize, totalSizeCopiedGB: parseFloat(totalSizeGB), sourceBucket: S3_BUCKET, backupBucket: BACKUP_BUCKET }, successful: successful, failed: failed } const reportFile = './backup_copy_report.json' writeFileSync(reportFile, JSON.stringify(copyReport, null, 2)) console.log(`\n📝 Copy report saved to: ${reportFile}`) if (successful.length === toDelete.length) { console.log('\n🎉 All files successfully downloaded and re-uploaded to backup bucket!') console.log('✅ Safe to proceed with deletion if needed') } else { console.log(`\n⚠️ ${failed.length} files failed to download/upload`) console.log('💡 Review failed operations before proceeding with any deletion') } console.log(`\n📈 Summary:`) console.log(` 📦 Files downloaded and re-uploaded: ${successful.length}/${toDelete.length}`) console.log(` 💾 Size transferred: ${totalSizeGB} GB`) console.log(` 📁 Backup bucket: ${BACKUP_BUCKET}`) } async function copy_cleanup_candidates_direct() { console.log('🔄 Copying cleanup candidates using direct S3 copy...') const BACKUP_BUCKET = 'backup-cleanup-r2' // Check if cleanup_candidates.json exists const cleanupFile = './cleanup_candidates.json' if (!existsSync(cleanupFile)) { console.error('❌ No cleanup_candidates.json file found. Run "prepare_cleanup_zip" first.') process.exit(1) } console.log('📂 Loading cleanup candidates...') // Load cleanup candidates file const cleanupData = JSON.parse(readFileSync(cleanupFile, 'utf-8')) const toDelete = cleanupData.toDelete ?? [] if (toDelete.length === 0) { console.log('✅ No files to copy - cleanup candidates is empty') return } console.log(`📦 Found ${toDelete.length} files to copy to backup bucket`) // Initialize S3 client console.log('🔗 Connecting to R2...') const s3 = await initS3() // Calculate total size const totalSize = toDelete.reduce((sum: number, file: any) => sum + (file.size?? 0), 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) console.log(`💾 Total size to copy: ${totalSizeGB} GB`) console.log(`📁 From bucket: ${S3_BUCKET}`) console.log(`📁 To backup bucket: ${BACKUP_BUCKET}`) // Copy files in parallel using direct S3 copy console.log('⚡ Copying files to backup bucket using direct S3 copy...') const copyOperations = toDelete.map(async (file: any, index: number) => { try { const copyCommand = new CopyObjectCommand({ Bucket: BACKUP_BUCKET, CopySource: `${S3_BUCKET}/${file.key}`, Key: file.key }) await s3.send(copyCommand) // Log progress every 10 files if ((index + 1) % 10 === 0) { console.log(`📊 Progress: ${index + 1}/${toDelete.length} files copied`) } return { key: file.key, success: true, error: null } } catch (error: any) { console.error(`❌ Error copying ${file.key}:`, error.message) return { key: file.key, success: false, error: error.message } } }) // Execute all copy operations in parallel const results = await Promise.all(copyOperations) // Analyze results const successful = results.filter(r => r.success) const failed = results.filter(r => !r.success) console.log('\n📊 Copy Results:') console.log('================') console.log(`✅ Successfully copied: ${successful.length} files`) console.log(`❌ Failed to copy: ${failed.length} files`) if (failed.length > 0) { console.log('\n💥 Failed copies:') failed.forEach((failure, index) => { console.log(`${(index + 1).toString().padStart(2, '0')}. ${failure.key} - ${failure.error}`) }) } // Save copy results const copyReport = { generated: new Date().toISOString(), summary: { totalFiles: toDelete.length, successfulCopies: successful.length, failedCopies: failed.length, totalSizeCopied: totalSize, totalSizeCopiedGB: parseFloat(totalSizeGB), sourceBucket: S3_BUCKET, backupBucket: BACKUP_BUCKET, method: 'direct_s3_copy' }, successful: successful, failed: failed } const reportFile = './backup_copy_direct_report.json' writeFileSync(reportFile, JSON.stringify(copyReport, null, 2)) console.log(`\n📝 Copy report saved to: ${reportFile}`) if (successful.length === toDelete.length) { console.log('\n🎉 All files successfully copied to backup bucket!') console.log('✅ Safe to proceed with deletion if needed') } else { console.log(`\n⚠️ ${failed.length} files failed to copy`) console.log('💡 Review failed copies before proceeding with any deletion') } console.log(`\n📈 Summary:`) console.log(` 📦 Files copied: ${successful.length}/${toDelete.length}`) console.log(` 💾 Size copied: ${totalSizeGB} GB`) console.log(` 📁 Backup bucket: ${BACKUP_BUCKET}`) console.log(` 🔧 Method: Direct S3 Copy`) } async function delete_cleanup_candidates() { console.log('⚠️ DANGER: This will PERMANENTLY DELETE files from the main bucket!') console.log('🔄 Deleting cleanup candidates from main bucket...') // Safety check - ensure this is intentional console.log('\n🛡️ SAFETY CHECKS:') console.log(' - Make sure you have backed up these files first') console.log(' - This operation cannot be undone') console.log(' - Files will be permanently removed from main bucket') // Check if cleanup_candidates.json exists const cleanupFile = './cleanup_candidates.json' if (!existsSync(cleanupFile)) { console.error('❌ No cleanup_candidates.json file found. Run "prepare_cleanup_zip" first.') process.exit(1) } console.log('📂 Loading cleanup candidates...') // Load cleanup candidates file const cleanupData = JSON.parse(readFileSync(cleanupFile, 'utf-8')) const toDelete = cleanupData.toDelete ?? [] if (toDelete.length === 0) { console.log('✅ No files to delete - cleanup candidates is empty') return } console.log(`📦 Found ${toDelete.length} files to delete from main bucket`) // Calculate total size const totalSize = toDelete.reduce((sum: number, file: any) => sum + (file.size?? 0), 0) const totalSizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(2) console.log(`💾 Total size to delete: ${totalSizeGB} GB`) console.log(`📁 From bucket: ${S3_BUCKET}`) // Initialize S3 client console.log('🔗 Connecting to R2...') const s3 = await initS3() // Delete files in parallel console.log('⚡ Deleting files from main bucket...') const deleteOperations = toDelete.map(async (file: any, index: number) => { try { const deleteCommand = new DeleteObjectCommand({ Bucket: S3_BUCKET, Key: file.key }) await s3.send(deleteCommand) // Log progress every 10 files if ((index + 1) % 10 === 0) { console.log(`📊 Progress: ${index + 1}/${toDelete.length} files deleted`) } return { key: file.key, success: true, error: null } } catch (error: any) { console.error(`❌ Error deleting ${file.key}:`, error.message) return { key: file.key, success: false, error: error.message } } }) // Execute all delete operations in parallel const results = await Promise.all(deleteOperations) // Analyze results const successful = results.filter(r => r.success) const failed = results.filter(r => !r.success) console.log('\n📊 Delete Results:') console.log('================') console.log(`✅ Successfully deleted: ${successful.length} files`) console.log(`❌ Failed to delete: ${failed.length} files`) if (failed.length > 0) { console.log('\n💥 Failed deletions:') failed.forEach((failure, index) => { console.log(`${(index + 1).toString().padStart(2, '0')}. ${failure.key} - ${failure.error}`) }) } // Save delete results const deleteReport = { generated: new Date().toISOString(), summary: { totalFiles: toDelete.length, successfulDeletions: successful.length, failedDeletions: failed.length, totalSizeDeleted: totalSize, totalSizeDeletedGB: parseFloat(totalSizeGB), sourceBucket: S3_BUCKET }, successful: successful, failed: failed } const reportFile = './delete_report.json' writeFileSync(reportFile, JSON.stringify(deleteReport, null, 2)) console.log(`\n📝 Delete report saved to: ${reportFile}`) if (successful.length === toDelete.length) { console.log('\n🎉 All files successfully deleted from main bucket!') console.log('✅ Cleanup operation completed successfully') } else { console.log(`\n⚠️ ${failed.length} files failed to delete`) console.log('💡 Review failed deletions in the report') } console.log(`\n📈 Summary:`) console.log(` 📦 Files deleted: ${successful.length}/${toDelete.length}`) console.log(` 💾 Size deleted: ${totalSizeGB} GB`) console.log(` 📁 Source bucket: ${S3_BUCKET}`) } main()