Files
wish-pool/scripts/clear-storage.js
2025-07-19 02:12:37 +08:00

221 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 心願星河 - 清空 Supabase Storage
*
* ⚠️ 警告:此腳本將永久刪除所有存儲的圖片文件!
*
* 使用方法:
* 1. 確保已安裝依賴npm install
* 2. 設置環境變數或在 .env.local 中配置 Supabase 連接
* 3. 執行腳本node scripts/clear-storage.js
*/
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
// 載入環境變數
require('dotenv').config({ path: '.env.local' });
// Supabase 配置
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
// 檢查必要的環境變數
if (!supabaseUrl || !supabaseServiceKey) {
console.error('❌ 錯誤:缺少必要的環境變數');
console.error('請確保已設置以下環境變數:');
console.error('- NEXT_PUBLIC_SUPABASE_URL');
console.error('- SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_SUPABASE_ANON_KEY');
process.exit(1);
}
// 初始化 Supabase 客戶端
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// 存儲桶名稱
const BUCKETS = ['wish-images', 'wish-thumbnails'];
/**
* 清空指定存儲桶中的所有文件
*/
async function clearBucket(bucketName) {
try {
console.log(`\n🗂️ 正在處理存儲桶:${bucketName}`);
// 列出所有文件
const { data: files, error: listError } = await supabase.storage
.from(bucketName)
.list('', {
limit: 1000,
sortBy: { column: 'created_at', order: 'desc' }
});
if (listError) {
console.error(`❌ 列出 ${bucketName} 文件時出錯:`, listError.message);
return false;
}
if (!files || files.length === 0) {
console.log(`${bucketName} 已經是空的`);
return true;
}
console.log(`📊 找到 ${files.length} 個文件`);
// 獲取所有文件路徑(包括子目錄)
const allFilePaths = [];
for (const file of files) {
if (file.name && file.name !== '.emptyFolderPlaceholder') {
// 如果是目錄,遞歸獲取其中的文件
if (!file.metadata) {
const { data: subFiles, error: subListError } = await supabase.storage
.from(bucketName)
.list(file.name, { limit: 1000 });
if (!subListError && subFiles) {
subFiles.forEach(subFile => {
if (subFile.name && subFile.name !== '.emptyFolderPlaceholder') {
allFilePaths.push(`${file.name}/${subFile.name}`);
}
});
}
} else {
allFilePaths.push(file.name);
}
}
}
if (allFilePaths.length === 0) {
console.log(`${bucketName} 中沒有需要刪除的文件`);
return true;
}
console.log(`🗑️ 準備刪除 ${allFilePaths.length} 個文件...`);
// 批量刪除文件
const batchSize = 50; // Supabase 建議的批量操作大小
let totalDeleted = 0;
let hasErrors = false;
for (let i = 0; i < allFilePaths.length; i += batchSize) {
const batch = allFilePaths.slice(i, i + batchSize);
const { data, error } = await supabase.storage
.from(bucketName)
.remove(batch);
if (error) {
console.error(`❌ 刪除批次 ${Math.floor(i/batchSize) + 1} 時出錯:`, error.message);
hasErrors = true;
} else {
totalDeleted += batch.length;
console.log(`✅ 已刪除批次 ${Math.floor(i/batchSize) + 1}/${Math.ceil(allFilePaths.length/batchSize)} (${batch.length} 個文件)`);
}
// 避免請求過於頻繁
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log(`📊 ${bucketName} 清空完成:刪除了 ${totalDeleted}/${allFilePaths.length} 個文件`);
return !hasErrors;
} catch (error) {
console.error(`❌ 清空 ${bucketName} 時發生未預期錯誤:`, error.message);
return false;
}
}
/**
* 驗證存儲桶是否存在
*/
async function verifyBuckets() {
try {
const { data: buckets, error } = await supabase.storage.listBuckets();
if (error) {
console.error('❌ 無法獲取存儲桶列表:', error.message);
return false;
}
const existingBuckets = buckets.map(bucket => bucket.id);
const missingBuckets = BUCKETS.filter(bucket => !existingBuckets.includes(bucket));
if (missingBuckets.length > 0) {
console.warn('⚠️ 以下存儲桶不存在,將跳過:', missingBuckets.join(', '));
return BUCKETS.filter(bucket => existingBuckets.includes(bucket));
}
return BUCKETS;
} catch (error) {
console.error('❌ 驗證存儲桶時發生錯誤:', error.message);
return false;
}
}
/**
* 主函數
*/
async function main() {
console.log('🚀 開始清空 Supabase Storage...');
console.log('⚠️ 警告:這將永久刪除所有存儲的圖片文件!');
// 驗證存儲桶
const bucketsToProcess = await verifyBuckets();
if (!bucketsToProcess || bucketsToProcess.length === 0) {
console.error('❌ 沒有可處理的存儲桶');
process.exit(1);
}
console.log(`📋 將處理 ${bucketsToProcess.length} 個存儲桶:`, bucketsToProcess.join(', '));
// 給用戶 5 秒鐘考慮時間
console.log('\n⏰ 5 秒後開始刪除... (按 Ctrl+C 取消)');
await new Promise(resolve => setTimeout(resolve, 5000));
let allSuccess = true;
// 清空每個存儲桶
for (const bucket of bucketsToProcess) {
const success = await clearBucket(bucket);
if (!success) {
allSuccess = false;
}
}
// 顯示最終結果
console.log('\n' + '='.repeat(50));
if (allSuccess) {
console.log('✅ 所有存儲桶清空完成!');
} else {
console.log('⚠️ 存儲桶清空完成,但過程中有一些錯誤');
}
console.log('\n📝 建議後續步驟:');
console.log('1. 在 Supabase Dashboard 中確認 Storage 已清空');
console.log('2. 執行 clear-all-data.sql 清空資料庫');
console.log('3. 重新啟動應用程式');
}
// 錯誤處理
process.on('unhandledRejection', (error) => {
console.error('❌ 未處理的錯誤:', error);
process.exit(1);
});
process.on('SIGINT', () => {
console.log('\n❌ 用戶取消操作');
process.exit(0);
});
// 執行主函數
if (require.main === module) {
main().catch(error => {
console.error('❌ 腳本執行失敗:', error);
process.exit(1);
});
}
module.exports = { clearBucket, verifyBuckets };