221 lines
6.5 KiB
JavaScript
221 lines
6.5 KiB
JavaScript
#!/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 };
|