ข้ามไปยังเนื้อหา

Storage: Buckets and Policies

Supabase Storage คือ object storage แบบ S3-compatible จัดเป็น bucket และ access ของทุก object ถูกบังคับด้วย Row Level Security policy ธรรมดาบน table Postgres — mechanism เดียวกับที่คุณรู้จักแล้วจาก module RLS ไม่ใช่ระบบสิทธิ์แยกต่างหากอีกชุด

bucket คือ container ระดับบนสุดสำหรับไฟล์ คล้าย S3 bucket เช่นคุณอาจมี bucket avatars สำหรับรูปโปรไฟล์ และ bucket public-assets สำหรับรูป marketing bucket หนึ่งตั้งเป็น public ได้ หมายความว่าใครก็ตามที่มี URL ของ object อ่านได้เลยโดยไม่ต้อง authenticate หรือตั้งเป็น private หมายความว่า access ถูกเช็คทุก request

การ upload, download และอ่านไฟล์จาก supabase-js ทำงานผ่านชื่อ bucket ทั้งหมด

// Upload a file into a path inside the bucket
const { data, error } = await supabase.storage
.from('avatars')
.upload('user123/photo.png', file);
// Download the raw file bytes
const { data: fileData } = await supabase.storage
.from('avatars')
.download('user123/photo.png');
// Get a public URL — only meaningful for a public bucket
const { data: publicUrlData } = supabase.storage
.from('avatars')
.getPublicUrl('user123/photo.png');
// Get a time-limited signed URL — for a private bucket
const { data: signedUrlData } = await supabase.storage
.from('avatars')
.createSignedUrl('user123/photo.png', 60);

getPublicUrl แค่สร้าง URL string ขึ้นมาเฉย ๆ ไม่ได้เช็คอะไรเลย ดังนั้นเรียกกับ object ใน private bucket ก็ยังได้ URL กลับมา แต่ URL นั้นจะถูก reject อยู่ดี createSignedUrl คือตัวที่ต้องใช้จริงสำหรับ private content โดยจะสร้าง URL ที่ valid ตามจำนวนวินาทีที่คุณส่งเข้าไป (60 ในตัวอย่างข้างบน) โดยฝัง signed token ที่ให้ access ชั่วคราวกับ object ตัวนั้นตัวเดียว

นี่คือข้อเท็จจริงที่ควรจำไว้ Storage ไม่มีภาษาสิทธิ์เฉพาะของตัวเองเลย metadata ของทุก object ที่ upload ไป — bucket, path, owner, content type — ถูกเก็บเป็น row จริงใน table Postgres ชื่อ storage.objects access control ก็คือแค่ Row Level Security policy บน table นั้น เขียนด้วย syntax using กับ with check เดียวกันเป๊ะกับที่คุณใช้กับ table อื่นในคอร์สนี้

pattern ที่พบบ่อยคือให้ user จัดการได้แค่ไฟล์ใต้ folder prefix ของตัวเอง โดย key ด้วย auth.uid() ของเขา

create policy "Users can upload their own avatar"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);
create policy "Users can read their own avatar"
on storage.objects
for select
to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);

ด้วย policy แบบนี้ ไฟล์ที่ upload ไปที่ avatars/user123/photo.png จะเขียนได้และอ่านได้เฉพาะ user ที่ id เป็น user123 เท่านั้น — เพราะ name (path เต็มของ object) ถูกเช็คกับ auth.uid() ทุก request เหมือนกับที่ RLS บน table อื่นกรอง row ตาม user ที่ request เข้ามาเป๊ะ ๆ

flowchart LR
  client["Client upload/download request"] --> api["Storage API"]
  api --> objects[("storage.objects metadata row: bucket_id, name, owner")]
  objects --> rls{"RLS policy: using / with check against auth.uid()"}
  rls -->|allowed| data["Underlying object data returned or written"]
  rls -->|denied| reject["403 rejected"]
ทุก request ของ Storage ถูกกรองด้วย RLS บน storage.objects mechanism เดียวกับ table RLS
bucket แบบ public กับ private ใน Storage ต่างกันยังไง
สิ่งที่บังคับจริง ๆ ว่าใคร upload, read หรือ delete object ใน Storage ได้คืออะไร
signed URL มีไว้ทำอะไร
policy ที่จำกัดให้ user จัดการได้แค่ folder avatar ของตัวเอง มักเทียบค่าอะไรกับ auth.uid()