حول المحتوى:
شرح كيفية رفع الملفات، ضغط الصور، إنشاء ثumbnails، دعم الفيديو، وحماية الرفع من الملفات الخبيثة.
في هذا الشرح العملي سنتعلم خطوة بخطوة كيف نبني نظام File Upload Django FastAPI احترافي يدعم:
إذا كنت مستخدم Django أو FastAPI لبناء API أو لوحة تحكم، فهذا الدليل سيساعدك في بناء طبقة رفع ملفات نظيفة، آمنة، وقابلة للتوسع. يمكنك كذلك الاستفادة من مقالاتنا عن التعامل مع Background Tasks في Django وFastAPI لدمج المعالجة الخلفية للصور والفيديو بشكل متقدم.
أول خطوة في 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)
لننشئ نموذجاً عاماً لحفظ الصور والفيديو مع معلومات أساسية:
# 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})"
في أبسط شكل يمكننا استخدام 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.
لضغط الصور وإنشاء thumbnail، سنستخدم Pillow. الفكرة العامة:
# 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)
يمكن تنفيذ ذلك داخل 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.
لمعالجة الفيديو واستخراج صورة معاينة (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)
# 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 حسب تصميم مشروعك.
في 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}",
}
# 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
# 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.
# 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
رفع الملفات نقطة حساسة أمنياً. بعض النقاط المهمة:
content_type أو الامتداد ضمن قائمة مسموح بها فقط.uploaded_file.content_type قبل الحفظ.file.content_type أو الامتداد.media منفصل..php، .exe، .sh، إلخ.FILE_UPLOAD_MAX_MEMORY_SIZE في settings، أو تحقق يدوياً من uploaded_file.size.في تطبيقات حساسة (أنظمة تبادل ملفات، تطبيقات شركات)، يمكنك دمج ClamAV أو خدمة خارجية لفحص الملفات قبل إتاحتها للمستخدمين.
في بعض الأنظمة، قد تستخدم Django للجزء الإداري وFastAPI كخدمة معالجة ملفات (Microservice). مثال بسيط:
بهذا التصميم، يمكنك الاستفادة من قوة File Upload Django FastAPI معاً، وتوزيع الحمل على خدمات متعددة وقابلة للتوسع.
شرح كيفية رفع الملفات، ضغط الصور، إنشاء ثumbnails، دعم الفيديو، وحماية الرفع من الملفات الخبيثة.
مساحة اعلانية