eee0885811
- Remove recursive directory size calculations (single Seafile API call per list) - Remove 'Used in this location' usage display - Fix move using v2 per-type endpoints instead of broken batch endpoint - Send entry type from frontend for correct move routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.6 KiB
JavaScript
114 lines
3.6 KiB
JavaScript
/**
|
|
* cleanup-orphaned-storage.mjs
|
|
*
|
|
* Removes files in the `submissions` storage bucket that have no matching
|
|
* row in `submission_files`. These are orphaned uploads from before the
|
|
* silent-failure bug was fixed.
|
|
*
|
|
* Usage:
|
|
* SUPABASE_SERVICE_ROLE_KEY=<your-key> node scripts/cleanup-orphaned-storage.mjs
|
|
*
|
|
* The service role key is in Supabase dashboard → Project Settings → API.
|
|
* Run once, then you can delete this script if you like.
|
|
*/
|
|
|
|
import { createClient } from '@supabase/supabase-js';
|
|
|
|
const SUPABASE_URL = 'https://fqflxxqvennhvoeywrdw.supabase.co';
|
|
const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
|
|
if (!SERVICE_ROLE_KEY) {
|
|
console.error('Error: SUPABASE_SERVICE_ROLE_KEY env var is required.');
|
|
console.error(' SUPABASE_SERVICE_ROLE_KEY=<key> node scripts/cleanup-orphaned-storage.mjs');
|
|
process.exit(1);
|
|
}
|
|
|
|
const supabase = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
|
|
|
|
async function listAllStorageFiles(bucket) {
|
|
// Storage structure: submissions/{taskId}/{timestamp}_{filename}
|
|
// List the root to get task-ID "folders", then list each folder for files.
|
|
const allPaths = [];
|
|
|
|
const { data: folders, error: folderError } = await supabase.storage
|
|
.from(bucket)
|
|
.list('', { limit: 1000, sortBy: { column: 'name', order: 'asc' } });
|
|
|
|
if (folderError) throw new Error(`Failed to list root: ${folderError.message}`);
|
|
if (!folders?.length) return allPaths;
|
|
|
|
for (const folder of folders) {
|
|
// Supabase returns folders as items with `id: null`
|
|
if (folder.id !== null) {
|
|
// It's a top-level file (rare, skip)
|
|
allPaths.push(folder.name);
|
|
continue;
|
|
}
|
|
|
|
const { data: files, error: fileError } = await supabase.storage
|
|
.from(bucket)
|
|
.list(folder.name, { limit: 1000, sortBy: { column: 'name', order: 'asc' } });
|
|
|
|
if (fileError) {
|
|
console.warn(` Warning: failed to list ${folder.name}/: ${fileError.message}`);
|
|
continue;
|
|
}
|
|
|
|
for (const file of files ?? []) {
|
|
if (file.id !== null) {
|
|
allPaths.push(`${folder.name}/${file.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return allPaths;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Fetching all submission_files records from database...');
|
|
const { data: dbFiles, error: dbError } = await supabase
|
|
.from('submission_files')
|
|
.select('storage_path')
|
|
.not('storage_path', 'is', null);
|
|
|
|
if (dbError) throw new Error(`DB query failed: ${dbError.message}`);
|
|
|
|
const knownPaths = new Set((dbFiles ?? []).map(r => r.storage_path));
|
|
console.log(` ${knownPaths.size} file records found in database.`);
|
|
|
|
console.log('\nListing all files in submissions storage bucket...');
|
|
const storagePaths = await listAllStorageFiles('submissions');
|
|
console.log(` ${storagePaths.length} files found in storage.`);
|
|
|
|
const orphans = storagePaths.filter(p => !knownPaths.has(p));
|
|
console.log(`\n${orphans.length} orphaned file(s) found (in storage but not in DB).`);
|
|
|
|
if (orphans.length === 0) {
|
|
console.log('Nothing to clean up.');
|
|
return;
|
|
}
|
|
|
|
console.log('\nOrphaned files:');
|
|
orphans.forEach(p => console.log(` ${p}`));
|
|
|
|
// Delete in batches of 100 (Supabase limit)
|
|
const BATCH = 100;
|
|
let deleted = 0;
|
|
for (let i = 0; i < orphans.length; i += BATCH) {
|
|
const batch = orphans.slice(i, i + BATCH);
|
|
const { error: delError } = await supabase.storage.from('submissions').remove(batch);
|
|
if (delError) {
|
|
console.error(` Error deleting batch: ${delError.message}`);
|
|
} else {
|
|
deleted += batch.length;
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. ${deleted}/${orphans.length} orphaned file(s) deleted.`);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Fatal error:', err.message);
|
|
process.exit(1);
|
|
});
|