# استراتيجية التخزين — Storage Strategy (Cloudflare R2 + Spatie Media Library)

## نظرة عامة

يتم تخزين جميع ملفات الصور على **Cloudflare R2** عبر **Spatie Media Library**.
R2 هو تخزين كائني (Object Storage) متوافق مع S3 API بدون رسوم egress (خروج البيانات).

**ملاحظة:** دعم الفيديو مؤجل للإصدار v2.

---

## تكوين R2 (Laravel Filesystem)

```php
// config/filesystems.php
'disks' => [
    'r2' => [
        'driver' => 's3',
        'key' => env('R2_ACCESS_KEY_ID'),
        'secret' => env('R2_SECRET_ACCESS_KEY'),
        'region' => 'auto',
        'bucket' => env('R2_BUCKET'),
        'url' => env('R2_URL'),
        'endpoint' => env('R2_ENDPOINT'),
        'use_path_style_endpoint' => true,
        'throw' => false,
    ],
],
```

---

## هيكل الـ Buckets

يُستخدم bucket واحد مع مسارات منظمة:

```
r2://bilgiagaci-prod/
├── students/
│   ├── {student_id}/
│   │   ├── profile/          ← صورة الملف الشخصي للطالب
│   │   └── photos/           ← صور يومية
│   │       └── 2026/
│   │           └── 06/
│   │               └── {uuid}.webp
├── activities/
│   ├── {activity_id}/
│   │   └── images/
├── class/
│   ├── {class_id}/
│   │   ├── photos/           ← صور الصف الكامل
│   │   └── group/            ← صور جماعية
├── announcements/            ← مرفقات الإعلانات
│   └── {announcement_id}/
├── profiles/                 ← صور الملف الشخصي للمستخدمين
│   └── {user_id}/
│       └── avatar.webp
└── temp/                     ← ملفات مؤقتة (تنضيف تلقائي)
    └── uploads/
```

---

## Spatie Media Library Collections

| Collection | النموذج (Model) | الوصف | Size Limit |
|------------|-----------------|-------|------------|
| `profile_photo` | Student | صورة الملف الشخصي للطالب | 2 MB |
| `daily_photos` | Student | صور يومية | 10 MB per file |
| `activity_images` | Activity | صور الأنشطة | 10 MB per file |
| `class_photos` | Classes | صور الصف الكامل | 20 MB per file |
| `announcement_attachments` | Announcement | مرفقات الإعلانات | 10 MB per file |
| `user_avatar` | User | صورة الملف الشخصي للمستخدم | 2 MB |

---

## آلية الرفع: Presigned URLs (Direct-to-R2)

**الملفات لا تمر عبر سيرفر Laravel مطلقًا** — الرفع مباشر من التطبيق إلى R2.

### الخطوة ① — طلب Presigned URL

يطلب التطبيق من Laravel رابط رفع مؤقت (صالح 5 دقائق):

```
Mobile App → POST /api/media/request-upload
  { mime_type, size, collection, original_name }

Laravel:
  ├─ التحقق من mime type المسموح
  ├─ التحقق من الحجم
  ├─ توليد file_key فريد: uploads/temp/{uuid}.{ext}
  ├─ إنشاء presigned PUT URL عبر AWS SDK
  └─ رد: { upload_id, upload_url, file_key, expires_in }
```

### الخطوة ② — الرفع المباشر لـ R2

```
Mobile App → HTTP PUT (مع binary body) → R2 مباشرة
```

لا يمر الطلب عبر Laravel إطلاقًا.

### الخطوة ③ — تسجيل المرجع

```
Mobile App → POST /api/media/register
  { upload_id, student_ids, class_id, visibility, activity_id }

Laravel:
  ├─ التحقق من وجود الملف فعلاً في R2 (HEAD request)
  ├─ Dispatch Job::ProcessMediaFile ← queue على database
  │    ├─ تحميل من R2 temp/ ← ذاكرة السيرفر (tmpfs)
  │    ├─ Resize to max 1920px width
  │    ├─ Generate thumbnail (300px)
  │    ├─ Convert to WebP
  │    ├─ رفع الملفات المعالجة إلى R2 (المسار الدائم)
  │    └─ حذف الملف المؤقت من temp/
  ├─ إنشاء سجل Spatie Media
  ├─ إنشاء media_permissions
  └─ رد: { media_id, status: "processing" }
```

### الخطوة ④ — التأكد من اكتمال المعالجة

```
Mobile App → GET /api/media/{id}/status (كل 3 ثوانٍ)
  حتى status = "ready"
```

---

### الصيغ المقبولة (v1)

