הטמעת העלאת קבצים מרובה: React, Laravel, S3 Turnkey

משתמשים נוטשים העלאות כשהדפדפן קופא על עשרות קבצים ללא התקדמות. אנחנו בונים העלאות מרובות קבצים עם React ו-Laravel, הכוללות שליחה מקבילה וממשק ברור. הצוות שלנו מספק את הפרויקט במפתח מלא—מביקורת ועד תמיכה—ומבטיח מערכת אמינה וניתנת להרחבה.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
הטמעת העלאת קבצים מרובה: React, Laravel, S3 Turnkey
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1504
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

תארו לעצמכם משתמש באתר מסחר אלקטרוני שמנסה להעלות 50 תמונות מוצר—הדפדפן נתקע, הקבצים נשלחים אחד אחד, ולא מוצג שום התקדמות. הלקוח עוזב למתחרה. אנחנו פותרים את זה עם העלאה מרובת קבצים הכוללת שליחה מקבילה והתקדמות ברורה. מערכת זו נקראת העלאה מרובה—העלאת קבצים המונית לאתר. למהנדסים שלנו יש ניסיון של 10+ שנים בתכנון מערכות כאלה, מטפסות פשוטות ועד ספריות מדיה בעומס גבוה.

למה זה מורכב?

יישום לקוי מוביל לזמן קצוב שחלף, כפילויות ואובדן נתונים. בעיות נפוצות: חוסר אימות בצד הלקוח (משתמש מעלה וידאו של 2 GB), בקשות N+1 לשרת, חסימת ממשק המשתמש במהלך ההעלאה, ופגיעויות (העלאת PHP shell). אנחנו משתמשים בתבניות מוכחות: תור עם מגבלת בקשות מקבילות (מקביליות 3), ניסיונות חוזרים בשגיאות, ואימות דו-שלבי.

אימות בצד הלקוח ובצד השרת

בצד הלקוח אנחנו בודקים גודל וסוג MIME באמצעות מאפייני accept ו-max—אבל זו רק השורה הראשונה. אימות בצד השרת הוא חובה: בדיקת MIME לפי תוכן, סריקת קוד זדוני, הגבלת מספר קבצים. בפרויקט אחד גילינו שרכיב העלאה של React מצד שלישי אפשר קבצים עם סיומות כפולות—לאחר יישום אימות בצד השרת, ההתקפות נפסקו.

איך רכיב ההעלאה של React עובד?

בליבו נמצא תור קבצים עם סטטוס אישי לכל אחד. מאפיין multiple על input[type=file] מאפשר בחירת מספר קבצים; מסנן accept הוא שורה ראשונה אבל לא אבטחה. אימות לקוח בודק גודל (מקסימום 10 MB) ומספר. העלאה מקבילה עם מגבלה: שלושה קבצים בו-זמנית, האחרים ממתינים. התקדמות לכל קובץ מנוטרת באמצעות XMLHttpRequest. הנה דוגמה לרכיב:

// components/BulkUpload.tsx
import { useRef, useState, useCallback } from 'react'

interface UploadFile {
  id: string
  file: File
  status: 'pending' | 'uploading' | 'done' | 'error'
  progress: number
  error?: string
  url?: string // URL after upload
}

interface BulkUploadProps {
  accept?: string
  maxFiles?: number
  maxSizeBytes?: number
  onUploadComplete?: (urls: string[]) => void
}

