В современных веб-приложениях часто требуется загружать файлы не только из собственного хранилища, но и из внешних источников: проксировать скачивание пользователю, сохранять на сервере, обрабатывать большие файлы без «съедания» памяти, а затем раздавать их клиентам. 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()).