إطار Next.js

رفع الملفات والتخزين

25 دقيقة الدرس 29 من 40

رفع الملفات والتخزين

التعامل مع رفع الملفات بشكل آمن وفعال أمر ضروري لتطبيقات الويب الحديثة. يغطي هذا الدرس معالجة رفع الملفات، وتكامل التخزين السحابي، ومعالجة الصور، وأفضل الممارسات لإدارة المحتوى المرفوع من المستخدمين.

رفع ملف أساسي باستخدام FormData

ابدأ بتنفيذ بسيط لرفع الملفات:

مكون نموذج الرفع

// components/UploadForm.tsx 'use client'; import { useState } from 'react'; export default function UploadForm() { const [file, setFile] = useState<File | null>(null); const [uploading, setUploading] = useState(false); const [message, setMessage] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { setMessage('يرجى تحديد ملف'); return; } setUploading(true); const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData, }); const result = await response.json(); if (response.ok) { setMessage('تم رفع الملف بنجاح!'); setFile(null); } else { setMessage(result.error || 'فشل الرفع'); } } catch (error) { setMessage('خطأ في الرفع'); } finally { setUploading(false); } }; return ( <form onSubmit={handleSubmit}> <input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)} disabled={uploading} /> <button type="submit" disabled={!file || uploading}> {uploading ? 'جاري الرفع...' : 'رفع'} </button> {message && <p>{message}</p>} </form> ); }

مسار API لرفع الملفات

// app/api/upload/route.ts import { writeFile } from 'fs/promises'; import { NextRequest } from 'next/server'; import path from 'path'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { return Response.json( { error: 'لم يتم توفير ملف' }, { status: 400 } ); } // التحقق من نوع الملف const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { return Response.json( { error: 'نوع ملف غير صالح' }, { status: 400 } ); } // التحقق من حجم الملف (5 ميجابايت كحد أقصى) const maxSize = 5 * 1024 * 1024; if (file.size > maxSize) { return Response.json( { error: 'الملف كبير جدًا' }, { status: 400 } ); } // إنشاء اسم ملف فريد const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const filename = `${Date.now()}-${file.name}`; const filepath = path.join(process.cwd(), 'public/uploads', filename); // حفظ الملف على القرص await writeFile(filepath, buffer); return Response.json({ success: true, filename, url: `/uploads/${filename}`, }); } catch (error) { console.error('خطأ في الرفع:', error); return Response.json( { error: 'فشل الرفع' }, { status: 500 } ); } }
تحذير أمني: تحقق دائمًا من أنواع الملفات والأحجام والمحتوى. لا تثق أبدًا بالتحقق من جانب العميل وحده. قم بتطهير أسماء الملفات لمنع هجمات اجتياز الدليل. قم بتخزين التحميلات خارج جذر الويب عندما يكون ذلك ممكنًا.

رفع ملفات متعددة

تعامل مع ملفات متعددة في وقت واحد:

// components/MultiUpload.tsx 'use client'; import { useState } from 'react'; export default function MultiUpload() { const [files, setFiles] = useState<FileList | null>(null); const [uploading, setUploading] = useState(false); const [results, setResults] = useState<any[]>([]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!files || files.length === 0) return; setUploading(true); const formData = new FormData(); // إلحاق جميع الملفات Array.from(files).forEach((file) => { formData.append('files', file); }); try { const response = await fetch('/api/upload-multiple', { method: 'POST', body: formData, }); const data = await response.json(); setResults(data.files); } catch (error) { console.error(error); } finally { setUploading(false); } }; return ( <form onSubmit={handleSubmit}> <input type="file" multiple onChange={(e) => setFiles(e.target.files)} /> <button type="submit" disabled={uploading}> رفع {files?.length || 0} ملفات </button> {results.length > 0 && ( <ul> {results.map((file, i) => ( <li key={i}> {file.filename}: {file.success ? '✓' : '✗'} </li> ))} </ul> )} </form> ); }

تكامل AWS S3

قم بتخزين الملفات في AWS S3 للتخزين السحابي القابل للتطوير:

إعداد AWS SDK

# تثبيت AWS SDK npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner # متغيرات البيئة AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_REGION=us-east-1 AWS_S3_BUCKET=your-bucket-name

أداة رفع S3

