Как загружать большие файлы в Laravel

При стандартной загрузке больших файлов через multipart/form-data в Laravel разработчики часто сталкиваются с ограничениями PHP: тайм-ауты, лимиты памяти и обрывы соединения превращают попытки загрузить видео или гигабайтные архивы в настоящую боль.

Решение, которое давно используется в крупных сервисах - это разбиение файла на небольшие части (чанки). Это позволяет:

  • обходить серверные ограничения;

  • при обрыве соединения возобновлять загрузку с места остановки;

  • не перегружать память сервера, обрабатывая по одному небольшой части за раз.

Общая стратегия

Чтобы реализовать такую схему, нужно, чтобы бэкенд умел:

  1. Уникально идентифицировать сессию загрузки;

  2. Определять, какие части уже получены;

  3. Сохранять чанки во временном хранилище;

  4. Собирать файл после получения всех частей.

Как устроен контроллер Laravel

Нам нужен контроллер, который умеет как проверять статус загрузки, так и принимать новые части.

Проверка статуса

Этот метод возвращает список уже загруженных чанков по upload_id.

public function checkStatus(Request $request)
{
    $uploadId = $request->get('upload_id');
    $path = "chunks/{$uploadId}";

    $existingChunks = Storage::exists($path) 
        ? collect(Storage::files($path))->map(fn($f) => (int) basename($f, '.part'))->values()
        : [];

    return response()->json($existingChunks);
}

Приём части

Здесь мы сохраняем часть и проверяем, все ли чанки получены. Если да, то запускаем сборку.

public function uploadChunk(Request $request)
{
    $file = $request->file('file');
    $uploadId = $request->upload_id;
    $index = $request->chunk_index;
    $total = $request->total_chunks;

    $path = "chunks/{$uploadId}";
    
    Storage::putFileAs($path, $file, "{$index}.part");

    if (count(Storage::files($path)) === (int) $total) {
        return $this->mergeChunks($path, $request->filename, $total);
    }

    return response()->json(['status' => 'chunk_received']);
}

Сборка файла из чанков

Чтобы не загружать весь файл в память, мы используем построчное стриминг-считывание частей:

protected function mergeChunks($chunkPath, $originalName, $total)
{
    $finalPath = storage_path("app/public/uploads/{$originalName}");
    
    $out = fopen($finalPath, "wb");

    for ($i = 0; $i < $total; $i++) {
        $chunkFile = storage_path("app/chunks/{$chunkPath}/{$i}.part");
        $in = fopen($chunkFile, "rb");
        
        stream_copy_to_stream($in, $out);
        
        fclose($in);
    }

    fclose($out);

    Storage::deleteDirectory($chunkPath);

    return response()->json([
        'status' => 'complete',
        'path' => asset("storage/uploads/{$originalName}")
    ]);
}

Такой подход сохраняет оперативную память и делает возможным сборку файлов любых размеров.

Клиентская часть

На стороне браузера вам нужна функция, которая:

  1. Делит файл на чанки;

  2. Спрашивает сервер, какие части уже загружены;

  3. Загружает только отсутствующие;

Пример на JavaScript:

async function uploadLargeFile(file) {
    const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
    const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
    const uploadId = btoa(file.name + file.size);

    const response = await fetch(`/api/upload/status?upload_id=${uploadId}`);
    const uploadedChunks = await response.json();

    for (let i = 0; i < totalChunks; i++) {
        if (uploadedChunks.includes(i)) continue;

        const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);

        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('upload_id', uploadId);
        formData.append('chunk_index', i);
        formData.append('total_chunks', totalChunks);
        formData.append('filename', file.name);

        await fetch('/api/upload/chunk', { method: 'POST', body: formData });
    }
}

Советы для продакшена

Защита от двойной записи

При сборке файла используйте файловую блокировку (flock), чтобы избежать конфликта, если пользователь случайно запустит загрузку дважды.

Удаление заброшенных частей

Создайте планировщик (cron) или Laravel-команду, которая очищает старые директории чанков (например, старше 24 часов).

Переход от одной монолитной загрузки к chunked upload делает вашу систему гораздо устойчивее к сбоям, снимает ограничения upload_max_filesize и post_max_size на сервере и позволяет обрабатывать загрузки большого размера даже в условиях нестабильного соединения.

Похожие статьи

Рекомендательные технологии Подробнее
Разработка и инструменты • 5 месяцев назад

Правила rewrite URL в NGINX

Разбор директив rewrite и return в NGINX с примерами редиректов, обработки URL и настройки маршрутов для Laravel и других приложений. В статье показаны основные флаги rewrite, использование регулярных выражений и типовые конфигурации.

Разработка и инструменты • 9 месяцев назад

Как скачивать файлы по URL в Laravel

Подробное руководство по скачиванию файлов из внешних URL в Laravel: от простого стриминга до продакшен-ориентированных подходов с сохранением и раздачей файлов пользователям.

Разработка и инструменты • 9 месяцев назад

Как работает time.sleep() в Python

Подробно разбираем функцию time.sleep() в Python: как она работает, где применяется и какие ограничения имеет. Рассмотрены практические примеры, использование в циклах, многопоточном и асинхронном коде.

Разработка и инструменты • 9 месяцев назад

Генерация PDF в Laravel: Spatie Laravel PDF vs Laravel DomPDF

Сравнение двух популярных пакетов для генерации PDF в Laravel, современного Spatie Laravel PDF с рендерингом через браузер и классического Laravel DomPDF, который работает в чистом PHP. Обсуждаем преимущества, примеры, ограничения и когда что использовать.