Cloud Storage: رفع الصور والملفات في Firebase
Firestore ممتازة للنصوص والأرقام، لكن أين تذهب صورة المستخدم الشخصية، أو ملف PDF، أو مقطع صوتي؟ المستند في Firestore له حد أقصى صغير للحجم، وليس مكاناً للملفات. لهذا توجد خدمة Cloud Storage: مخزن ملفات سحابي تكلّمه من تطبيقك مباشرة، وتحميه بقواعد أمان مثل Firestore. في هذا الدرس سترفع صورة، وتحصل على رابطها، وتمنع الآخرين من العبث بها.
تخيّل الخدمتين مثل مكتبة: Firestore هي بطاقات الفهرس الصغيرة (اسم الكتاب ومكانه)، و Cloud Storage هي الرفوف التي تحمل الكتب نفسها. البطاقة تدلّك على الرف.
التجهيز
Section titled “التجهيز”فعّل الخدمة من Console (قسم Storage ثم Get started)، ثم جهّزها في ملف firebase.js الذي أنشأته في الدرس الثاني:
import { getStorage } from 'firebase/storage';
export const storage = getStorage(app);الخدمة تعرف مكان ملفاتك من الحقل storageBucket الموجود في إعدادات مشروعك. والحاوية (Bucket) هي المخزن الكبير الذي يضم كل ملفات المشروع.
المرجع: عنوان الملف
Section titled “المرجع: عنوان الملف”كل ملف له مرجع (Reference) يحدد مساره داخل الحاوية، مثل مسار ملف على جهازك:
import { ref } from 'firebase/storage';
const uid = auth.currentUser.uid;const avatarRef = ref(storage, `avatars/${uid}/profile.png`);
console.log(avatarRef.name);console.log(avatarRef.fullPath);profile.pngavatars/Xk2pQ7.../profile.pngإنشاء المرجع لا يرفع شيئاً ولا يتصل بالخادم. هو عنوان فقط. ووضع معرّف المستخدم uid في المسار عادة مهمة: سنبني عليها قواعد الأمان بعد قليل.
رفع ملف
Section titled “رفع ملف”في صفحة الويب يختار المستخدم الملف من حقل input نوعه file، ثم ترفعه بالدالة uploadBytes:
import { uploadBytes } from 'firebase/storage';
const file = document.querySelector('#photo').files[0];
const result = await uploadBytes(avatarRef, file);console.log(result.metadata.size, result.metadata.contentType);3000 image/pngرفعنا صورة حجمها 3000 بايت، وعادت البيانات الوصفية (Metadata) للملف: حجمه ونوعه. المتصفح يرسل نوع الملف تلقائياً مع كائن File. وإذا رفعت بيانات خام فحدد النوع بنفسك في معامل ثالث: uploadBytes(avatarRef, data, { contentType: 'image/png' }).
والرفع إلى مسار فيه ملف قديم يستبدله دون سؤال.
رابط التنزيل
Section titled “رابط التنزيل”لعرض الصورة في وسم img تحتاج رابطاً. الدالة getDownloadURL تعطيك إياه:
import { getDownloadURL } from 'firebase/storage';
const url = await getDownloadURL(avatarRef);console.log(url);https://firebasestorage.googleapis.com/v0/b/school-app-4f2a1.firebasestorage.app/o/avatars%2FXk2pQ7...%2Fprofile.png?alt=media&token=5b1c...الرابط طويل وفي آخره رمز (token) سري. جرّبنا طلبه بـ fetch فعاد الرد 200 ونوعه image/png وحجمه 3000 بايت: الصورة نفسها.
والنمط المعتاد: ارفع الملف إلى Storage، ثم احفظ رابطه في Firestore مع بيانات صاحبه:
import { doc, setDoc } from 'firebase/firestore';
await setDoc(doc(db, 'users', uid), { name: 'سارة', photoURL: url });هكذا تعرض أي صفحة صورة سارة بقراءة مستندها فقط. هذه بطاقة الفهرس التي تدل على الرف.
متابعة تقدم الرفع
Section titled “متابعة تقدم الرفع”الملف الكبير يأخذ وقتاً، والمستخدم يريد شريط تقدم. استخدم uploadBytesResumable بدل uploadBytes:
import { uploadBytesResumable } from 'firebase/storage';
const task = uploadBytesResumable(ref(storage, `avatars/${uid}/big.png`), file);
task.on('state_changed', (snap) => { const percent = Math.round((snap.bytesTransferred / snap.totalBytes) * 100); console.log(percent + '%'); }, (error) => console.log('فشل الرفع:', error.code), () => console.log('اكتمل الرفع'));0%43%100%اكتمل الرفعهذا ناتج رفع ملف حجمه 600 كيلوبايت في تجربتنا، وعدد التحديثات يختلف حسب حجم الملف وسرعة الاتصال. الدالة الأولى تُستدعى مع كل تقدم، والثانية عند الخطأ، والثالثة عند النجاح. والمهمة task تقبل pause() و resume() و cancel().
عرض الملفات وحذفها
Section titled “عرض الملفات وحذفها”import { listAll, deleteObject } from 'firebase/storage';
const list = await listAll(ref(storage, `avatars/${uid}`));list.items.forEach((item) => console.log(item.name));
await deleteObject(avatarRef);big.pngprofile.pnglistAll تعيد ملفات المجلد في items والمجلدات الفرعية في prefixes. وبعد الحذف جرّبنا طلب رابط الصورة فعاد الخطأ storage/object-not-found، والرابط القديم نفسه صار يرد بالرمز 404. فإن حذفت ملفاً، احذف رابطه المحفوظ في Firestore أيضاً.
قواعد الأمان
Section titled “قواعد الأمان”كما في Firestore: التطبيق يكلّم المخزن مباشرة، والقواعد هي الحارس. تُكتب في ملف storage.rules بلغة شبيهة جداً:
rules_version = '2';service firebase.storage { match /b/{bucket}/o {
match /avatars/{userId}/{fileName} { allow read: if true; allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 2 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); allow delete: if request.auth != null && request.auth.uid == userId; }
match /private/{userId}/{allPaths=**} { allow read, write: if request.auth != null && request.auth.uid == userId; } }}request.resourceهنا هو الملف القادم:sizeحجمه بالبايت، وcontentTypeنوعه.- الصور الشخصية يقرؤها الجميع، ولا يكتب في مجلد إلا صاحبه، بصورة أصغر من 2 ميجابايت.
{allPaths=**}يطابق أي ملف مهما كان عمق مجلداته.- شرط الحذف مكتوب وحده، لأن الحذف لا يحمل ملفاً قادماً نفحص حجمه.
جرّبنا القواعد على المحاكي بثلاثة أشخاص:
| المحاولة | النتيجة |
|---|---|
| سارة ترفع صورتها (3000 بايت) | نجحت |
| سارة ترفع صورة حجمها 3 ميجابايت | storage/unauthorized |
| سارة ترفع ملف PDF في مجلد الصور | storage/unauthorized |
| سارة ترفع في مسار لم تذكره القواعد | storage/unauthorized |
| زائر يطلب رابط صورة سارة | نجحت |
| زائر يرفع صورة | storage/unauthorized |
| عمر يكتب فوق صورة سارة | storage/unauthorized |
| عمر يحذف صورة سارة | storage/unauthorized |
| عمر يقرأ ملف سارة الخاص | storage/unauthorized |
| سارة تحذف صورتها | نجحت |
لاحظ الصف الرابع: القاعدة الذهبية نفسها. ما لم يُسمح به صراحة فهو مرفوض.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل المحاكي بالأمر
firebase emulators:start، واربط تطبيقك به:connectStorageEmulator(storage, '127.0.0.1', 9199). - أنشئ صفحة فيها حقل اختيار ملف وزر، وارفع الصورة المختارة إلى
avatars/داخل مجلد باسم معرّف المستخدم. - اعرض نسبة التقدم أثناء الرفع، ثم اعرض الصورة في وسم
imgمن رابط التنزيل. - تحدٍّ: أضف مساراً
documentsلا يقبل إلا ملفات PDF أصغر من 5 ميجابايت، ولا يقرؤها إلا صاحبها. (تلميح: نوع ملف PDF هوapplication/pdf.)
أخطاء شائعة
Section titled “أخطاء شائعة”- تخزين الصورة داخل Firestore: تحويل الصورة إلى نص طويل وحفظها في مستند يصطدم بحد حجم المستند ويبطئ كل قراءة. الملف في Storage، ورابطه فقط في Firestore.
- قواعد بلا حد للحجم أو النوع: أي مستخدم مسجل يرفع ملفات ضخمة فتدفع أنت ثمن تخزينها. حدد
sizeوcontentTypeدائماً. - اسم ثابت لكل المستخدمين: الرفع إلى
avatars/profile.pngيجعل كل مستخدم يمسح صورة من قبله. ضعuidفي المسار. - نسيان الرابط القديم: حذف الملف لا يحذف رابطه من Firestore، فتظهر صورة مكسورة في التطبيق.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
أين نحفظ صورة المستخدم الشخصية، وأين نحفظ رابطها؟
Cloud Storage مخصصة للملفات، و Firestore للبيانات الصغيرة مثل الرابط والاسم.
سؤال
في قواعد Storage، ماذا يمثل request.resource.size؟
request.resource هو الملف الذي يحاول المستخدم رفعه، و size حجمه بالبايت.
الخلاصة
Section titled “الخلاصة”- Cloud Storage للملفات، والمرجع
ref(storage, 'مسار')عنوان الملف داخل الحاوية. uploadBytesترفع، وuploadBytesResumableترفع مع متابعة التقدم.getDownloadURLتعطي رابطاً يُحفظ في Firestore ويُعرض في التطبيق.listAllتعرض ملفات مجلد، وdeleteObjectتحذف ملفاً.- قواعد
storage.rulesتربط المجلد بصاحبه وتحدد حجم الملف ونوعه.