export function BulkUpload({
  accept = 'image/*',
  maxFiles = 10,
  maxSizeBytes = 10 * 1024 * 1024, // 10 MB
  onUploadComplete,
}: BulkUploadProps) {
  const inputRef = useRef<HTMLInputElement>(null)
  const [files, setFiles] = useState<UploadFile[]>([])

  const addFiles = useCallback((incoming: FileList | File[]) => {
    const arr = Array.from(incoming)
    const validated = arr
      .filter(f => {
        if (f.size > maxSizeBytes) {
          alert(`${f.name}: file too large (max ${maxSizeBytes / 1024 / 1024} MB)`)
          return false
        }
        return true
      })
      .slice(0, maxFiles - files.length)

    setFiles(prev => [
      ...prev,
      ...validated.map(file => ({
        id: crypto.randomUUID(),
        file,
        status: 'pending' as const,
        progress: 0,
      })),
    ])
  }, [files.length, maxFiles, maxSizeBytes])

  const uploadFile = useCallback(async (uploadFile: UploadFile) => {
    const formData = new FormData()
    formData.append('file', uploadFile.file)

    setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'uploading' } : f ))

    try {
      await new Promise<void>((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.open('POST', '/api/upload')
        xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector<HTMLMetaElement>('meta[name=csrf-token]')?.content ?? '')

        xhr.upload.onprogress = (e) => {
          if (e.lengthComputable) {
            const progress = Math.round((e.loaded / e.total) * 100)
            setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f ))
          }
        }

        xhr.onload = () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            const { url } = JSON.parse(xhr.responseText)
            setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'done', progress: 100, url } : f ))
            resolve()
          } else {
            reject(new Error(`HTTP ${xhr.status}`))
          }
        }

        xhr.onerror = () => reject(new Error('Network error'))
        xhr.send(formData)
      })
    } catch (e) {
      setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'error', error: (e as Error).message } : f ))
    }
  }, [])

  const uploadAll = useCallback(async () => {
    const pending = files.filter(f => f.status === 'pending')
    // Parallel, but max 3 at a time
    const CONCURRENCY = 3
    for (let i = 0; i < pending.length; i += CONCURRENCY) {
      await Promise.all(pending.slice(i, i + CONCURRENCY).map(uploadFile))
    }
    const urls = files.filter(f => f.status === 'done' && f.url).map(f => f.url!)
    onUploadComplete?.(urls)
  }, [files, uploadFile, onUploadComplete])

  const removeFile = (id: string) => {
    setFiles(prev => prev.filter(f => f.id !== id))
  }

  return (
    <div>
      <button onClick={() => inputRef.current?.click()}>
        Choose files
      </button>
      <input
        ref={inputRef}
        type="file"
        multiple
        accept={accept}
        className="hidden"
        onChange={e => e.target.files && addFiles(e.target.files)}
      />
      {files.length > 0 && (
        <ul className="mt-4 space-y-2">
          {files.map(f => (
            <li key={f.id} className="flex items-center gap-3">
              <span className="truncate flex-1">{f.file.name}</span>
              <span className="text-sm text-muted">
                {(f.file.size / 1024).toFixed(1)} KB
              </span>
              {f.status === 'uploading' && (
                <div className="w-24 bg-gray-200 rounded-full h-2">
                  <div
                    className="bg-blue-500 h-2 rounded-full transition-all"
                    style={{ width: `${f.progress}%` }}
                  />
                </div>
              )}
              {f.status === 'done' && <span className="text-green-600">✓</span>}
              {f.status === 'error' && (
                <span className="text-red-600 text-sm">{f.error}</span>
              )}
              <button
                onClick={() => removeFile(f.id)}
                disabled={f.status === 'uploading'}
                aria-label={`Remove ${f.file.name}`}
              >
                ✕
              </button>
            </li>
          ))}
        </ul>
      )}
      {files.some(f => f.status === 'pending') && (
        <button onClick={uploadAll} className="mt-4 btn-primary">
          Upload {files.filter(f => f.status === 'pending').length} files
        </button>
      )}
    </div>
  )
}

למה כתובות Presigned מהירות פי 5?

כשמעלים דרך השרת (multipart), כל קובץ עובר דרך PHP/Laravel—עם 50 קבצים של 10 MB כל אחד, השרת הופך לצוואר בקבוק. כתובות Presigned מאפשרות ללקוח להעלות ישירות ל-S3, תוך עקיפת השרת. זה מפחית עומס ומאיץ העלאות עד פי 5. בפרויקט אחד, הלקוח חסך 2500 BYN חודשיים על תעבורה באמצעות טכניקה זו.

מאפיין Multipart דרך השרת כתובת Presigned (העלאה ישירה)
עומס על השרת גבוה (100% תעבורה) מינימלי (רק יצירת כתובת)
תפוקה מוגבלת על ידי השרת מוגבלת רק על ידי ערוץ הלקוח
העלאת 50 קבצים של 10 MB כ-2 דקות כ-25 שניות
אבטחה דורש אימות שרת אימות לקוח + שרת

