Storage - تخزين الملفات — Supabase
التخزين (Storage)
Supabase Storage لتخزين الصور والملفات، مع صلاحيات تُدار بسياسات RLS مثل الجداول.
الحاويات (Buckets)
| النوع | الوصول للقراءة | الاستخدام |
|---|---|---|
| عامة | أي شخص يعرف الرابط | صور المنتجات، الصور الشخصية العامة |
| خاصة | روابط موقّعة مؤقتة فقط | فواتير، مستندات، ملفات المستخدم الخاصة |
حدد لكل حاوية الحد الأقصى للحجم و أنواع الملفات المسموحة في إعداداتها.
السياسات
الكتابة تحتاج سياسات حتى في الحاوية العامة. نمط شائع: كل مستخدم يكتب في مجلد باسم معرّفه فقط:
CREATE POLICY "users upload to own folder" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);الرفع من المتصفح
import { supabase } from "./supabaseClient.js";
export async function uploadAvatar(userId, file) {
const ext = file.name.split(".").pop()?.toLowerCase() ?? "png";
const path = `${userId}/avatar.${ext}`;
const { error } = await supabase.storage
.from("avatars")
.upload(path, file, { upsert: true, contentType: file.type, cacheControl: "3600" });
if (error) throw error;
return supabase.storage.from("avatars").getPublicUrl(path).data.publicUrl;
}الرفع من React Native
supabase-js لا يقبل كائناً بشكل { uri, type, name } (ذلك أسلوب FormData). اقرأ الملف كبيانات ثنائية أولاً:
// const response = await fetch(localUri); // يعمل مع روابط file:// في Expo
// const bytes = await response.arrayBuffer();
// await supabase.storage.from("avatars").upload(path, bytes, { contentType: "image/jpeg", upsert: true });
const steps = ["fetch(uri)", "arrayBuffer()", "upload(path, bytes, { contentType })"];
console.log(steps.join(" ← "));(في بعض إصدارات React Native تُقرأ الصورة بـ base64 من expo-file-system ثم تُحوّل لـ ArrayBuffer.)
الروابط
| الدالة | الاستخدام |
|---|---|
| getPublicUrl(path) | حاوية عامة — رابط دائم (لا يحتاج await) |
| createSignedUrl(path, 60) | حاوية خاصة — رابط ينتهي بعد 60 ثانية |
| download(path) | تحميل كـ Blob |
| getPublicUrl(path, { transform: { width: 200 } }) | صورة مصغرة (ميزة تحويل الصور في الخطط المدفوعة) |
نصائح
- لا تستخدم أسماء الملفات الأصلية من المستخدم في المسار؛ ولّد اسماً أو استخدم مسارات ثابتة.
- خزّن المسار في قاعدة البيانات لا الرابط العام (يمكن توليده، ويتغير إن نقلت الحاوية).
- عند حذف السجل من قاعدة البيانات احذف الملف أيضاً (أو بدالة خادم) — وإلا تتراكم ملفات يتيمة.
تمرين
أنشئ حاوية خاصة documents بحد 5MB لملفات PDF فقط، مع سياسة رفع وقراءة لمجلد المستخدم، ودالة تعرض قائمة ملفات المستخدم مع رابط موقّع لكل منها صالح لدقيقتين.
import { supabase } from "./supabaseClient.js";
const AVATARS = "avatars"; // حاوية عامة: حد 2MB، أنواع: image/png, image/jpeg, image/webp
const DOCUMENTS = "documents"; // حاوية خاصة: روابط موقّعة فقط
// ===== من المتصفح: <input type="file"> =====
export async function uploadAvatar(userId, file) {
if (!file.type.startsWith("image/")) throw new Error("صورة فقط");
const path = `${userId}/avatar`; // مسار ثابت بمعرّف المستخدم — لا اسم الملف الأصلي
const { error } = await supabase.storage
.from(AVATARS)
.upload(path, file, { upsert: true, contentType: file.type, cacheControl: "3600" });
if (error) throw error;
const { data } = supabase.storage.from(AVATARS).getPublicUrl(path);
return `${data.publicUrl}?v=${Date.now()}`; // تجاوز التخزين المؤقت بعد الاستبدال
}
// ===== من React Native / Expo: نقرأ الملف كبيانات ثنائية =====
export async function uploadAvatarFromUri(userId, localUri, mimeType = "image/jpeg") {
const response = await fetch(localUri);
const bytes = await response.arrayBuffer();
const { error } = await supabase.storage
.from(AVATARS)
.upload(`${userId}/avatar`, bytes, { contentType: mimeType, upsert: true });
if (error) throw error;
}
// ===== ملفات خاصة: قائمة مع روابط موقّعة مؤقتة =====
export async function listMyDocuments(userId) {
const { data: files, error } = await supabase.storage
.from(DOCUMENTS)
.list(userId, { limit: 100, sortBy: { column: "created_at", order: "desc" } });
if (error) throw error;
const paths = files.map((f) => `${userId}/${f.name}`);
if (paths.length === 0) return [];
const { data: signed, error: signError } = await supabase.storage
.from(DOCUMENTS)
.createSignedUrls(paths, 120); // دقيقتان
if (signError) throw signError;
return signed.map((s, i) => ({ name: files[i].name, url: s.signedUrl }));
}
export async function deleteDocument(userId, name) {
const { error } = await supabase.storage.from(DOCUMENTS).remove([`${userId}/${name}`]);
if (error) throw error;
}اختبر فهمك