📦

Supabase Storage + pg_cron で実装する自動ファイルクリーンアップ

に公開

課題

Supabase Storage に保存された画像ファイルを、一定期間後に自動削除してストレージコストを抑えたい。例えば、完了したジョブのファイルを7日後に削除するような運用です。

解決策

以下の3つを組み合わせることで、完全自動化されたクリーンアップを実装できます。

  1. Edge Function: 古いファイルを削除する処理
  2. pg_cron: 毎日定時実行するスケジューラ
  3. 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 について

https://pornfusion.com/

PornFusion は Next.js App Router, Supabase で構築しています。
生成結果は業界最高峰レベルだと自負しているので、ぜひ遊んでみて欲しいです!

Discussion