| النوع | الصيغ | الحد الأقصى |
|-------|-------|-------------|
| صور | jpg, jpeg, png, webp, heic | 10 MB |

**ملاحظة:** التحقق من mime type وحجم الملف يتم في مرحلة `request-upload` (الخطوة ①)، والتي يتم أخذ قرار رفض الرفع بواسطة السيرفر قبل إصدار الـ presigned URL وليس بعد الرفع.

### سياسة التخزين المؤقت (Caching)

- الصور: `Cache-Control: public, max-age=2592000` (شهر)
- استخدام Cloudflare CDN أمام R2 لتحسين سرعة التحميل (Domain: `cdn.bilgiagaci.com`)

---

## صلاحيات الوصول للملفات (Access Control)

لا يُسمح بالوصول المباشر لـ R2. جميع الملفات يتم تقديمها عبر Laravel Temporary Signed URLs:

```php
// Spatie Media Library + R2 temporary URLs
$student->getMedia('daily_photos');
// يتم إنشاء رابط مؤقت صالح لمدة 60 دقيقة
$photo->getTemporaryUrl(now()->addMinutes(60));
```

هذا يضمن:
1. التحقق من صلاحية المستخدم قبل إعطاء الرابط
2. منع تسريب الروابط الدائمة
3. تتبع من شاهد الملف (اختياري)

---

## العزل حسب الصلاحية (Data Isolation)

| من يرى | نطاق الملفات |
|--------|-------------|
| ولي الأمر | `students/{student_id}/*` لأطفاله فقط |
| المعلم | `students/{student_id}/*` لطلاب صفه + `activities/{activity_id}/*` |
| الإدارة | جميع الملفات |

---

## دورة حياة الملف (File Lifecycle)

```
1. طلب الرفع (Request Upload)
   ├─ يتم إنشاء طلب من mobile app
   ├─ يتم التحقق من mime type والحجم وتحديد مسار في R2
   └─ يتم إصدار Presigned URL (صلاحية 5 دقائق)

2. رفع (Upload to R2) — مباشر من التطبيق
   ├─ التطبيق يرفع الملف إلى R2 مباشرة (HTTP PUT)
   ├─ الملف يصل إلى R2 في المسار المؤقت: uploads/temp/{file_key}
   └─ التطبيق يُخبر Laravel عبر POST /api/media/register

3. معالجة (Processing) — عبر Queue (database)
   ├─ Job::ProcessMediaFile:
   │    ├─ يحمّل الملف من temp/ ← يعالجه ← يرفع النتيجة للمسار الدائم
   │    └─ يحذف الملف المؤقت
   ├─ يُنشئ سجل Spatie Media + Media Permissions
   └─ الحالة: processing → ready (أو failed)

4. الموافقة (Approval) — إذا require_approval = true
   ├─ الملف يظهر بـ visibility='admin_only'
   └─ Admin يوافق ← visibility تتغير لـ 'parents_tagged' أو 'teachers'

5. العرض (Serve)
   └─ API ترجع Temporary Signed URL عند الطلب (صلاحية 60 دقيقة)

6. الحذف (Cleanup)
   ├─ Admin يحذف يدويًا → تُحذف من R2
   ├─ Job::PruneOrphanedMedia يحذف الملفات غير المرتبطة (أسبوعيًا)
   └─ Job::CleanupTempUploads يحذف الملفات في temp/ الأقدم من 24 ساعة (يوميًا)
```

---

## تقدير التخزين الشهري

| النوع | متوسط لكل طالب/شهر | لـ 120 طالب |
|-------|-------------------|-------------|
| صور يومية (5 صور × WebP ~200KB) | 30 MB | 3.6 GB |
| صور أنشطة | 10 MB | 1.2 GB |
| **الإجمالي الشهري** | **~40 MB** | **~4.8 GB** |

**تقدير سنوي:** ~57.6 GB.
**تكلفة R2:** ~$0.7/شهر (لأول 10 GB مجاني + $0.015/GB بعدها).

---

## نقاط يجب مراعاتها

1. **R2 CORS** — يجب ضبط CORS policy في R2 للسماح بطلبات الـ mobile app.
2. **Temporary URLs expiration** — 60 دقيقة كافٍ لتحميل الصورة في التطبيق.
3. **تنضيف الملفات المؤقتة** — الملفات في `temp/` تُحذف بعد 24 ساعة عبر Job.
4. **النسخ الاحتياطي** — R2 يوفر نسخ احتياطية تلقائية، ولكن يمكن إعداد sync إلى bucket آخر.
5. **عدم تخزين الملفات محليًا** — السيرفر لا يحتفظ بنسخ دائمة، فقط مؤقتة أثناء المعالجة.