// lib/s3.ts import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const s3Client = new S3Client({ region: process.env.AWS_REGION!, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }); export async function uploadToS3( file: Buffer, filename: string, contentType: string ) { const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_BUCKET!, Key: filename, Body: file, ContentType: contentType, }); await s3Client.send(command); return `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${filename}`; } export async function getPresignedUrl(key: string, expiresIn = 3600) { const command = new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET!, Key: key, }); return await getSignedUrl(s3Client, command, { expiresIn }); }

مسار API لرفع S3

// app/api/upload-s3/route.ts import { NextRequest } from 'next/server'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { return Response.json({ error: 'لا يوجد ملف' }, { status: 400 }); } const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const filename = `${Date.now()}-${file.name}`; const url = await uploadToS3(buffer, filename, file.type); return Response.json({ success: true, url, filename, }); } catch (error) { console.error('خطأ في رفع S3:', error); return Response.json({ error: 'فشل الرفع' }, { status: 500 }); } }

تكامل Cloudinary

توفر Cloudinary استضافة الصور/الفيديو مع تحويلات مدمجة:

إعداد Cloudinary

# تثبيت Cloudinary SDK npm install cloudinary # متغيرات البيئة CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name

أداة رفع Cloudinary

// lib/cloudinary.ts import { v2 as cloudinary } from 'cloudinary'; cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, }); export async function uploadToCloudinary( fileBuffer: Buffer, folder: string = 'uploads' ) { return new Promise((resolve, reject) => { cloudinary.uploader .upload_stream( { folder, resource_type: 'auto', }, (error, result) => { if (error) reject(error); else resolve(result); } ) .end(fileBuffer); }); }

Cloudinary Upload API

// app/api/upload-cloudinary/route.ts import { NextRequest } from 'next/server'; import { uploadToCloudinary } from '@/lib/cloudinary'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const result = await uploadToCloudinary(buffer, 'user-uploads'); return Response.json({ success: true, url: result.secure_url, publicId: result.public_id, }); } catch (error) { return Response.json({ error: 'فشل الرفع' }, { status: 500 }); } }

معالجة الصور باستخدام Sharp

قم بتحسين وتحويل الصور من جانب الخادم:

# تثبيت Sharp npm install sharp // lib/image-processing.ts import sharp from 'sharp'; export async function processImage( buffer: Buffer, options: { width?: number; height?: number; quality?: number; format?: 'jpeg' | 'png' | 'webp'; } ) { let image = sharp(buffer); // تغيير الحجم إذا تم توفير الأبعاد if (options.width || options.height) { image = image.resize(options.width, options.height, { fit: 'cover', position: 'center', }); } // تحويل التنسيق const format = options.format || 'webp'; const quality = options.quality || 80; if (format === 'jpeg') { image = image.jpeg({ quality }); } else if (format === 'png') { image = image.png({ quality }); } else { image = image.webp({ quality }); } return await image.toBuffer(); } export async function createThumbnail(buffer: Buffer, size: number = 200) { return await sharp(buffer) .resize(size, size, { fit: 'cover' }) .webp({ quality: 80 }) .toBuffer(); }

مسار API مع معالجة الصور

// app/api/upload-optimized/route.ts import { NextRequest } from 'next/server'; import { processImage, createThumbnail } from '@/lib/image-processing'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // إنشاء نسخة محسّنة const optimized = await processImage(buffer, { width: 1200, quality: 85, format: 'webp', }); // إنشاء صورة مصغرة const thumbnail = await createThumbnail(buffer, 300); // رفع كلاهما إلى S3 const filename = `${Date.now()}-${file.name}`; const [mainUrl, thumbUrl] = await Promise.all([ uploadToS3(optimized, `images/${filename}.webp', 'image/webp'), uploadToS3(thumbnail, `thumbnails/${filename}.webp', 'image/webp'), ]); return Response.json({ success: true, urls: { main: mainUrl, thumbnail: thumbUrl, }, }); } catch (error) { return Response.json({ error: 'فشلت المعالجة' }, { status: 500 }); } }

رفع السحب والإفلات

عزز تجربة المستخدم بوظيفة السحب والإفلات:

// components/DragDropUpload.tsx 'use client'; import { useState, DragEvent } from 'react'; export default function DragDropUpload() { const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState<File[]>([]); const handleDrag = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDragIn = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragOut = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); setFiles((prev) => [...prev, ...droppedFiles]); }; return ( <div onDragEnter={handleDragIn} onDragLeave={handleDragOut} onDragOver={handleDrag} onDrop={handleDrop} style={{ border: `2px dashed ${isDragging ? '#4f46e5' : '#ccc'}`, padding: '40px', textAlign: 'center', background: isDragging ? '#f0f0f0' : 'white', }} > {files.length === 0 ? ( <p>اسحب وأفلت الملفات هنا، أو انقر لتحديدها</p> ) : ( <ul> {files.map((file, i) => ( <li key={i}>{file.name}</li> ))} </ul> )} <input type="file" multiple onChange={(e) => { if (e.target.files) { setFiles(Array.from(e.target.files)); } }} style={{ display: 'none' }} id="file-input" /> <label htmlFor="file-input"> <button type="button">تحديد الملفات</button> </label> </div> ); }

تتبع تقدم الرفع

أظهر تقدم الرفع للمستخدمين:

// components/UploadWithProgress.tsx 'use client'; import { useState } from 'react'; export default function UploadWithProgress() { const [progress, setProgress] = useState(0); const [uploading, setUploading] = useState(false); const handleUpload = async (file: File) => { setUploading(true); setProgress(0); const xhr = new XMLHttpRequest(); // تتبع تقدم الرفع xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; setProgress(Math.round(percentComplete)); } }); xhr.addEventListener('load', () => { setUploading(false); if (xhr.status === 200) { console.log('اكتمل الرفع'); } }); xhr.open('POST', '/api/upload'); const formData = new FormData(); formData.append('file', file); xhr.send(formData); }; return ( <div> <input type="file" onChange={(e) => { const file = e.target.files?.[0]; if (file) handleUpload(file); }} /> {uploading && ( <div> <div style={{ width: '100%', height: '20px', background: '#f0f0f0', borderRadius: '10px', overflow: 'hidden', }}> <div style={{ width: `${progress}%`, height: '100%', background: '#4f46e5', transition: 'width 0.3s', }} /> </div> <p>تم رفع {progress}%</p> </div> )} </div> ); }

الرفع المباشر إلى S3 باستخدام عناوين URL الموقعة مسبقًا

اسمح بعمليات الرفع من جانب العميل مباشرة إلى S3 للحصول على أداء أفضل:

// app/api/presigned-url/route.ts import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const s3Client = new S3Client({ region: process.env.AWS_REGION!, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }); export async function POST(request: Request) { const { filename, contentType } = await request.json(); const key = `uploads/${Date.now()}-${filename}`; const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_BUCKET!, Key: key, ContentType: contentType, }); const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600, }); return Response.json({ uploadUrl: presignedUrl, key, }); } // مكون العميل // components/DirectS3Upload.tsx const handleUpload = async (file: File) => { // الحصول على عنوان URL الموقع مسبقًا const response = await fetch('/api/presigned-url', { method: 'POST', body: JSON.stringify({ filename: file.name, contentType: file.type, }), }); const { uploadUrl, key } = await response.json(); // الرفع مباشرة إلى S3 await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type, }, }); console.log('تم الرفع إلى S3:', key); };
أفضل الممارسات: تحقق دائمًا من أنواع الملفات والأحجام من جانب الخادم. استخدم عناوين URL الموقعة مسبقًا لعمليات الرفع المباشرة لتقليل حمل الخادم. قم بإنشاء صور مصغرة بشكل غير متزامن. قم بتخزين بيانات تعريف الملف في قاعدة البيانات الخاصة بك. قم بتنفيذ فحص الفيروسات لتحميلات المستخدم.

تمرين: إنشاء نظام رفع ملفات

  1. قم بإنشاء نموذج رفع ملفات متعددة مع دعم السحب والإفلات
  2. قم بتنفيذ التحقق من جانب الخادم (النوع، الحجم، المحتوى)
  3. قم بدمج AWS S3 أو Cloudinary للتخزين
  4. معالجة الصور باستخدام Sharp (تغيير الحجم، التحسين، إنشاء صور مصغرة)
  5. أضف مؤشرات تقدم الرفع
  6. قم بتنفيذ عمليات الرفع المباشرة إلى S3 باستخدام عناوين URL الموقعة مسبقًا
  7. قم بإنشاء معرض ملفات يعرض الصور المرفوعة
  8. أضف وظيفة حذف الملفات مع التأكيد