Supabase storage でのクォータ機能

Supabase storage でクォータ機能を実装したい場合の tips です。

storage.objects テーブルを見ると、metadata に size というメンバーが定義されていますが、取り出しが面倒なので、file_uploads というテーブルを作ります。中身は以下のようになります。

CREATE TABLE file_uploads (
  id uuid default getRandomUuid() primary key,
  fname text,
  uid uuid,
  fsize int,
  uploaded_at datetime default now()
);
CREATE INDEX file_uploads_qgroup_index file_uploads(uid);

ファイルアップロードプログラムに file_uploads テーブルの更新を追加します。

import { NextRequest, NextResponse } from "next/server"
import { NextApiResponse } from "next"
import { getServerSession } from "next-auth"
import { createClient } from "@supabase/supabase-js"

export async POST(request: NextRequest): Promise<NextApiResponse> {
  // JWT を取得
  const session = getServerSession(request, response)
  if (!session || !session.user) {
    return NextResponse.status(401).json({error: {message: "No JWT"}})
  }

  // ユーザ ID を取得
  const uid = session.user.id

  // ファイルのアップロード本体
  const file = request.body.file
  const fsize = file.size

  /* (1) */

  const supabase = createClient("url", "api-key")
  const { sdata: uploadData, serror: uploadError } = supabase.storage.from("bucket").upload(file.name, file)
  if (uploadError) {
    return NextResponse.status(500).json({error: {message: "Can't upload file"}})
  }

  // file_uploads テーブルにクォータのための情報を保存
  const { error: dbError } = supabase.from("file_uploads").insert([
    {fname: file.name, fsize: fsize, uid: uid}
  ])
  if (dbError) {
    return NextResponse.status(500).json({error: {message: "DB error"}})
  }
  return NextResponse.json({data: true})
}

基本的な線はこれでよいのですが、supabase の制限から、view を1つ作成します。

CREATE VIEW file_upload_usage AS
SELECT uid, SUM(fsize) AS total_size FROM file_uploads GROUP BY uid;

ソフトリミット的な使い方をする場合は、バッチで回します。Vercel を使う場合は vercel.cron に起動を仕掛けておくことが出来ます。

import { createClient } from "@supabae/supabase-js"
import { NextRequest, NextResponse } from "next/server"
import { NextApiResponse } from "next"

export default async function GET(req: NextRequst): Promise<NextApiResponse> {
  /* 本来は CRON_SECRET を比較するべきだが省略 */
  const { data, error } = supabase.from("file_upload_usage").select("*").gte("total_size", TOTAL_SIZE)
  if (error) {
    return NextResponse.status(500).json({error: {message: "Failed to get upload usage"}})
  }
  return NextResponse.json({data: data})
}

ハードリミットを設定したい場合は view の total_size と、ファイルの fsize の和をスレッショルドと比較するコードを (1) に挿入することになります。

const { data, error } = supabase.from("file_upload_usage").select("*").eq("uid", uid).single()
if (error) {
  return res.status(500).json({error:{message: "failed get file usage"}})
}
if (data.total_size + fsize > THRESHOLD) {
  return res.status(400).json({error: {message: "quota exceeded"}})
}

投稿者について
みのしす

小さいときは科学者になろうとしたのに、その時にたまたま身に着けたプログラミングで未だに飯を食っているしがないおじさんです。(年齢的にはもうすぐおじいさん)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です