بناء نظام رفع ملفات في Django وFastAPI مع معالجة الصور والفيديو

بناء نظام رفع ملفات في Django وFastAPI مع معالجة الصور والفيديو

في هذا الشرح العملي سنتعلم خطوة بخطوة كيف نبني نظام File Upload Django FastAPI احترافي يدعم:

  • رفع الملفات (صور، فيديو، مستندات) في Django وFastAPI
  • ضغط الصور وتقليل حجمها بدون خسارة كبيرة في الجودة
  • إنشاء thumbnails للصور
  • دعم رفع ومعالجة الفيديو (استخراج صورة معاينة thumbnail)
  • حماية الرفع من الملفات الخبيثة والمخاطر الأمنية
  • أفضل الممارسات في تنظيم الكود وهيكلة المشروع

إذا كنت مستخدم Django أو FastAPI لبناء API أو لوحة تحكم، فهذا الدليل سيساعدك في بناء طبقة رفع ملفات نظيفة، آمنة، وقابلة للتوسع. يمكنك كذلك الاستفادة من مقالاتنا عن التعامل مع Background Tasks في Django وFastAPI لدمج المعالجة الخلفية للصور والفيديو بشكل متقدم.

1. أساسيات رفع الملفات في Django

1.1 إعدادات MEDIA في settings.py

أول خطوة في Django هي تعريف مكان حفظ الملفات وتهيئة إعدادات MEDIA:


# settings.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# تأكد من إضافة مكتبة Pillow في requirements
# Pillow==10.0.0

ثم في urls.py الخاص بالمشروع الرئيسي:


# urls.py (project level)

from django.conf import settings
from django.conf.urls.static import static
from django.urls import path, include

