أين تخزن ملفات المستخدمين؟ استراتيجية الرفع والتخزين لتطبيقات الويب

احفظ الملفات خارج الكود والمجلد العام، وابدأ بالقرص المحلي حتى نحو 200GB ثم انتقل إلى تخزين متوافق مع S3 على سيرفر سعودي. الملفات الخاصة بروابط موقعة تنتهي خلال دقائق، والنسخ الاحتياطي للملفات منفصل عن الكود.
لأغلب تطبيقات الأعمال الجواب كالتالي: احفظ ملفات المستخدمين خارج مجلد الكود وخارج المجلد العام للموقع، وابدأ بقرص SSD المحلي ما دام إجمالي التخزين أقل من نحو 200GB وعندك سيرفر تطبيق واحد، ثم انتقل إلى تخزين كائنات متوافق مع S3 مثل MinIO على سيرفر سعودي لما تضيف سيرفر تطبيق ثانياً، أو تحتاج قواعد حذف تلقائية، أو تبي الملفات تكبر بمعزل عن المعالجة. الملفات الخاصة تُقدَّم عبر روابط موقعة مؤقتة تنتهي من 5 إلى 15 دقيقة، ولا تُقدَّم أبداً عبر مسار عام يمكن تخمينه. كل ملف مرفوع يُفحص بنوع محتواه الحقيقي لا بامتداده، ويُعاد تسميته بمعرّف عشوائي، وتتوحد حدود الرفع بين PHP وNginx حتى لا يواجه المستخدم خطأ 413 محيّراً. أخيراً، للملفات نسخ احتياطي مستقل بجدوله الخاص، منفصل عن الكود الموجود أصلاً في Git، ومنفصل عن قاعدة البيانات اللي تحتاج تفريغاً متسقاً. إذا استقبل تطبيقك 800 ملف يومياً بمتوسط 400KB فهذا نحو 117GB في السنة، وكلفتها على سيرفر كلاود قرابة 1,000 ريال سنوياً من مساحة SSD الإضافية. الأقسام التالية تشرح كل قرار مع إعدادات جاهزة للنسخ، والأرقام اللي تخطط بها التخزين قبل ما يمتلئ القرص الساعة 2 الفجر.
القرص المحلي أم تخزين الكائنات
القرص المحلي أبسط حل يشتغل. قرص local في Laravel أو مجلد عادي مثل /srv/storage/uploads يعطيك قراءة بأجزاء من الملي ثانية، بلا قفزات شبكة ولا خدمة إضافية تراقبها، لكنه يتعثر في ثلاث حالات: لما يحتاج سيرفر تطبيق ثانٍ نفس الملفات، ولما تبي قواعد مثل «احذف ملفات التصدير المؤقتة بعد 7 أيام» تديرها طبقة التخزين نفسها، ولما تنمو الملفات أسرع بكثير من بقية التطبيق.
تخزين الكائنات يحل هذه المشاكل بوضع الملفات خلف واجهة HTTP، وتشغيله بنفسك على سيرفر داخل المملكة يبقي البيانات تحت الولاية السعودية، وهذا مهم للجهات المرتبطة بمنصات حكومية أو اللي تحفظ بيانات شخصية تحت نظام حماية البيانات الشخصية.
| المعيار | SSD محلي | S3 مستضاف ذاتياً (MinIO أو مثيله) |
|---|---|---|
| وقت الإعداد | دقائق | نحو ساعة مع التشفير |
| عدة سيرفرات تطبيق | يحتاج NFS أو مزامنة | مدعوم أصلاً |
| الروابط الموقعة | عبر التطبيق أو X-Accel-Redirect | روابط presigned مدمجة |
| دورة الحياة والإصدارات | سكربتات cron | سياسات على مستوى الحاوية |
| زمن القراءة | أقل من 1ms | من 2 إلى 5ms على نفس الشبكة |
الطريق العملي أن تكتب كودك فوق طبقة تخزين مجرّدة من اليوم الأول، ففي Laravel استخدم دائماً Storage::disk('uploads') ولا تكتب file_put_contents بمسار ثابت. بهذا يصير الانتقال لاحقاً تعديلاً في الإعدادات مع نسخ لمرة واحدة بأمر rclone sync.
شكل الإعداد في Laravel
توجيه Laravel إلى نقطة S3 مستضافة ذاتياً يحتاج أربعة متغيرات بيئة وخياراً واحداً ينساه كثيرون:
AWS_ACCESS_KEY_ID=app-uploads
AWS_SECRET_ACCESS_KEY=change-me-40-chars
AWS_BUCKET=uploads
AWS_ENDPOINT=https://s3.internal.example.sa
AWS_USE_PATH_STYLE_ENDPOINT=true
بدون AWS_USE_PATH_STYLE_ENDPOINT=true تحاول المكتبة الوصول عبر نطاق فرعي باسم الحاوية مثل uploads.s3.internal.example.sa، والسيرفر المفرد ما يرد عليه أصلاً. أعطِ التطبيق مفتاح وصول خاصاً به محصوراً في حاوية واحدة، ولا تستخدم أبداً بيانات الدخول الرئيسية لسيرفر التخزين، لأن تسرّب مفتاح التطبيق حينها يكشف حاوية واحدة لا كل ما عندك.
حدود رفع متفقة مع بعضها
أشهر مشكلة في الرفع ثلاث طبقات بثلاثة حدود مختلفة: Nginx يرفض الطلب بخطأ 413، أو PHP يُسقط الملف بصمت، أو يعترض المدقق في إطار العمل آخر شيء. اضبطها معاً، واجعل حد Nginx أعلى قليلاً من PHP حتى يرجع PHP رسالة مفهومة:
# /etc/nginx/conf.d/uploads.conf
client_max_body_size 25m;
client_body_timeout 120s;
; php.ini or the pool config
upload_max_filesize = 20M
post_max_size = 22M
max_execution_time = 120
max_file_uploads = 10
بعدها كرر نفس الـ20MB في قاعدة التحقق، مثل 'file' => 'required|file|max:20480|mimes:pdf,jpg,png'، واكتب الحد في الواجهة نفسها، فعبارة «حتى 20MB، PDF أو صورة» قبل اختيار الملف تقلل تذاكر الدعم بشكل ملحوظ.
للملفات الأكبر من 100MB تجاوز سيرفر التطبيق كلياً: ولّد رابط PUT موقعاً وخلِّ المتصفح يرفع مباشرة إلى مخزن الكائنات، فتبقى عمّال PHP فاضية ولا يتعلق طلب 3 دقائق على اتصال بطيء.
الملفات الخاصة والروابط الموقعة
العقود والفواتير وصور الهويات والتقارير الطبية مكانها ليس public/. احفظها في موقع خاص وأعطِ روابط تنتهي صلاحيتها، ومع تخزين متوافق مع S3 يسويها Laravel بسطر واحد:
$url = Storage::disk('s3')->temporaryUrl(
$document->path, now()->addMinutes(10)
);
أما على القرص المحلي فتحقق من الصلاحية في الـcontroller، ثم خلِّ Nginx يبث الملف عبر موقع داخلي حتى لا يحمل PHP الملف في الذاكرة:
location /protected/ {
internal;
alias /srv/storage/private/;
}
الـcontroller يتحقق من الصلاحيات ويرد بالترويسة X-Accel-Redirect: /protected/2026/10/a8f3c1.pdf، فيقدّم Nginx الملف بسرعة القرص الكاملة بينما يبقى منطق الصلاحيات داخل تطبيقك. خلِّ مدة الصلاحية قصيرة، فعشر دقائق تكفي لفتح المستند، والرابط اللي ينتشر في قروب واتساب يتوقف قبل أن يتوسع.
افحص ما يرفعه المستخدمون فعلاً
اعتبر كل ملف مرفوع عدائياً حتى يثبت العكس، فملف اسمه invoice.pdf ممكن يكون سكربت PHP، وملف SVG ممكن يحمل JavaScript يشتغل داخل نطاقك. قائمة فحص سليمة:
- حدد النوع من المحتوى عبر
finfoأو قاعدةmimetypesفي إطار العمل، لا من الامتداد اللي أرسله المتصفح. - أعد تسمية كل ملف بمعرّف عشوائي مثل UUID، واحفظ الاسم الأصلي في قاعدة البيانات فقط.
- لا تحفظ الملفات المرفوعة في مجلد يقدر PHP ينفذ فيه، ولو اضطررت أضف
location ~ ^/uploads/.*\.php$ { deny all; }. - امنع SVG وHTML في محتوى المستخدمين ما لم تنظفها، وقدّم التنزيلات بترويسة
Content-Disposition: attachment. - افحص بـClamAV في التطبيقات اللي تعتمد على المستندات، فأمر
clamdscanيفحص PDF بحجم 5MB في حدود 50 إلى 150ms والخدمة شغالة، ويحتاج نحو 1 إلى 1.5GB من الذاكرة لقاعدة التواقيع. - احذف بيانات EXIF من الصور، لأنها قد تتضمن إحداثيات GPS للمكان اللي التقطت فيه الصورة.
خطط لنمو التخزين بالسنة
مشاكل التخزين نادراً ما تكون مفاجئة، فهي خط مستقيم ما أحد رسمه. خذ أسبوعاً من الأرقام الحقيقية: عدد الملفات في اليوم ومتوسط حجمها، ثم اضربها وامددها على 12 شهراً، وأضف 30% للصور المصغرة والإصدارات والملفات المؤقتة.
| ملفات يومياً | متوسط الحجم | النمو في السنة | مع 30% إضافية | كلفة SSD الإضافية سنوياً |
|---|---|---|---|---|
| 200 | 250KB | 18GB | 24GB | تكفيها المساحة الأساسية |
| 800 | 400KB | 117GB | 152GB | 2 × 500 = 1,000 ريال |
| 3,000 | 1.2MB | 1.3TB | 1.7TB | 17 × 500 = 8,500 ريال |
على الخادم السحابي من كلاود كل كتلة +100GB SSD سعرها 500 ريال سنوياً شامل الضريبة، فتشتري السعة بالحجم اللي تحتاجه فعلاً. اضبط تنبيهاً عند 75% من القرص وبيكون عندك أسابيع من المهلة لا ساعات. إذا كانت التنزيلات كثيرة راقب نقل البيانات أيضاً، فخمسون ألف تنزيل في السنة لملف بحجم 2MB تساوي 100GB فقط، لكن ملف تثبيت شائع بحجم 200MB يأكل تيرابايت بسرعة، وكل 1TB نقل إضافي بـ1,150 ريالاً سنوياً.
انسخ الملفات احتياطياً بمعزل عن الكود
الكود وقاعدة البيانات والملفات لكل منها شكل مختلف، فيحتاج كل واحد استراتيجية نسخ خاصة:
- الكود موجود أصلاً في Git، ونسخ
vendor/أوnode_modules/يهدر المساحة ويبطئ الاسترجاع. - قاعدة البيانات تحتاج تفريغاً متسقاً، مثل
mysqldump --single-transactionكل ليلة مع السجلات الثنائية إذا احتجت الاسترجاع للدقيقة. - الملفات تنمو بالإضافة فقط، وهذا يخليها مثالية لأدوات النسخ التزايدي مثل
restic، فبعد أول تشغيل كامل ينتهي النسخ الليلي لـ150GB تغيّر منها 400MB خلال دقائق.
خلِّ النسخة على جهاز غير الجهاز اللي تحميه، وجرّب الاسترجاع كل ربع سنة وسجّل كم أخذ من الوقت. إضافة النسخ الاحتياطي اليومي باحتفاظ 7 أيام سعرها 800 ريال سنوياً، ونسخة الثلاثين يوماً بـ2,000 ريال، بينما باقات الاستضافة السحابية المُدارة تتضمن نسخاً يومياً وأسبوعياً وباحتفاظ أطول بدون كلفة إضافية.
قرار تقدر تتخذه هذا الأسبوع
- انقل الملفات المرفوعة خارج المجلد العام وخلف قرص تخزين مسمّى.
- اجعل حدود Nginx وPHP والتحقق متطابقة، ثم اعرض الحد في نموذج الرفع.
- قدّم الملفات الخاصة فقط عبر روابط موقعة أو تحويل داخلي ينتهي خلال 15 دقيقة.
- أضف فحص النوع من المحتوى وأسماء عشوائية، مع ClamAV إذا كان المستخدمون يرفعون مستندات.
- قِس أسبوعاً من الرفع، وامدده على 12 شهراً، واحسب حجم SSD بالجدول أعلاه.
- انسخ الملفات بأداة تزايدية بجدولها الخاص، وجرّب استرجاعاً فعلياً.
لما تقول الأرقام إنك تجاوزت القرص الواحد، راجع الباقات والإضافات وأضف سيرفر تخزين مخصصاً بجوار تطبيقك في نفس مركز بيانات الرياض، على بعد نحو 8ms من مستخدميك.


