Skip to content

مزودو التخزين

يوفر FORGE تجريداً موحداً لتخزين الملفات يعمل بشكل متطابق سواء خزّنت الملفات على نظام الملفات المحلي أو في مخزن كائنات سحابي. ارفع ملفاً مرة واحدة وبدّل واجهات التخزين في أي وقت بدون تغيير كود التطبيق.

سمة StorageProvider

جميع drivers التخزين تُنفّذ سمة StorageProvider:

rust
#[async_trait]
pub trait StorageProvider: Send + Sync {
    /// Store a file from bytes
    async fn put(&self, path: &str, data: &[u8]) -> Result<StoredFile, StorageError>;

    /// Store a file from stream (for large uploads)
    async fn put_stream(
        &self,
        path: &str,
        stream: impl AsyncRead + Send + Unpin,
        content_type: Option<&str>,
    ) -> Result<StoredFile, StorageError>;

    /// Retrieve file contents as bytes
    async fn get(&self, path: &str) -> Result<Vec<u8>, StorageError>;

    /// Get the public URL for a stored file
    fn url(&self, path: &str) -> String;

    /// Generate a time-limited signed URL for private files
    async fn temporary_url(
        &self,
        path: &str,
        expires_in: Duration,
    ) -> Result<String, StorageError>;

    /// Delete a file from storage
    async fn delete(&self, path: &str) -> Result<(), StorageError>;

    /// Check if a file exists
    async fn exists(&self, path: &str) -> Result<bool, StorageError>;
}

بنية StoredFile

كل رفع ناجح يُرجع StoredFile مع بيانات وصفية عن الكائن المُخزّن:

rust
pub struct StoredFile {
    /// Relative path within the storage backend
    pub path: String,
    /// Publicly accessible URL (or base URL + path for local storage)
    pub url: String,
    /// File size in bytes
    pub size: u64,
    /// Detected MIME type (e.g., "image/png", "application/pdf")
    pub mime_type: String,
}

الـ Drivers المتاحة

التخزين المحلي (الافتراضي)

يخزن الملفات على نظام الملفات المحلي. هذا هو الـ driver الافتراضي ولا يتطلب تثبيتاً إضافياً أو خدمة خارجية. مثالي للتطوير والنشر على خادم واحد.

الإعدادات:

الإعدادالنوعالافتراضيالوصف
storage_driverstring"local"معرّف driver التخزين
local_pathstring"./storage"مسار نظام الملفات للملفات المُخزّنة
local_urlstring"/storage"بادئة الرابط لتقديم الملفات (نسبي أو مطلق)

نصيحة

للتطوير، يعمل التخزين المحلي الافتراضي بدون أي إعدادات. تُحفظ الملفات تحت مجلد ./storage في جذر مشروعك وتُقدّم من خلال معالج الملفات الثابتة المدمج في /storage.

بنية المجلدات:

project-root/
└── storage/
    ├── uploads/
    │   ├── avatars/
    │   │   └── user-123.jpg
    │   └── documents/
    │       └── invoice-456.pdf
    └── media/
        └── images/
            └── hero-banner.webp

تحذير

التخزين المحلي لا يدعم temporary_url(). استدعاء هذه الدالة على driver المحلي يُرجع رابطاً دائماً بدلاً من ذلك. إذا كنت تحتاج تحكماً بالوصول محدود الوقت، بدّل إلى driver S3 أو نفّذ التحكم بالوصول على مستوى التطبيق.

تخزين S3

تخزين الكائنات عبر Amazon S3 API. هذا الـ driver يعمل مع Amazon S3 و DigitalOcean Spaces و MinIO و Backblaze B2 و Cloudflare R2 وأي خدمة متوافقة مع S3.

التثبيت:

bash
forge provider:add storage:s3

الإعدادات:

الإعدادالنوعالوصف
storage_driverstringاضبط على "s3"
s3_bucketstringاسم الـ Bucket
s3_regionstringمنطقة AWS (مثل us-east-1)
s3_access_keyencryptedAWS Access Key ID
s3_secret_keyencryptedAWS Secret Access Key
s3_endpointstringرابط نقطة النهاية المخصص (للخدمات المتوافقة مع S3 غير AWS)
s3_urlstringالرابط الأساسي العام للملفات المُخزّنة (CDN أو رابط الـ bucket)

الميزات:

  • الروابط الموقّعة -- توليد روابط محدودة الوقت للكائنات الخاصة بدون كشف بيانات الاعتماد
  • الرفوعات الموقّعة مسبقاً -- السماح للعملاء بالرفع مباشرة إلى S3 بدون التوجيه عبر خادمك
  • دعم CDN -- تقديم الملفات من خلال CloudFront أو أي CDN بتعيين s3_url إلى نطاق التوزيع
  • الرفوعات متعددة الأجزاء -- رفع مُقسّم تلقائي للملفات الكبيرة عبر put_stream()
إعدادات الخدمات المتوافقة مع S3

للخدمات غير AWS، عيّن s3_endpoint إلى نقطة نهاية مزودك:

الخدمةمثال نقطة النهايةالمنطقة
DigitalOcean Spaceshttps://nyc3.digitaloceanspaces.comnyc3
MinIOhttp://localhost:9000us-east-1
Backblaze B2https://s3.us-west-002.backblazeb2.comus-west-002
Cloudflare R2https://<account-id>.r2.cloudflarestorage.comauto

