Как скачивать файлы по 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)

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

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

Почему стоит использовать Python для скриптов

Почему Bash-скрипты часто ломаются на macOS и в CI, и как Python решает эту проблему. Разбираем, в каких случаях Python лучше подходит для автоматизации, чем bash, и почему он делает скрипты понятнее, стабильнее и кросс-платформеннее.

54 0 1 мин

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

Подробное руководство по созданию пользователей в MySQL и управлению их правами доступа. В статье разобраны команды CREATE USER, GRANT, REVOKE, примеры настройки аутентификации и типичные ошибки при работе с правами.

7 0 2 мин

Как создать AI-агента на Python с OpenAI для автоматического парсинга счетов

Подробный гайд, как разработать полноценного AI-агента на Python и OpenAI API, который автоматически парсит счета (PDF, фото), вытягивает данные в структурированный JSON, валидирует их и сохраняет в CSV или базу. Плюс интеграция с Telegram-ботом для загрузки документов.

45 0 3 мин