تكامل Firebase

Cloud Storage — قواعد الأمان وإدارة الملفات

16 دقيقة الدرس 9 من 13

Cloud Storage — قواعد الأمان وإدارة الملفات

يتيح لك Firebase Cloud Storage تخزين المحتوى الذي ينشئه المستخدمون كالصور ومقاطع الفيديو والمستندات وتقديمها بشكل موسّع. لكن الوصول غير المقيّد إلى التخزين أمر خطير: بدون قواعد مناسبة، يمكن لأي شخص قراءة كل ملف في الدلو أو الكتابة فوقه. في هذا الدرس ستتقن قواعد أمان Firebase Storage، وتتعلم كيفية إدراج محتويات الدلو، وحذف الملفات، والتعامل مع الأخطاء الناتجة عندما تحظر القواعد عملية ما.

فهم قواعد أمان التخزين

تُكتب قواعد أمان التخزين بـلغة قواعد تقطن داخل Firebase console (أو في ملف storage.rules). تُقيَّم القواعد قبل وصول أي عملية SDK إلى الدلو الخاص بك. كل كتلة match تتعامل مع نمط مسار، وكل عبارة allow تمنح عملية محددة (read، write، create، update، delete، أو list).

أهم متغيرَين مدمجَين داخل تعبير القواعد هما:

  • request.auth — رمز Firebase Auth الخاص بالمتصل (null إذا لم يكن مصادقاً)
  • request.auth.uid — سلسلة معرّف المستخدم الفريد
ملاحظة: القواعد ليست مرشحات. إذا رفضت قاعدة list الوصول إلى مسار ما، تفشل عملية الإدراج بأكملها — لا تتجاهل Firebase الملفات المحظورة بصمت. صمّم بنية مساراتك بحيث تقع ملفات كل مستخدم داخل مجلد يحمل اسم UID الخاص به.

تحديد نطاق القراءة والكتابة للمستخدمين المصادق عليهم

النمط الأكثر شيوعاً هو تقييد ملفات المستخدم بمجلد يحمل اسم UID الخاص به. فيما يلي مجموعة قواعد بسيطة لكنها جاهزة للإنتاج:

storage.rules — وصول مجلد خاص لكل مستخدم

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // قراءة عامة للأصول المشتركة (أيقونات التطبيق، الصور الرمزية)
    match /public/{allPaths=**} {
      allow read: if true;
      allow write: if false;
    }

    // كل مستخدم مصادق يملك مجلده الخاص
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }

    // المسؤولون يمكنهم قراءة كل شيء (يتطلب مطالبة مخصصة)
    match /{allPaths=**} {
      allow read: if request.auth != null
                  && request.auth.token.admin == true;
    }
  }
}

النقاط الرئيسية في مجموعة القواعد أعلاه:

  • rules_version = '2' إلزامي لكي يعمل الحرف البدل {allPaths=**} بشكل صحيح.
  • نمط users/{userId}/ يربط الجزء الخاص بالـUID كمتغير، ثم يقارنه الشرط بـrequest.auth.uid.
  • allow read, write هو اختصار يمنح كل العمليات الفرعية (get, list, create, update, delete).

إدراج محتويات الدلو في Flutter

لتعداد الملفات تحت بادئة معينة، استدعِ listAll() (يُعيد كل عنصر دفعةً واحدة) أو list() (يُعيد صفحة مع nextPageToken). استخدم list() للمجلدات التي تحتوي على ملفات كثيرة.

إدراج صور المستخدم المرفوعة

import 'package:firebase_storage/firebase_storage.dart';

Future<List<Reference>> listUserImages(String uid) async {
  final storageRef = FirebaseStorage.instance
      .ref()
      .child('users/$uid/images');

  // listAll() تجلب كل عنصر وبادئة (مجلد فرعي)
  final ListResult result = await storageRef.listAll();

  // result.items   — قائمة مراجع الملفات
  // result.prefixes — قائمة مراجع المجلدات الفرعية
  return result.items;
}

// الاستخدام مع روابط التحميل
Future<void> showUserImages(String uid) async {
  final items = await listUserImages(uid);
  for (final ref in items) {
    final url = await ref.getDownloadURL();
    print('الملف: ${ref.name} — الرابط: $url');
  }
}
نصيحة: تُحمِّل listAll() كل شيء إلى الذاكرة دفعةً واحدة. في دلاء الإنتاج التي تحوي مئات الملفات لكل مستخدم، يُفضَّل استخدام list(maxResults: 20) مع رموز الصفحات للإبقاء على استخدام الذاكرة ضمن حدود يمكن التنبؤ بها.

