يتطلب هذا المقال مستوى: متوسط. لازم تكون عارف Python أساسي، وشكل طلب HTTP، وعندك فكرة عن S3. مش محتاج تكون خبير AWS.
ارفع الملفات مباشرة إلى S3 بروابط موقّعة: خادمك يوقّع بس، والملف ميعديش عليه
لو المستخدم بيرفع ملف 240 ميجابايت وبيعدّي على خادمك الأول، انت بتدفع الثمن مرتين: ذاكرة الخادم بتتحجز طول الرفع، والـ request بيقع عند 30 ثانية. الحل إن المتصفح يرفع الملف مباشرة على S3، وخادمك دوره الوحيد إنه يوقّع رابط. في المقال ده هتبني ده خطوة بخطوة بـ boto3، وهتقيس الفرق بالأرقام.
المشكلة باختصار
الطريقة الشائعة: المتصفح يبعت الملف لخادمك (Flask أو FastAPI أو Express)، وخادمك يرفعه على S3. الطريقة دي بتفشل بمجرد ما الملفات تكبر. كل رفع بيحجز ذاكرة على الخادم بحجم الملف تقريبًا. عشرة مستخدمين بيرفعوا 240 ميجابايت في نفس الوقت معناه ~2.4 جيجابايت RAM محجوزة في لحظة واحدة. وأغلب البروكسيات (زي nginx أو الـ load balancer) بتقفل الاتصال بعد 30 أو 60 ثانية، فالرفع الكبير بيضرب 504 Gateway Timeout.
الفكرة الأول بمثال بسيط
تخيّل مطعم فيه فاليه باركنج. انت مش بتدّي مفتاح عربيتك لكل واحد واقف على الباب. بتاخد من موظف الاستقبال تذكرة عليها رقم ومكتوب إنها صالحة النهاردة بس. التذكرة دي بتخوّلك تسيب العربية في مكان محدد، من غير ما موظف الاستقبال يمسك العربية بنفسه.
الرابط الموقّع (Presigned URL) هو التذكرة دي بالظبط. خادمك عنده مفتاح AWS السري (زي مدير المطعم). بدل ما يمرّر كل ملف بنفسه، بيولّد رابطًا موقّعًا: عنوان طويل فيه توقيع تشفيري بيقول لـ S3 «الشخص اللي معاه اللينك ده مسموح له يكتب الملف ده بالتحديد في المكان ده بالتحديد، ولمدة 5 دقايق بس». المتصفح بيستخدم الرابط ويرفع مباشرة على S3.
علميًا: الرابط الموقّع هو URL عادي مضاف له معاملات توقيع بخوارزمية AWS Signature Version 4. التوقيع بيتحسب من مفتاحك السري + اسم الـ bucket + اسم الملف (key) + وقت الانتهاء + العملية (PUT). S3 بيعيد حساب نفس التوقيع لما الطلب يوصله؛ لو طابق، بيقبل الرفع. مفتاحك السري نفسه معدّاش على الشبكة أبدًا، والرابط بينتهي لوحده بعد المدة اللي حددتها.
الخطوات
- جهّز bucket ومستخدم IAM بصلاحية محدودة. اعمل bucket، واعمل مستخدم أو Role عنده صلاحية
s3:PutObjectعلى مسار الرفع بس، مش على الـ bucket كله. النتيجة المتوقعة: عندكAWS_ACCESS_KEY_IDوAWS_SECRET_ACCESS_KEYبصلاحية ضيقة. - ثبّت boto3. شغّل الأمر ده. النتيجة: مكتبة AWS جاهزة في بيئتك.
pip install boto3
- اكتب endpoint يولّد الرابط الموقّع. الكود ده على FastAPI. بيستقبل اسم الملف ونوعه، ويرجّع رابط PUT صالح 5 دقايق.
import uuid
import boto3
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
s3 = boto3.client("s3", region_name="eu-central-1")
BUCKET = "my-uploads-bucket"
class SignRequest(BaseModel):
filename: str
content_type: str
@app.post("/uploads/sign")
def sign_upload(req: SignRequest):
# مفتاح فريد يمنع الكتابة فوق ملفات المستخدمين ببعض
key = f"uploads/{uuid.uuid4()}-{req.filename}"
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={
"Bucket": BUCKET,
"Key": key,
"ContentType": req.content_type, # نثبّت النوع فما يتغيّرش
},
ExpiresIn=300, # 5 دقايق
)
return {"upload_url": url, "key": key}
- اضبط CORS على الـ bucket. من غير ده، المتصفح هيرفض الـ PUT المباشر لأنه من دومين تاني. حط الإعداد ده في تبويب Permissions > CORS في الـ bucket. النتيجة: المتصفح مسموح له يعمل PUT من موقعك.
[
{
"AllowedOrigins": ["https://your-site.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
- ارفع من المتصفح على مرحلتين. الأول اطلب الرابط من خادمك، بعدين اعمل PUT للملف مباشرة على S3. لاحظ إن الـ
Content-Typeلازم يطابق اللي وقّعت بيه بالظبط.
async function uploadFile(file) {
// 1) اطلب رابطًا موقّعًا من خادمك
const res = await fetch("/uploads/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, content_type: file.type }),
});
const { upload_url, key } = await res.json();
// 2) ارفع الملف مباشرة على S3 — مش على خادمك
const put = await fetch(upload_url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
if (!put.ok) throw new Error("Upload failed: " + put.status);
return key; // ابعت الـ key لخادمك عشان يسجّله في قاعدة البيانات
}
- سجّل الـ key بعد نجاح الرفع. بعد ما الـ PUT يرجّع 200، ابعت الـ
keyلخادمك ليحفظه في قاعدة البيانات ويربطه بالمستخدم. النتيجة: عندك مرجع للملف من غير ما بايت واحد منه عدّى على خادمك.
التحقق من إنه شغّال
ارفع ملف من الواجهة، وافتح تبويب Network في المتصفح. المفروض تشوف طلبين: طلب صغير لـ /uploads/sign (حجم الرد ~1 كيلوبايت)، وطلب PUT كبير رايح على دومين s3.amazonaws.com مباشرة. لو الملف ظهر في الـ bucket والـ PUT رجّع 200، يبقى تمام. لو رجّع 403 SignatureDoesNotMatch، غالبًا الـ Content-Type في الـ PUT مختلف عن اللي وقّعت بيه.
الأرقام: قبل وبعد
قياس تقريبي على ملف 240 ميجابايت وخادم متوسط:
- عبر الخادم: ذاكرة محجوزة ~240 ميجابايت لكل رفع متزامن، والاتصال بيقع عند 30 ثانية على البروكسي، والباندويدث بيتحاسب مرتين (دخول للخادم + خروج لـ S3).
- بالرابط الموقّع: خادمك بيعمل شغل بحجم ~1 كيلوبايت في ~3 مللي ثانية (توليد التوقيع)، الذاكرة المحجوزة للرفع ~صفر، والباندويدث بيتحاسب مرة واحدة على S3 بس.
الافتراض هنا إنك على AWS S3 حقيقي (مش نظام تخزين تاني)، وإن الملفات أقل من 5 جيجابايت (حد الـ PUT الواحد في S3).
الـ trade-offs
بتكسب: خادمك يتفضّى تمامًا من حمل الرفع، ويوسّع مستقل عن حجم الملفات. بتخسر: مبقاش تقدر تفحص أو تعدّل الملف وهو داخل (ضغط صورة، فحص فيروسات) لأنه بيروح S3 مباشرة. الحل إنك تعمل الفحص بعد الرفع عن طريق S3 Event يشغّل Lambda.
نقطة أمان: الرابط الموقّع بيدّي صلاحية كتابة على الـ key ده طول مدة الصلاحية. لو اتسرّب في الـ 5 دقايق دول، حد يقدر يكتب فوق نفس الملف. خفّف الخطر بمدة قصيرة + مفتاح فريد (UUID) + تثبيت ContentType. لو محتاج تفرض حجم أقصى للرفع، استخدم generate_presigned_post بدل put_object، لأنه بيدعم شرط content-length-range.
متى لا تستخدم هذه الطريقة
- ملفات أكبر من 5 جيجابايت — محتاج multipart upload برابط موقّع لكل جزء.
- محتاج تتحقق من محتوى الملف أو تحوّله قبل ما يتخزّن أصلاً — هنا الرفع المباشر ميناسبكش.
- ملفات صغيرة جدًا (بضعة كيلوبايت) وكتير — الطلب الإضافي لجلب الرابط بيزوّد latency من غير مكسب حقيقي.
- تخزينك مش S3 ولا متوافق معاه (بعض الأنظمة مش بتدعم presign).
الخطوة التالية
افتح أقرب endpoint رفع عندك، وشوف هل الملف بيعدّي على خادمك ولا لأ. لو آه، انقل الرفع لرابط موقّع بنفس الـ 6 خطوات دي، وابدأ بمسار واحد (رفع الصور مثلاً). قِس ذاكرة الخادم قبل وبعد؛ لو نزلت فعلاً، عمّم الطريقة على باقي المسارات.
المصادر
- AWS — Sharing objects with presigned URLs: docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
- Boto3 — generate_presigned_url: boto3.amazonaws.com/.../generate_presigned_url.html
- Boto3 — generate_presigned_post (لفرض شروط الحجم): boto3.amazonaws.com/.../generate_presigned_post.html
- AWS — CORS configuration on S3: docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html
- AWS — Signature Version 4: docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html