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

В современных веб-приложениях часто требуется загружать файлы не только из собственного хранилища, но и из внешних источников: проксировать скачивание пользователю, сохранять на сервере, обрабатывать большие файлы без «съедания» памяти, а затем раздавать их клиентам. Laravel предоставляет для этого всё необходимое.

В этой статье мы разберём несколько надёжных способов скачивания файлов по внешнему URL в Laravel, включая потоковую передачу, сохранение на диск, оптимизированный стриминг и последующее скачивание пользователем.

Общая настройка

Предположим, у вас есть контроллер FileDownloadController и маршруты, которые его используют:

// routes/web.php
use App\Http\Controllers\FileDownloadController;
use Illuminate\Support\Facades\Route;

Route::get('/download-remote', [FileDownloadController::class, 'downloadRemote']);
Route::get('/cache-remote', [FileDownloadController::class, 'cacheRemote']);
Route::get('/cache-remote-stream', [FileDownloadController::class, 'cacheRemoteStreamed']);
Route::get('/download-cached', [FileDownloadController::class, 'downloadCached']);
Route::get('/proxy-remote', [FileDownloadController::class, 'proxyRemote']);

Контроллер может выглядеть так:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use GuzzleHttp\Psr7\Utils;

class FileDownloadController extends Controller
{
    // Методы ниже
}

Потоковое скачивание внешнего URL напрямую пользователю

Если вы хотите передать файл напрямую пользователю, не сохраняя его на диск, это делается через потоковую передачу.

public function downloadRemote(Request $request)
{
    $url = $request->query('url');
    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $fileName = basename(parse_url($url, PHP_URL_PATH)) ?: 'downloaded-file';

    return response()->streamDownload(function () use ($url) {
        $handle = fopen($url, 'rb');
        if (! $handle) {
            abort(502, 'Unable to open remote file');
        }
        while (! feof($handle)) {
            echo fread($handle, 1024 * 1024); // читаем по 1 МБ
        }
        fclose($handle);
    }, $fileName);
}

Когда это полезно:

  • Когда нужно проксировать скачивание через Laravel (для аутентификации, логирования, контроля доступа).

  • Файл не сохраняется на сервере.

Для этого должен быть разрешён allow_url_fopen в настройках PHP.

Скачивание файла и сохранение на сервер

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

public function cacheRemote(Request $request)
{
    $url = $request->query('url');
    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $response = Http::timeout(30)->get($url);
    if (! $response->successful()) {
        abort(502, 'Failed to download remote file');
    }

    $extension = pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
    $fileName = 'remote_' . time() . ($extension ? ".{$extension}" : '');

    $path = "downloads/{$fileName}";
    Storage::disk('local')->put($path, $response->body());

    return response()->json([
        'stored_as' => $path,
        'disk' => 'local',
    ]);
}

Когда использовать:

  • Файлы небольшие или средние по размеру.

  • Можно выделить память под загрузку в один проход.

Потоковое скачивание больших файлов в хранилище

Большие файлы могут съедать память, если загружать их целиком. Для этого подхода лучше подойдет стриминг прямо в файл через Guzzle.

public function cacheRemoteStreamed(Request $request)
{
    $url = $request->query('url');
    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $extension = pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
    $fileName = 'remote_' . time() . ($extension ? ".{$extension}" : '');
    $relativePath = "downloads/{$fileName}";
    $absolutePath = Storage::disk('local')->path($relativePath);

    $resource = Utils::tryFopen($absolutePath, 'w');

    Http::withOptions([
        'sink' => $resource,
        'timeout' => 60,
    ])->get($url);

    return response()->json([
        'stored_as' => $relativePath,
        'disk' => 'local',
    ]);
}

Плюсы:

  • Удерживает минимальное потребление памяти.

  • Подходит для очень больших файлов и производственных систем.

Предоставление сохранённого файла пользователю

Когда файл уже есть в хранилище, его легко отдать на скачивание через Laravel.

public function downloadCached(Request $request)
{
    $path = $request->query('path');
    if (! $path) {
        abort(400, 'Missing path parameter');
    }
    if (! Storage::disk('local')->exists($path)) {
        abort(404, 'File not found');
    }
    return Storage::disk('local')->download($path);
}

Laravel использует фасад Storage для скачивания с правильными заголовками браузера (имя, тип, заголовки и т.п.).

Прокси — скачивание и отправка пользователю в одном запросе

Если нужно получить файл с внешнего сервера и сразу передать пользователю, без промежуточного сохранения на диск:

public function proxyRemote(Request $request)
{
    $url = $request->query('url');
    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $fileName = basename(parse_url($url, PHP_URL_PATH)) ?: 'downloaded-file';
    $response = Http::timeout(60)->get($url);
    if (! $response->successful()) {
        abort(502, 'Failed to download remote file');
    }

    return response()->streamDownload(function () use ($response) {
        echo $response->body();
    }, $fileName, [
        'Content-Type' => $response->header('Content-Type', 'application/octet-stream'),
    ]);
}

Когда это удобно:

  • Когда файл небольшой и нужно отдать его пользователю сразу.

Рекомендации для продакшена

  • Для больших или медленных источников используйте стриминг в фоновых задачах или очередях (Jobs), чтобы не блокировать HTTP-запросы.

  • Добавьте валидацию URL и проверку исхода запроса (successful()), чтобы избежать некорректного скачивания.

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

  • Обрабатывайте ошибки и таймауты HTTP-клиента (timeout(), retry()).

Комментарии (0)

Войдите, чтобы оставить комментарий

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

Laravel-Zipstream: потоковая генерация ZIP-архивов без нагрузки на сервер

Обзор пакета laravel-zipstream для потоковой генерации ZIP-архивов в Laravel. Разбираем, как работает стриминг, его преимущества и когда его стоит использовать.

Как отключить SELinux на Ubuntu, CentOS, Debian и RHEL

Подробная инструкция по проверке, временной и постоянной деактивации SELinux на популярных Linux-дистрибутивах: Ubuntu, Debian, CentOS и RHEL. Объяснены способы через консоль и конфигурационные файлы.

Декодирование JSON из HTTP-ответов в Laravel

Как Laravel обрабатываются JSON-ответы HTTP-клиента и какие возможности дают флаги декодирования JSON. Рассматриваются ошибки декодирования, работа с большими числами и настройка флагов по умолчанию.