الرفوعات الموقّعة مسبقاً

لرفوعات الملفات الكبيرة، استخدم الروابط الموقّعة مسبقاً للسماح للعملاء بالرفع مباشرة إلى S3. هذا يتجنب توجيه الملفات الكبيرة عبر خادم API:

rust
let presigned = storage.temporary_url("uploads/large-video.mp4", Duration::from_secs(3600)).await?;
// أرجع الرابط الموقّع مسبقاً للعميل للرفع المباشر

الاستخدام

إنشاء المزود

يُنشئ المصنع الـ driver الصحيح بناءً على إعداداتك:

rust
use crate::services::storage::StorageFactory;

let storage = StorageFactory::create(&settings).await?;

رفع ملف

rust
// Upload from bytes
let data = std::fs::read("local-file.pdf")?;
let stored = storage.put("uploads/documents/report.pdf", &data).await?;

println!("Stored at: {}", stored.path);  // uploads/documents/report.pdf
println!("URL: {}", stored.url);          // https://cdn.example.com/uploads/documents/report.pdf
println!("Size: {} bytes", stored.size);  // 245760
println!("Type: {}", stored.mime_type);   // application/pdf

الرفع من Stream

للملفات الكبيرة، استخدم الرفع بالـ streaming لتجنب تحميل الملف بأكمله في الذاكرة:

rust
use tokio::fs::File;

let file = File::open("large-video.mp4").await?;
let stored = storage.put_stream(
    "uploads/videos/intro.mp4",
    file,
    Some("video/mp4"),
).await?;

استرجاع ملف

rust
// Get file contents as bytes
let data = storage.get("uploads/documents/report.pdf").await?;

// Get public URL
let url = storage.url("uploads/documents/report.pdf");
// => "https://cdn.example.com/uploads/documents/report.pdf"

توليد روابط مؤقتة

إنشاء روابط موقّعة محدودة الوقت للملفات الخاصة:

rust
use std::time::Duration;

let signed_url = storage.temporary_url(
    "uploads/private/contract.pdf",
    Duration::from_secs(3600),  // Expires in 1 hour
).await?;

// => "https://bucket.s3.amazonaws.com/uploads/private/contract.pdf?X-Amz-Signature=..."

تحذير

الروابط المؤقتة تتطلب driver S3. driver التخزين المحلي لا يدعم الروابط المنتهية الصلاحية. إذا كنت تحتاج تحكماً بالوصول مع التخزين المحلي، نفّذه من خلال طبقة التفويض في تطبيقك.

التحقق من الوجود

rust
if storage.exists("uploads/avatars/user-123.jpg").await? {
    println!("Avatar exists");
} else {
    println!("No avatar found, using default");
}

حذف ملف

rust
storage.delete("uploads/documents/old-report.pdf").await?;

معالجة رفوعات الملفات في المعالجات

معالج رفع ملفات نموذجي يجمع بين مزود التخزين وطبقة HTTP:

rust
use actix_multipart::Multipart;
use futures_util::StreamExt;

pub async fn upload_avatar(
    mut payload: Multipart,
    storage: web::Data<Box<dyn StorageProvider>>,
    auth: AuthenticatedUser,
) -> Result<HttpResponse, AppError> {
    while let Some(field) = payload.next().await {
        let mut field = field?;
        let content_type = field.content_type().to_string();
        let mut data = Vec::new();

        while let Some(chunk) = field.next().await {
            data.extend_from_slice(&chunk?);
        }

        // Validate file type
        if !["image/jpeg", "image/png", "image/webp"].contains(&content_type.as_str()) {
            return Err(AppError::validation("Only JPEG, PNG, and WebP images allowed"));
        }

        // Store the file
        let path = format!("uploads/avatars/{}.jpg", auth.user_id);
        let stored = storage.put(&path, &data).await?;

        return Ok(HttpResponse::Ok().json(stored));
    }

    Err(AppError::validation("No file provided"))
}

الإعدادات عبر الإدارة

تُدار إعدادات التخزين من خلال صفحة الإعدادات في الإدارة تحت مجموعة التخزين:

الإعدادات > التخزين
┌──────────────────────────────────────────────────┐
│  الـ Driver:       [S3               v]          │
│  Bucket:          [my-app-uploads     ]         │
│  المنطقة:         [us-east-1          ]         │
│  Access Key:      [AKIA...            ]         │
│  Secret Key:      [................   ]         │
│  نقطة النهاية:   [                   ]         │
│  الرابط العام:   [https://cdn.example.com]     │
└──────────────────────────────────────────────────┘

معالجة الأخطاء

جميع عمليات التخزين تُرجع Result<T, StorageError> مع أنواع أخطاء منظمة:

rust
match storage.put("uploads/file.pdf", &data).await {
    Ok(stored) => println!("مُخزّن: {}", stored.url),
    Err(StorageError::NotFound) => println!("الملف غير موجود"),
    Err(StorageError::PermissionDenied) => println!("الوصول مرفوض"),
    Err(StorageError::QuotaExceeded) => println!("تم الوصول لحد التخزين"),
    Err(StorageError::ProviderError(msg)) => println!("خطأ المزود: {}", msg),
    Err(e) => println!("خطأ غير متوقع: {}", e),
}

Released under the MIT License.