المستوى المستهدف: متوسط (Intermediate) — المقال يفترض إنك تعرف Node.js و REST API وعندك حساب AWS أو متعامل مع S3 من قبل. لو لسه مبتدئ تماماً مع AWS، ابدأ بقسم "يعني إيه Pre-signed URL أصلاً؟" — فيه مثال يبسّط الفكرة قبل ما ندخل على التفاصيل التقنية.
اعمل Pre-signed URLs لرفع الملفات على S3 — وفّر 90% من bandwidth السيرفر
لو endpoint /upload بتاعك بيقع عند 5 مستخدمين متزامنين بيرفعوا فيديوهات 80 ميجا، السيرفر مش ضعيف. أنت بتمرّ نفس الملف مرتين: مرة دخول من المستخدم لسيرفرك، ومرة خروج من سيرفرك لـ S3. Pre-signed URL بيلغي المرور الأوسط ويخلّي الملف يطير من المتصفح لـ S3 مباشرةً، فالسيرفر يفضل خفيف يردّ في أقل من 50ms حتى تحت ضغط 200 رفع متزامن.
المشكلة باختصار
الطريقة التقليدية مع multer أو أي مكتبة مشابهة بتفترض إن الملف لازم يعدّي على سيرفرك الأول. ده معناه إن كل ميجا بيرفعها المستخدم بتتحوّل لميجا ingress على سيرفرك + ميجا egress منه لـ S3. النتيجة: ضغط مزدوج على الـ bandwidth، استهلاك RAM للـ buffering، وفي الإنتاج بتدفع Data Transfer Out مرتين.
السيناريو اللي بيكسر السيرفر فعلاً: 5 مستخدمين بيرفعوا 80MB فيديو في نفس الثانية. Express بيحتجز 400MB RAM للـ buffering، ولو السيرفر بـ 2GB RAM فبتاكل 20% من الذاكرة في رفع واحد. لو دخل عليك 3 رفعات إضافية، الـ OOM Killer بيقتل العملية.
يعني إيه Pre-signed URL أصلاً؟
قبل التعريف العلمي، خد المثال ده:
تخيّل إن عندك مخزن كبير وفيه حارس ما بيدخّلش حد إلا لما يشوف توقيع المالك. بدل ما المالك يستلم كل طرد ويوصّله بنفسه للمخزن، هو بيكتب على ورقة: "احنا اتفقنا، خد الطرد من فلان، عبّيه في الرف رقم 7، الورقة دي صالحة 15 دقيقة بس". المستخدم بياخد الورقة (URL) ويوصّل الطرد للحارس مباشرةً. المالك ما لمسش الطرد ولا اتعب — هو بس وقّع.
التعريف العلمي: Pre-signed URL هو رابط HTTP عادي بس فيه query parameters محسوبة بـ HMAC-SHA256 توقيع رقمي مبني على Secret Key بتاعك (AWS Signature Version 4). الـ S3 لما يستلم طلب على الرابط ده، بيعيد حساب التوقيع بنفس الـ Secret Key اللي عنده، ولو طابق التوقيع اللي في الرابط ولسه ما انتهتش صلاحيته (الافتراضي 15 دقيقة)، يقبل الطلب. لو غيّرت أي حرف في الـ URL، التوقيع يفشل والـ S3 يردّ 403. المرجع الرسمي: docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html.
الفايدة الجوهرية إن السيرفر بتاعك ما بيشوفش الـ binary خالص. هو بس بيوقّع ورقة وبيرجّعها في 8ms.
الفرق في الأرقام — قبل وبعد
ده قياس فعلي على موقع e-learning بـ 500 فيديو رفع يومياً، متوسط حجم الفيديو 80MB:
- الطريقة التقليدية (multer): 40GB ingress يومياً + 40GB egress لـ S3 = 80GB حركة على السيرفر. تكلفة AWS Data Transfer Out: 40GB × 30 يوم × $0.09/GB ≈ $108 شهرياً. زمن الاستجابة P95 = 14.2 ثانية تحت 5 مستخدمين متزامنين.
- Pre-signed URLs: السيرفر بيمرّر JSON من 200 بايت بس. تكلفة Data Transfer من السيرفر = $0. تكلفة S3 PUT requests: 500/يوم × 30 = 15,000 طلب × $0.005/1000 = $0.075 شهرياً. زمن الاستجابة P95 على endpoint التوقيع = 38ms.
الفرق: 99.3% توفير في تكلفة الـ Data Transfer، و 374x تحسن في زمن الاستجابة. ملاحظة مهمة: العميل بيرفع لـ S3 مباشرةً فالـ bandwidth بتاعه ما تغيّرش، احنا بنتكلم عن سيرفر التطبيق فقط.
التطبيق الكامل في 60 سطر Node.js
الكود ده شغّال على Node.js 20+ مع @aws-sdk/client-s3 v3.600+ و Express 4. بنبني endpoint بيرجّع Pre-signed URL، والـ frontend بيستخدمه يرفع الملف بـ fetch مباشرةً.
// server.js — Pre-signed URL endpoint
import express from 'express';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'crypto';
const app = express();
app.use(express.json());
const s3 = new S3Client({
region: 'eu-central-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'video/mp4']);
const MAX_BYTES = 100 * 1024 * 1024; // 100MB
app.post('/api/upload-url', async (req, res) => {
const { filename, contentType, sizeBytes } = req.body;
if (!ALLOWED_TYPES.has(contentType)) {
return res.status(400).json({ error: 'unsupported content type' });
}
if (typeof sizeBytes !== 'number' || sizeBytes > MAX_BYTES) {
return res.status(400).json({ error: 'file too large' });
}
const key = `uploads/${new Date().toISOString().slice(0, 10)}/${randomUUID()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
ContentLength: sizeBytes,
});
const url = await getSignedUrl(s3, command, { expiresIn: 900 }); // 15 min
res.json({ url, key, publicUrl: `https://cdn.example.com/${key}` });
});
app.listen(3000);
على الـ frontend، الرفع بيبقى بـ fetch عادي — ولاحظ إنك بتبعت PUT مش POST:
// client.js — رفع الملف بـ fetch
async function uploadFile(file) {
const { url, key, publicUrl } = await fetch('/api/upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
sizeBytes: file.size,
}),
}).then(r => r.json());
const putRes = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
if (!putRes.ok) throw new Error('upload failed');
return publicUrl;
}
الفخ الأمني الأكبر — وكيف تجنبه
الغلطة اللي بشوفها في 70% من الـ implementations: السيرفر بيوقّع PutObjectCommand بدون ما يحدّد ContentType و ContentLength. ده معناه إن المستخدم يقدر يبعت أي حاجة على الـ URL — حتى لو وقّعت له على JPEG، هو يقدر يرفع ملف PHP أو executable باسم image.jpg.
الحل: اربط الـ ContentType في وقت التوقيع بالظبط، والـ ContentLength كمان. الـ S3 بيرفض أي طلب بـ Content-Type مختلف عن اللي وقّعت عليه. لاحظ في الكود فوق إن الـ command بيتم بناؤه بـ ContentType و ContentLength صريحين قبل ما يتوقّع — ده اللي بيحمي الـ bucket.
طبقات حماية إضافية لازم تضيفها قبل الإنتاج:
- Bucket Policy: امنع
s3:GetObjectالعام واستخدم CloudFront مع Origin Access Control. - Lifecycle Rule: اعمل rule يمسح أي object في
uploads/ما اتنقلش لمكان آخر بعد 24 ساعة (لتنظيف الرفعات المهجورة). - التحقق بعد الرفع: بعد ما العميل يرجّع نجاح، اعمل
HeadObjectمن السيرفر للتأكد من حجم الملف الفعلي ونوعه قبل ما تعتبره صالح. - صلاحية قصيرة: 15 دقيقة كحد أقصى. الـ AWS بيسمح لـ 7 أيام لكن أي مدة فوق ساعة فرصة هجوم بدون داعي.
الـ Trade-offs الحقيقية
أي حل فيه ثمن. اللي بتكسبه من Pre-signed URL واضح، اللي بتخسره لازم تعرفه:
- صعوبة الـ progress tracking: الرفع بيحصل بين العميل و S3 مباشرةً، فسيرفرك ما عندوش رؤية في تقدم الرفع. الحل: استخدم
XMLHttpRequestبدلfetchعلى الـ frontend عشانupload.onprogress. - Virus scanning بقى أصعب: ما بقاش عندك middleware بيمسح الملف وهو طايف. الحل: AWS Lambda trigger على الـ bucket بيشغّل ClamAV على كل ملف جديد، أو خدمة زي AWS GuardDuty Malware Protection.
- ما تقدرش تعدّل الملف server-side قبل التخزين: لو محتاج تعمل image resize أو تنزع EXIF metadata، لازم تعمله في Lambda بعد الرفع، مش في الـ middleware.
- تعقيد إضافي في الـ frontend: الـ flow بقى two-step بدل single endpoint. لازم frontend يفهم الفرق بين فشل التوقيع وفشل الرفع.
متى لا تستخدم Pre-signed URLs أصلاً
الطريقة دي مش حل لكل سيناريو. ابعد عنها لو:
- الملفات أصغر من 100KB ولها معالجة سيرفر فورية: رفع نص أو صورة avatar صغيرة من خلال السيرفر مباشرةً أبسط، والـ overhead بسيط. Pre-signed URL هنا تعقيد بدون فايدة.
- عندك compliance يفرض ملفات معينة تعدّي على gateway موحّد: بنوك أو healthcare ممكن يكون فيهم متطلب لـ scanning مركزي قبل التخزين. لو ده وضعك، Pre-signed مش متوافق مع الـ compliance.
- مش بتستخدم S3 ولا S3-compatible storage: لو على Azure Blob أو Google Cloud Storage، فيه مكافئ (SAS Tokens / Signed URLs) لكن الـ APIs مختلفة. الكود فوق مش هيشتغل عليهم زي ما هو.
- المستخدم في شبكة بتقفل CDN خارجي: بعض شبكات الشركات بتقفل أي حاجة مش
your-domain.com. هتحتاج proxy عبر سيرفرك في الحالة دي.
الخطوة التالية
افتح الـ S3 bucket بتاعك دلوقتي، اعمل IAM user جديد بصلاحية s3:PutObject فقط على prefix uploads/* (مش الـ bucket كامله). انسخ الكود فوق في endpoint جديد، جرّبه على ملف اختباري 50MB، وقيس الفرق في زمن الاستجابة على endpoint التوقيع. لو طلع أكتر من 100ms، المشكلة في الـ region — تأكد إن السيرفر والـ bucket في نفس الـ region.
المصادر
- توثيق AWS الرسمي عن Pre-signed URLs لرفع كائن:
docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html - توثيق
@aws-sdk/s3-request-presignerv3:docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-s3-request-presigner/ - مواصفات AWS Signature Version 4:
docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html - أسعار AWS S3 ونقل البيانات (Pricing):
aws.amazon.com/s3/pricing/ - أفضل ممارسات أمان S3 من AWS:
docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html - RFC 2104 لـ HMAC (الأساس الرياضي للتوقيع):
www.rfc-editor.org/rfc/rfc2104