📦
Supabase Storage + pg_cron で実装する自動ファイルクリーンアップ
課題
Supabase Storage に保存された画像ファイルを、一定期間後に自動削除してストレージコストを抑えたい。例えば、完了したジョブのファイルを7日後に削除するような運用です。
解決策
以下の3つを組み合わせることで、完全自動化されたクリーンアップを実装できます。
- Edge Function: 古いファイルを削除する処理
- pg_cron: 毎日定時実行するスケジューラ
- Soft Delete: データベースに削除日時を記録
実装
1. データベース設計
まず、削除日時を記録するカラムとインデックスを追加します。
-- テーブルに削除日時カラムを追加
ALTER TABLE files ADD COLUMN storage_deleted_at timestamptz;
-- クリーンアップ対象を高速に検索するインデックス
CREATE INDEX idx_files_cleanup
ON files(created_at)
WHERE storage_deleted_at IS NULL;
2. Edge Function
古いファイルを取得して削除する関数を作成します。
// supabase/functions/cleanup-storage/index.ts
import { createClient } from '@supabase/supabase-js'
Deno.serve(async () => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// 7日以上前のレコードを取得
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - 7)
const { data: records } = await supabase
.from('files')
.select('id, storage_path')
.lt('created_at', cutoffDate.toISOString())
.is('storage_deleted_at', null)
let deletedCount = 0
for (const record of records ?? []) {
// ストレージからファイル削除
await supabase.storage
.from('my-bucket')
.remove([record.storage_path])
// 削除フラグを記録
await supabase
.from('files')
.update({ storage_deleted_at: new Date().toISOString() })
.eq('id', record.id)
deletedCount++
}
return new Response(
JSON.stringify({
success: true,
deletedCount
}),
{ headers: { 'Content-Type': 'application/json' } }
)
})
3. pg_cron設定
毎日深夜0時に自動実行するようスケジュール設定します。
-- pg_cron拡張を有効化
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- 毎日0時に実行
SELECT cron.schedule(
'cleanup-storage',
'0 0 * * *',
$$
SELECT net.http_post(
url := 'https://your-project.supabase.co/functions/v1/cleanup-storage',
headers := jsonb_build_object(
'Authorization',
'Bearer YOUR_SERVICE_ROLE_KEY'
)
);
$$
);
ポイント
Admin Client(Service Role)を使う理由
Edge FunctionではAdmin Client(Service Role Key)を使用します。これにより、RLSポリシーをバイパスして全ユーザーのファイルを削除できます。
Soft Delete vs Hard Delete
storage_deleted_atにタイムスタンプを記録することで:
- データベース上のメタデータは保持
- 監査ログとして活用可能
- 必要に応じて完全削除を後から実行
複数バケット対応
複数のバケットを扱う場合は、バケット名もデータベースに保存します。
await supabase.storage
.from(record.bucket_name) // 動的にバケット指定
.remove([record.storage_path])
まとめ
Supabase の標準機能だけで、ファイルの自動クリーンアップを実装できました。外部サービス不要で、設定も最小限です。できるだけ外部変数は増やしたくないものであります。
PornFusion について
PornFusion は Next.js App Router, Supabase で構築しています。
生成結果は業界最高峰レベルだと自負しているので、ぜひ遊んでみて欲しいです!
Discussion