المستوى المطلوب: متوسط — محتاج تكون مرتاح مع Node.js وفكرة الـ HTTP requests، ومعندكش مشكلة تفتح حساب S3 أو R2.
بعد المقال ده سيرفرك مش هيشيل وزن رفع الملفات خالص. المتصفح هيرفع الصورة أو الفيديو على S3 مباشرة، وسيرفرك دوره هيبقى بس إنه يوقّع تذكرة دخول مؤقتة. النتيجة: استهلاك أقل، رفع أسرع، وفاتورة bandwidth أنضف.
المشكلة باختصار
الطريقة الشائعة إن الملف يترفع على سيرفرك الأول، وبعدين سيرفرك يرفعه على S3. الطريقة دي بتفشل لمّا الملفات تكبر أو العدد يزيد.
تخيّل متجر بيرفع 2000 صورة منتج في اليوم، كل واحدة 5 ميجا. ده معناه 10 جيجا في اليوم بتعدّي على سيرفرك من غير أي داعي. كل request بيقفل جزء من الـ RAM وطول الـ bandwidth، والـ worker بيفضل مشغول ثواني لحد ما الملف يخلّص. على VPS صغير، ده بيخنق باقي الطلبات العادية.
إزاي الـ Presigned URL بيشتغل
قبل التعريف العلمي، خد المثال ده. تخيّل إنك صاحب مخزن مقفول. صديقك عايز يدخل يحط صندوق جوه. انت مش محتاج تروح معاه وتحمل الصندوق بنفسك. انت بتديله مفتاح مؤقت يفتح باب واحد بس، صالح دقيقة واحدة، وبيقفل لوحده بعدها. هو يدخل يحط الصندوق ويمشي، وانت مقعدتش من مكانك.
الـ Presigned URL بالظبط كده. هو رابط مؤقت بيوقّعه سيرفرك بمفتاح S3 السري. الرابط بيقول لـ S3: "اسمح للي معاه الرابط ده إنه يرفع ملف في المكان الفلاني، بشرط معيّن، ولفترة محدودة". المتصفح بيستخدم الرابط ده ويرفع الملف على S3 مباشرة. مفتاحك السري عمره ما بيوصل للمتصفح، وسيرفرك مبيلمسش بايت واحد من الملف.
الخطوات
- الباك إند: endpoint بيستقبل اسم الملف ونوعه، ويرجّع Presigned URL صالح لثواني معدودة.
- الفرونت إند: يطلب الرابط الموقّع، وبعدين يعمل
PUTبالملف على الرابط ده مباشرة. - إعداد CORS على الـ bucket علشان المتصفح يُسمح له يرفع من دومينك.
- حفظ الـ key في قاعدة بياناتك بعد نجاح الرفع.
1) الباك إند (Node.js + AWS SDK v3)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import crypto from "node:crypto";
const s3 = new S3Client({ region: "eu-central-1" });
// POST /uploads/presign { filename, contentType }
app.post("/uploads/presign", async (req, res) => {
const { filename, contentType } = req.body;
// اسم فريد علشان الملفات ماتدوسش على بعض
const key = `uploads/${Date.now()}-${crypto.randomUUID()}-${filename}`;
const command = new PutObjectCommand({
Bucket: "my-app-uploads",
Key: key,
ContentType: contentType,
});
// الرابط صالح 60 ثانية بس
const url = await getSignedUrl(s3, command, { expiresIn: 60 });
res.json({ url, key });
});2) الفرونت إند (المتصفح بيرفع مباشرة)
async function uploadFile(file) {
// اطلب رابط موقّع من سيرفرك (رد صغير ~1KB)
const res = await fetch("/uploads/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, contentType: file.type }),
});
const { url, key } = await res.json();
// ارفع الملف على S3 مباشرة — سيرفرك مش في النص
await fetch(url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
return key; // احفظه في الـ DB
}3) إعداد CORS على الـ bucket
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"MaxAgeSeconds": 3000
}
]من غير الإعداد ده هيظهرلك خطأ CORS في الكونسول والرفع هيفشل. ركز إن Content-Type اللي بتبعته في الـ PUT لازم يطابق اللي وقّعت بيه في الباك إند بالظبط، وإلا S3 هيرفض بـ 403.
الأرقام والمكسب الحقيقي
الافتراض هنا إنك على S3 (أو متوافق معاه زي Cloudflare R2 و MinIO)، وبترفع ملفات متوسطة لكبيرة. على سيناريو المتجر اللي فوق:
- الـ bandwidth: 2000 ملف × 5MB ≈ 10GB في اليوم اتشالت بالكامل عن سيرفرك.
- زمن شغل السيرفر لكل رفعة: من حوالي 1.8 ثانية (وهو ماسك الملف كله) لـ ~15 مللي ثانية (مجرد توقيع رابط).
- الذاكرة: السيرفر مبقاش يحتاج يحجز buffer بحجم الملف، فالـ RAM الطرفية بتفضل فاضية للطلبات العادية.
الـ trade-off هنا واضح: بتكسب إن سيرفرك خفيف وسريع، بس بتخسر إنك مش شايف الملف وهو داخل. يعني مفيش فحص فيروسات ولا ضغط ولا تصغير وقت الرفع. الملف بيوصل S3 على طول. الحل إنك تشغّل المعالجة بعد الرفع عن طريق S3 Event يشغّل Lambda، أو job في الخلفية يقرأ الملف ويعالجه.
نقطة تانية مهمة: الـ presigned PUT ماينفعش يفرض حد أقصى لحجم الملف. لو محتاج تمنع حد يرفع ملف 2 جيجا، استخدم presigned POST policy اللي بتسمح بشرط content-length-range، أو افرض الحد من الـ Lambda بعد الرفع وامسح اللي يعدّي.
متى لا تستخدم هذه الطريقة
متستخدمهاش لو الملفات صغيرة جداً (بضع كيلوبايتات) — وقتها التعقيد الزيادة في الـ round-trip الإضافي مش مستاهل، خلّي الرفع يعدّي عادي على سيرفرك. كمان متستخدمهاش لو محتاج تتحقق من محتوى الملف أو ترفضه قبل ما يتخزن نهائياً (زي رفع مستندات حساسة لازم تتفحص أولاً)، لأن الملف بيوصل التخزين قبل ما تشوفه. وأخيراً لو معندكش تخزين object storage أصلاً وكل حاجة على نفس السيرفر، مفيش مكسب من الموضوع.
الخطوة التالية
افتح أي endpoint رفع عندك دلوقتي، وحوّله للنمط ده على ملف واحد بس كتجربة: اعمل الـ /uploads/presign، ظبّط CORS على bucket تجريبي، وارفع صورة من المتصفح. بصّ في تبويب Network في الـ DevTools — المفروض تلاقي طلب الـ PUT رايح على دومين S3 مباشرة مش على سيرفرك. لو شفته كده، يبقى الموضوع شغّال صح.
المصادر
- توثيق AWS SDK for JavaScript v3 — إنشاء Presigned URLs بـ
getSignedUrl: aws-sdk/s3-request-presigner - AWS S3 — Uploading objects with presigned URLs: PresignedUrlUploadObject
- AWS S3 — إعداد CORS على الـ bucket: Cross-origin resource sharing (CORS)
- AWS S3 — Presigned POST مع
content-length-rangeلفرض حجم: Creating a POST policy - Cloudflare R2 — توافق Presigned URLs مع S3 API: R2 Presigned URLs