تخطَّ إلى المحتوى

Cloud Storage: رفع الصور والملفات في Firebase

الدرس 8 من 9 · Firebase

Firestore ممتازة للنصوص والأرقام، لكن أين تذهب صورة المستخدم الشخصية، أو ملف PDF، أو مقطع صوتي؟ المستند في Firestore له حد أقصى صغير للحجم، وليس مكاناً للملفات. لهذا توجد خدمة Cloud Storage: مخزن ملفات سحابي تكلّمه من تطبيقك مباشرة، وتحميه بقواعد أمان مثل Firestore. في هذا الدرس سترفع صورة، وتحصل على رابطها، وتمنع الآخرين من العبث بها.

تخيّل الخدمتين مثل مكتبة: Firestore هي بطاقات الفهرس الصغيرة (اسم الكتاب ومكانه)، و Cloud Storage هي الرفوف التي تحمل الكتب نفسها. البطاقة تدلّك على الرف.

فعّل الخدمة من Console (قسم Storage ثم Get started)، ثم جهّزها في ملف firebase.js الذي أنشأته في الدرس الثاني:

firebase.js
import { getStorage } from 'firebase/storage';
export const storage = getStorage(app);

الخدمة تعرف مكان ملفاتك من الحقل storageBucket الموجود في إعدادات مشروعك. والحاوية (Bucket) هي المخزن الكبير الذي يضم كل ملفات المشروع.

كل ملف له مرجع (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.png
avatars/Xk2pQ7.../profile.png

إنشاء المرجع لا يرفع شيئاً ولا يتصل بالخادم. هو عنوان فقط. ووضع معرّف المستخدم uid في المسار عادة مهمة: سنبني عليها قواعد الأمان بعد قليل.

في صفحة الويب يختار المستخدم الملف من حقل 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' }).

والرفع إلى مسار فيه ملف قديم يستبدله دون سؤال.

لعرض الصورة في وسم 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 });

هكذا تعرض أي صفحة صورة سارة بقراءة مستندها فقط. هذه بطاقة الفهرس التي تدل على الرف.

الملف الكبير يأخذ وقتاً، والمستخدم يريد شريط تقدم. استخدم 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().

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.png
profile.png

listAll تعيد ملفات المجلد في items والمجلدات الفرعية في prefixes. وبعد الحذف جرّبنا طلب رابط الصورة فعاد الخطأ storage/object-not-found، والرابط القديم نفسه صار يرد بالرمز 404. فإن حذفت ملفاً، احذف رابطه المحفوظ في Firestore أيضاً.

كما في Firestore: التطبيق يكلّم المخزن مباشرة، والقواعد هي الحارس. تُكتب في ملف storage.rules بلغة شبيهة جداً:

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
سارة تحذف صورتها نجحت

لاحظ الصف الرابع: القاعدة الذهبية نفسها. ما لم يُسمح به صراحة فهو مرفوض.

  1. شغّل المحاكي بالأمر firebase emulators:start، واربط تطبيقك به: connectStorageEmulator(storage, '127.0.0.1', 9199).
  2. أنشئ صفحة فيها حقل اختيار ملف وزر، وارفع الصورة المختارة إلى avatars/ داخل مجلد باسم معرّف المستخدم.
  3. اعرض نسبة التقدم أثناء الرفع، ثم اعرض الصورة في وسم img من رابط التنزيل.
  4. تحدٍّ: أضف مساراً documents لا يقبل إلا ملفات PDF أصغر من 5 ميجابايت، ولا يقرؤها إلا صاحبها. (تلميح: نوع ملف PDF هو application/pdf.)
  • تخزين الصورة داخل Firestore: تحويل الصورة إلى نص طويل وحفظها في مستند يصطدم بحد حجم المستند ويبطئ كل قراءة. الملف في Storage، ورابطه فقط في Firestore.
  • قواعد بلا حد للحجم أو النوع: أي مستخدم مسجل يرفع ملفات ضخمة فتدفع أنت ثمن تخزينها. حدد size و contentType دائماً.
  • اسم ثابت لكل المستخدمين: الرفع إلى avatars/profile.png يجعل كل مستخدم يمسح صورة من قبله. ضع uid في المسار.
  • نسيان الرابط القديم: حذف الملف لا يحذف رابطه من Firestore، فتظهر صورة مكسورة في التطبيق.

سؤال

أين نحفظ صورة المستخدم الشخصية، وأين نحفظ رابطها؟

سؤال

في قواعد Storage، ماذا يمثل request.resource.size؟

  • Cloud Storage للملفات، والمرجع ref(storage, 'مسار') عنوان الملف داخل الحاوية.
  • uploadBytes ترفع، و uploadBytesResumable ترفع مع متابعة التقدم.
  • getDownloadURL تعطي رابطاً يُحفظ في Firestore ويُعرض في التطبيق.
  • listAll تعرض ملفات مجلد، و deleteObject تحذف ملفاً.
  • قواعد storage.rules تربط المجلد بصاحبه وتحدد حجم الملف ونوعه.