urlpatterns = [
    path("api/", include("api.urls")),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

1.2 نموذج (Model) لحفظ الملفات

لننشئ نموذجاً عاماً لحفظ الصور والفيديو مع معلومات أساسية:


# models.py

from django.db import models

class MediaFile(models.Model):
    IMAGE = "image"
    VIDEO = "video"
    DOCUMENT = "document"

    FILE_TYPES = [
        (IMAGE, "Image"),
        (VIDEO, "Video"),
        (DOCUMENT, "Document"),
    ]

    file = models.FileField(upload_to="uploads/%Y/%m/")
    file_type = models.CharField(max_length=20, choices=FILE_TYPES)
    created_at = models.DateTimeField(auto_now_add=True)
    original_name = models.CharField(max_length=255, blank=True)
    size_bytes = models.BigIntegerField(default=0)
    mime_type = models.CharField(max_length=100, blank=True)

    thumbnail = models.ImageField(
        upload_to="thumbnails/%Y/%m/",
        null=True,
        blank=True
    )

    def __str__(self):
        return f"{self.original_name or self.file.name} ({self.file_type})"

1.3 View لرفع الملف في Django (بدون REST)

في أبسط شكل يمكننا استخدام Form و View تقليدي:


# forms.py

from django import forms
from .models import MediaFile

class MediaFileUploadForm(forms.ModelForm):
    class Meta:
        model = MediaFile
        fields = ["file"]

# views.py

import mimetypes
from django.shortcuts import render, redirect
from .forms import MediaFileUploadForm
from .models import MediaFile

def detect_file_type(mime_type: str) -> str:
    if mime_type.startswith("image/"):
        return MediaFile.IMAGE
    if mime_type.startswith("video/"):
        return MediaFile.VIDEO
    return MediaFile.DOCUMENT

def upload_file_view(request):
    if request.method == "POST":
        form = MediaFileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            instance = form.save(commit=False)
            uploaded_file = request.FILES["file"]

            instance.original_name = uploaded_file.name
            instance.size_bytes = uploaded_file.size
            mime_type, _ = mimetypes.guess_type(uploaded_file.name)
            instance.mime_type = mime_type or "application/octet-stream"
            instance.file_type = detect_file_type(instance.mime_type)

            instance.save()
            return redirect("upload_success")
    else:
        form = MediaFileUploadForm()
    return render(request, "upload_form.html", {"form": form})

إذا كنت تستخدم Django REST Framework وواجهات API، يمكنك إعادة استخدام المنطق السابق داخل ViewSet أو APIView بسهولة، جنباً إلى جنب مع ما تعلمته في نظام تسجيل مستخدمين باستخدام Django REST و Next.js.

2. ضغط الصور وإنشاء Thumbnails في Django

2.1 استخدام Pillow لمعالجة الصور

لضغط الصور وإنشاء thumbnail، سنستخدم Pillow. الفكرة العامة:

  • بعد حفظ الصورة، نفتحها باستخدام Pillow
  • نقلل أبعادها أو جودة الضغط
  • ننشيء نسخة مصغرة (thumbnail) بحجم ثابت

# utils/images.py

from io import BytesIO
from django.core.files.base import ContentFile
from PIL import Image

def compress_image(django_file, max_width=1920, quality=80):
    image = Image.open(django_file)
    image = image.convert("RGB")

    w, h = image.size
    if w > max_width:
        ratio = max_width / float(w)
        new_height = int(h * ratio)
        image = image.resize((max_width, new_height), Image.LANCZOS)

    buffer = BytesIO()
    image.save(buffer, format="JPEG", quality=quality, optimize=True)
    buffer.seek(0)

    return ContentFile(buffer.read(), name=django_file.name.rsplit(".", 1)[0] + ".jpg")


def generate_thumbnail(django_file, size=(300, 300)):
    image = Image.open(django_file)
    image = image.convert("RGB")
    image.thumbnail(size, Image.LANCZOS)

    buffer = BytesIO()
    image.save(buffer, format="JPEG", quality=80, optimize=True)
    buffer.seek(0)

    thumb_name = "thumb_" + django_file.name.rsplit("/", 1)[-1].rsplit(".", 1)[0] + ".jpg"
    return ContentFile(buffer.read(), name=thumb_name)

2.2 دمج الضغط وإنشاء thumbnail مع نموذج MediaFile

يمكن تنفيذ ذلك داخل save() في الموديل أو داخل إشارة (signal). مثال باستخدام post_save:


# signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import MediaFile
from .utils.images import compress_image, generate_thumbnail

@receiver(post_save, sender=MediaFile)
def process_image_after_upload(sender, instance: MediaFile, created, **kwargs):
    if not created:
        return

    if instance.file_type != MediaFile.IMAGE:
        return

    # ضغط الصورة
    compressed = compress_image(instance.file)
    instance.file.save(compressed.name, compressed, save=False)

    # إنشاء thumbnail
    thumb = generate_thumbnail(instance.file)
    instance.thumbnail.save(thumb.name, thumb, save=False)

    instance.save()

# apps.py

from django.apps import AppCompatActivity

class ApiConfig(AppCompatActivity):
    name = "api"

    def ready(self):
        import api.signals  # noqa

بهذا الشكل، سيتم ضغط أي صورة مرفوعة وإنشاء thumbnail لها تلقائياً بعد حفظها، دون الحاجة لتكرار الكود في كل View.

3. دعم رفع الفيديو واستخراج Thumbnails

3.1 استخدام ffmpeg لاستخراج صورة من الفيديو

لمعالجة الفيديو واستخراج صورة معاينة (thumbnail)، يمكن استخدام ffmpeg من خلال subprocess أو مكتبات مثل moviepy. هنا مثال بسيط مع ffmpeg:


# utils/videos.py

import subprocess
from pathlib import Path
from django.core.files.base import File

def extract_video_thumbnail(video_path: str, output_dir: str, time="00:00:01"):
    """
    يستخرج صورة من الفيديو عند الثانية 1.
    """
    output_path = Path(output_dir) / (Path(video_path).stem + "_thumb.jpg")

    cmd = [
        "ffmpeg",
        "-ss",
        time,
        "-i",
        video_path,
        "-frames:v",
        "1",
        "-q:v",
        "2",
        str(output_path),
    ]

    subprocess.run(cmd, check=True)
    return str(output_path)


def attach_video_thumbnail(instance):
    """
    instance: كائن MediaFile لفيديو
    """
    from django.conf import settings
    import os

    video_full_path = os.path.join(settings.MEDIA_ROOT, instance.file.name)
    thumb_dir = os.path.join(settings.MEDIA_ROOT, "thumbnails/video")
    os.makedirs(thumb_dir, exist_ok=True)

    thumb_path = extract_video_thumbnail(video_full_path, thumb_dir)
    with open(thumb_path, "rb") as f:
        instance.thumbnail.save(Path(thumb_path).name, File(f), save=False)

3.2 استدعاء معالجة الفيديو بعد الرفع


# signals.py (توسعة)

from .utils.videos import attach_video_thumbnail

@receiver(post_save, sender=MediaFile)
def process_media_after_upload(sender, instance: MediaFile, created, **kwargs):
    if not created:
        return

    if instance.file_type == MediaFile.IMAGE:
        from .utils.images import compress_image, generate_thumbnail

        compressed = compress_image(instance.file)
        instance.file.save(compressed.name, compressed, save=False)
        thumb = generate_thumbnail(instance.file)
        instance.thumbnail.save(thumb.name, thumb, save=False)
        instance.save()

    elif instance.file_type == MediaFile.VIDEO:
        # هنا من الأفضل استخدام Background Task حتى لا يتأخر الرد
        attach_video_thumbnail(instance)
        instance.save()

لمعالجة الفيديو بشكل أفضل، خاصة مع الملفات الكبيرة، ينصح بالاعتماد على Background Tasks باستخدام Celery أو RQ أو حتى خلفيات FastAPI وDjango حسب تصميم مشروعك.

4. أساسيات رفع الملفات في FastAPI

4.1 إعداد FastAPI لرفع الملفات

في FastAPI يمكن استقبال الملفات بسهولة بواسطة UploadFile. سنبني مسار (endpoint) بسيط لاستقبال صورة أو فيديو وحفظه في مجلد:


# main.py

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.staticfiles import StaticFiles
from pathlib import Path
import shutil
import mimetypes
import uuid
import os

app = FastAPI()

BASE_DIR = Path(__file__).resolve().parent
MEDIA_ROOT = BASE_DIR / "media"
UPLOAD_DIR = MEDIA_ROOT / "uploads"
THUMB_DIR = MEDIA_ROOT / "thumbnails"

os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(THUMB_DIR, exist_ok=True)

app.mount("/media", StaticFiles(directory=str(MEDIA_ROOT)), name="media")


def detect_file_type(mime_type: str) -> str:
    if mime_type.startswith("image/"):
        return "image"
    if mime_type.startswith("video/"):
        return "video"
    return "document"


@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    # حماية أساسية: تحديد أنواع الملفات المسموح بها
    allowed_mime_prefixes = ("image/", "video/", "application/pdf")
    mime_type = file.content_type or mimetypes.guess_type(file.filename)[0]

    if not mime_type or not mime_type.startswith(allowed_mime_prefixes):
        raise HTTPException(status_code=400, detail="نوع الملف غير مسموح")

    # اسم ملف فريد
    ext = Path(file.filename).suffix
    unique_name = f"{uuid.uuid4().hex}{ext}"
    dest_path = UPLOAD_DIR / unique_name

    with dest_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    file_type = detect_file_type(mime_type)

    return {
        "filename": file.filename,
        "stored_as": unique_name,
        "file_type": file_type,
        "mime_type": mime_type,
        "url": f"/media/uploads/{unique_name}",
    }

5. معالجة الصور في FastAPI (ضغط + Thumbnails)

5.1 وظيفة مساعدة باستخدام Pillow


# image_utils.py

from pathlib import Path
from PIL import Image

def compress_image(path: Path, max_width=1920, quality=80):
    image = Image.open(path)
    image = image.convert("RGB")

    w, h = image.size
    if w > max_width:
        ratio = max_width / float(w)
        new_height = int(h * ratio)
        image = image.resize((max_width, new_height), Image.LANCZOS)

    image.save(path, format="JPEG", quality=quality, optimize=True)


def create_thumbnail(src_path: Path, dest_dir: Path, size=(300, 300)) -> Path:
    image = Image.open(src_path)
    image = image.convert("RGB")

    image.thumbnail(size, Image.LANCZOS)

    thumb_name = f"thumb_{src_path.stem}.jpg"
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest_path = dest_dir / thumb_name

    image.save(dest_path, format="JPEG", quality=80, optimize=True)
    return dest_path

5.2 دمج المعالجة داخل FastAPI Endpoint


# main.py (تحديث endpoint)

from image_utils import compress_image, create_thumbnail

@app.post("/upload-image")
async def upload_image(file: UploadFile = File(...)):
    if not file.content_type.startswith("image/"):
        raise HTTPException(status_code=400, detail="يجب رفع صورة فقط")

    ext = Path(file.filename).suffix
    unique_name = f"{uuid.uuid4().hex}{ext}"
    dest_path = UPLOAD_DIR / unique_name

    with dest_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    # ضغط الصورة وإنشاء thumbnail
    compress_image(dest_path)
    thumb_path = create_thumbnail(dest_path, THUMB_DIR)

    return {
        "url": f"/media/uploads/{dest_path.name}",
        "thumbnail_url": f"/media/thumbnails/{thumb_path.name}",
    }

لمنع تأخير الرد على المستخدم عند معالجة ملفات كبيرة، يمكنك نقل منطق الضغط وإنشاء thumbnails إلى Background Task باستخدام BackgroundTasks من FastAPI، كما شرحنا في مقال التعامل مع Background Tasks في Django وFastAPI.

6. دعم الفيديو في FastAPI واستخراج Thumbnails

6.1 مسار لرفع الفيديو


# main.py

from video_utils import extract_video_thumbnail_fastapi

@app.post("/upload-video")
async def upload_video(file: UploadFile = File(...)):
    if not file.content_type.startswith("video/"):
        raise HTTPException(status_code=400, detail="يجب رفع ملف فيديو")

    ext = Path(file.filename).suffix
    unique_name = f"{uuid.uuid4().hex}{ext}"
    dest_path = UPLOAD_DIR / unique_name

    with dest_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    thumb_path = extract_video_thumbnail_fastapi(dest_path, THUMB_DIR)

    return {
        "url": f"/media/uploads/{dest_path.name}",
        "thumbnail_url": f"/media/thumbnails/{thumb_path.name}",
    }

# video_utils.py

from pathlib import Path
import subprocess

def extract_video_thumbnail_fastapi(video_path: Path, dest_dir: Path, time="00:00:01") -> Path:
    dest_dir.mkdir(parents=True, exist_ok=True)
    thumb_path = dest_dir / f"{video_path.stem}_thumb.jpg"

    cmd = [
        "ffmpeg",
        "-ss",
        time,
        "-i",
        str(video_path),
        "-frames:v",
        "1",
        "-q:v",
        "2",
        str(thumb_path),
    ]
    subprocess.run(cmd, check=True)
    return thumb_path

7. حماية نظام File Upload Django FastAPI من الملفات الخبيثة

رفع الملفات نقطة حساسة أمنياً. بعض النقاط المهمة:

7.1 تقييد أنواع الملفات

  • تأكد من أن content_type أو الامتداد ضمن قائمة مسموح بها فقط.
  • في Django: تحقق من uploaded_file.content_type قبل الحفظ.
  • في FastAPI: تحقق من file.content_type أو الامتداد.

7.2 منع تنفيذ الملفات الضارة

  • لا تحفظ الملفات ضمن نفس مسار الكود (project folder)؛ استخدم مجلد media منفصل.
  • تأكد من أن خادم الويب (nginx / Apache) لا يعامل المجلد ككود قابل للتنفيذ (مثلاً PHP أو CGI).
  • تجنب قبول ملفات مثل .php، .exe، .sh، إلخ.

7.3 تحديد الحجم الأقصى للملف

  • في Django: استخدم FILE_UPLOAD_MAX_MEMORY_SIZE في settings، أو تحقق يدوياً من uploaded_file.size.
  • في FastAPI: نفذ منطق للتحقق من حجم الملف أثناء الاستقبال (قد تقرأ على أجزاء وتتأكد).

7.4 فحص الملفات بمضاد فيروسات (اختياري)

في تطبيقات حساسة (أنظمة تبادل ملفات، تطبيقات شركات)، يمكنك دمج ClamAV أو خدمة خارجية لفحص الملفات قبل إتاحتها للمستخدمين.

8. مقارنة سريعة بين Django وFastAPI في رفع الملفات

  • Django:
    • مناسب للتطبيقات التقليدية (Panel + Forms) وREST API باستخدام Django REST Framework.
    • يتميز بنظام قوي لإدارة الملفات ضمن الموديلات (FileField, ImageField).
    • يصلح إذا كان تطبيقك متكامل (Auth, Admin, Templates)، ويمكنك الاستفادة من مقالة نظام قوالب Django لعرض الصور والفيديو.
  • FastAPI:
    • ممتاز لبناء APIs خفيفة وسريعة لرفع الملفات ومعالجتها.
    • دعم رائع لـ async وBackground Tasks، مما يجعله مناسباً لمعالجة الفيديو والصور الثقيلة.
    • أسهل في الدمج مع خدمات Frontend (React, Vue, Next.js) عبر REST أو WebSockets.

9. هيكلة مقترحة لمشروع يجمع Django وFastAPI

في بعض الأنظمة، قد تستخدم Django للجزء الإداري وFastAPI كخدمة معالجة ملفات (Microservice). مثال بسيط:

  • مشروع Django:
    • يتحكم في المستخدمين، الصلاحيات، لوحة التحكم، وربما Middleware مخصص للأمان.
    • يخزن روابط الملفات (URLs) فقط وليس الملفات نفسها.
  • خدمة FastAPI:
    • تستقبل الملفات من Django (أو Frontend مباشرة).
    • تقوم بضغط الصور، إنشاء thumbnails، استخراج صورة من الفيديو، ثم ترجع JSON يحوي مسارات الملفات.

بهذا التصميم، يمكنك الاستفادة من قوة File Upload Django FastAPI معاً، وتوزيع الحمل على خدمات متعددة وقابلة للتوسع.

10. خلاصة وأفضل الممارسات

  • عرّف بوضوح أنواع الملفات المسموح برفعها وحدد حجم أقصى منطقي.
  • استخدم Pillow لضغط الصور وإنشاء thumbnails للحفاظ على سرعة الموقع وتقليل استهلاك التخزين.
  • للفيديو، استخدم ffmpeg لاستخراج صورة معاينة، ويفضل نقل المعالجة إلى Background Tasks.
  • احفظ الملفات في مجلد

حول المحتوى:

شرح كيفية رفع الملفات، ضغط الصور، إنشاء ثumbnails، دعم الفيديو، وحماية الرفع من الملفات الخبيثة.

هل كان هذا مفيدًا لك؟

أضف تعليقك