איך ליישם כתובות Presigned בצד השרת?

  1. הלקוח מבקש כתובת Presigned, ומספק שם קובץ, סוג MIME וגודל.
  2. השרת יוצר כתובת מוגבלת בזמן (15 דקות) ומחזיר אותה.
  3. הלקוח מעלה קובץ ישירות ל-S3 באמצעות בקשת PUT.
  4. לאחר ההעלאה, ניתן לבצע עיבוד נוסף (למשל, המרה ל-WebP).

יצירת כתובות Presigned היא כמה שורות ב-Laravel:

// Генерация presigned URL
public function presign(Request $request): JsonResponse
{
    $request->validate([
        'filename' => 'required|string|max:255',
        'mime_type' => 'required|string|in:image/jpeg,image/png,image/webp,application/pdf',
        'size' => 'required|integer|max:' . 10 * 1024 * 1024,
    ]);

    $ext = pathinfo($request->filename, PATHINFO_EXTENSION);
    $key = 'uploads/' . date('Y/m') . '/' . Str::uuid() . '.' . $ext;

    $client = Storage::disk('s3')->getClient();
    $cmd = $client->getCommand('PutObject', [
        'Bucket' => config('filesystems.disks.s3.bucket'),
        'Key' => $key,
        'ContentType' => $request->mime_type,
        'ACL' => 'private',
    ]);

    $presignedUrl = (string) $client->createPresignedRequest($cmd, '+15 minutes')->getUri();

    return response()->json([
        'upload_url' => $presignedUrl,
        'public_url' => Storage::url($key),
        'key' => $key,
    ]);
}

בצד הפרונטאנד, ההעלאה ל-S3 נראית כך:

// Фронтенд: загрузка напрямую в S3
async function uploadToS3(file: File): Promise<string> {
  // 1. Получить presigned URL от нашего сервера
  const { upload_url, public_url } = await fetch('/api/upload/presign', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      filename: file.name,
      mime_type: file.type,
      size: file.size,
    }),
  }).then(r => r.json())

  // 2. Загрузить файл напрямую в S3
  await fetch(upload_url, {
    method: 'PUT',
    body: file,
    headers: {
      'Content-Type': file.type,
    },
  })

  return public_url
}

איך להגדיר תור עם לוגיקת ניסיון חוזר?

לעמידות בפני שגיאות רשת, אנחנו מיישמים לוגיקת ניסיון חוזר. כל קובץ יכול לנסות שוב עד 3 פעמים עם השהיה אקספוננציאלית. זה חוסך זמן למשתמש ומפחית העלאות שנכשלו. אנחנו משתמשים ב-Laravel Queues עם הגדרות ניסיון חוזר: עד 3 ניסיונות עם השהיות של 1, 5, 15 שניות.

פרטי לוגיקת הניסיון החוזר

ברכיב React, כל העלאה שנכשלה מגדירה את סטטוס הקובץ ל-'error'. המשתמש יכול ללחוץ על כפתור ניסיון חוזר. בצד השרת, אנחנו יכולים להגדיר תור משימות לעיבוד נוסף—אם משימה נכשלת, היא נדחפת חזרה אוטומטית. זה מטפל בכשלים זמניים מ-S3 או מהרשת.

מה כולל היישום המפתח-ביד?

  • רכיב React עם תור, התקדמות ותמיכה בביטול.
  • API בצד השרת על Laravel 11 עם אימות, עיבוד תמונות (WebP), ואינטגרציית S3.
  • יצירת כתובות Presigned אוטומטית להעלאה ישירה.
  • טיפול בשגיאות עם ניסיון חוזר (עד 3 פעמים).
  • תיעוד API ורכיב.
  • ניסיון של 10+ שנים ו-50+ פרויקטי אינטגרציית העלאת קבצים מבטיחים יציבות.

לוח זמנים

שלב משך
רכיב React + תור + התקדמות יומיים
נקודת קצה בשרת על Laravel יום אחד
אינטגרציית כתובות Presigned של S3 יום אחד
המרת WebP, הגבלת קצב, ניסיון חוזר יום אחד
סה"כ (מפתח-ביד) 2–4 ימים

צרו קשר להערכה של הפרויקט שלכם. הזמינו יישום מפתח-ביד וקבלו מערכת העלאה מרובה יציבה תוך 2–4 ימים. אתם יכולים גם לקבל ייעוץ לפרויקט שלכם.