حذف الملفات

يتطلب حذف ملف وجود Reference تشير إلى المسار الدقيق. تُعيد العملية Future<void> يُحلّ عند إزالة الملف من الدلو.

حذف ملف واحد والتعامل مع الأخطاء

import 'package:firebase_storage/firebase_storage.dart';

Future<void> deleteUserFile({
  required String uid,
  required String fileName,
}) async {
  final ref = FirebaseStorage.instance
      .ref()
      .child('users/$uid/images/$fileName');

  try {
    await ref.delete();
    print('تم الحذف: $fileName');
  } on FirebaseException catch (e) {
    switch (e.code) {
      case 'object-not-found':
        // الملف أُزيل مسبقاً — عامله كنجاح
        print('الملف غير موجود، يُتجاوز: $fileName');
        break;
      case 'unauthorized':
        // رفضت قواعد الأمان عملية الحذف
        print('تم رفض الإذن: ${e.message}');
        rethrow; // دع طبقة الواجهة تُظهر الخطأ
      default:
        print('خطأ في التخزين [${e.code}]: ${e.message}');
        rethrow;
    }
  }
}

التعامل مع أخطاء التخزين بأسلوب سلس

كل استدعاء لـ Storage SDK يمكن أن يُلقي FirebaseException. تحتوي الخاصية e.code على اختصار قصير يصف الفشل. أهم الأكواد هي:

  • object-not-found — المسار غير موجود في الدلو.
  • unauthorized — تعبير قواعد الأمان قُيِّم بـfalse.
  • canceled — تم إلغاء مهمة الرفع أو التنزيل صراحةً.
  • retry-limit-exceeded — عدد كبير جداً من عمليات إعادة المحاولة المتتالية؛ تحقق من الاتصال.
  • quota-exceeded — استنفدت حصة التخزين أو التنزيل في الخطة المجانية.
تحذير: لا تبتلع كل FirebaseExceptions بصمت. سجِّل e.code وe.message كحد أدنى، وميِّز بين "غير موجود" (يمكن تجاهله بأمان) و"غير مصرح به" (يجب إظهاره للمستخدم أو نظام تتبع الأخطاء لديك).

دمج القواعد والإدراج والحذف في مستودع

تلتف البنية النظيفة لـ Flutter على جميع تفاعلات Storage في فئة مستودع. هذا يُبقي الودجات خالية من تفاصيل SDK ويجعل اختبار الوحدات سهلاً من خلال السماح بمحاكاة المستودع.

StorageRepository مع الإدراج والحذف ومعالجة الأخطاء

class StorageRepository {
  final FirebaseStorage _storage;
  StorageRepository({FirebaseStorage? storage})
      : _storage = storage ?? FirebaseStorage.instance;

  /// تُعيد روابط التحميل لكل الملفات في مجلد المستخدم.
  Future<List<String>> getUserFileUrls(String uid) async {
    try {
      final result = await _storage
          .ref('users/$uid/images')
          .listAll();
      return await Future.wait(
        result.items.map((ref) => ref.getDownloadURL()),
      );
    } on FirebaseException catch (e) {
      if (e.code == 'unauthorized') {
        throw Exception('الوصول مرفوض. الرجاء تسجيل الدخول.');
      }
      rethrow;
    }
  }

  /// تحذف ملفاً بمساره في التخزين.
  Future<void> deleteFile(String path) async {
    try {
      await _storage.ref(path).delete();
    } on FirebaseException catch (e) {
      if (e.code == 'object-not-found') return; // عملية تكرارية آمنة
      rethrow;
    }
  }
}

ملخص

في هذا الدرس تعلمت كيفية كتابة قواعد أمان Firebase Storage التي تُقيِّد الوصول للمستخدمين المصادق عليهم ومسارات محددة مبنية على الـUID. استخدمت listAll() لتعداد محتويات الدلو، واستدعيت delete() لإزالة الملفات، وتعاملت مع أكواد خطأ FirebaseException الأكثر شيوعاً بشكل صحيح. في الدرس القادم ستدمج عمليات رفع التخزين وحذفه مباشرةً في واجهة Flutter مع مؤشرات تقدم في الوقت الحقيقي.