39

I'm keeping all uploads on a custom, external drive. The files are being stored via custom API.

In Laravel 5.2, I can do this for a local file to download it:

return response()->download('path/to/file/image.jpg');

Unfortunately, when I pass a URL instead of a path, Laravel throws an error:

The file "https://my-cdn.com/files/image.jpg" does not exist

(the URL is a dummy of course).

Is there any way I can download the image.jpg file using Laravel's implementation or do I do this with plain PHP instead?

lesssugar
  • 15,486
  • 18
  • 65
  • 115
  • Possible duplicate of http://stackoverflow.com/questions/32095572/laravel-download-file-from-s3-route-not-open-in-browser – Jamesking56 Aug 05 '16 at 14:27
  • 2
    @Jamesking56 Nah, first of all the post you linked is about S3 which Laravel supports out of the box as a possible remote disk (so not my case). Secondly, even if the post was similar, there's basically no answer. – lesssugar Aug 05 '16 at 14:29

8 Answers8

67

There's no magic, you should download external image using copy() function, then send it to user in the response:

$filename = 'temp-image.jpg';
$tempImage = tempnam(sys_get_temp_dir(), $filename);
copy('https://my-cdn.com/files/image.jpg', $tempImage);

return response()->download($tempImage, $filename);
Limon Monte
  • 52,539
  • 45
  • 182
  • 213
33

TL;DR
Use the streamDownload response if you're using 5.6 or greater. Otherwise implement the function below.

Original Answer
Very similar to the "download" response, Laravel has a "stream" response which can be used to do this. Looking at the API, both of the these functions are wrappers around Symfony's BinaryFileResponse and StreamedResponse classes. In the Symfony docs they have good examples of how to create a StreamedResponse

Below is my implementation using Laravel:

<?php

use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

Route::get('/', function () {
    $response = response()->stream(function () {
        echo file_get_contents('http://google.co.uk');
    });

    $name = 'index.html';

    $disposition = $response->headers->makeDisposition(
        ResponseHeaderBag::DISPOSITION_ATTACHMENT,
        $name,
        str_replace('%', '', Str::ascii($name))
    );

    $response->headers->set('Content-Disposition', $disposition);

    return $response;
});

Update 2018-01-17

This has now been merged into Laravel 5.6 and has been added to the 5.6 docs. The streamDownload response can be called like this:

<?php

Route::get('/', function () {
    return response()->streamDownload(function () {
        echo file_get_contents('https://my.remote.com/file/store-12345.jpg');
    }, 'nice-name.jpg');
});
Theo Kouzelis
  • 3,195
  • 5
  • 37
  • 66
  • 1
    Your method is much better than `copy()` especially when its a large file like a video download. – Neel Mar 11 '18 at 12:39
  • 1
    This gives causes php to run out of memory. From the error, it looks like it's trying to load the whole file into memory where I do `echo file_get_contents...` – Jon McClung Jan 09 '19 at 20:07
  • @JonMcClung Yeah you are right. In the example above "echo file_get_contents" pulls the whole file into memory. I did this just to keep the example simple. I think if you want to actually stream the file you have to pass it a stream context like this https://github.com/twistor/flysystem-http/blob/acba993b790777067d7aa405458b4e06d329a9f3/src/HttpAdapter.php#L174-L187 Here are the docs for stream context http://php.net/manual/en/function.stream-context-create.php – Theo Kouzelis Jan 10 '19 at 07:44
  • 1
    Could you just use `readfile` instead of `file_get_contents`? The answer to [this question](https://stackoverflow.com/questions/20095175/php-readfile-vs-file-get-contents) seems to suggest that `readfile` returns a stream, which might perform better when streaming a response? – Dwight Apr 10 '19 at 02:13
  • 1
    @Dwight yes I just used `readfile($url);` and it works. I think it's like `Header("Content-disposition: attachment; filename=$name"); Header("Content-Type: application/download"); readfile($url);` – Heichou May 03 '20 at 20:30
  • If you're all about ease of use and don't reinvent the wheel. You could leverage Guzzle to retrieve the file. It will automatically set the appropriate headers for you. – Ogier Schelvis Jun 09 '21 at 07:42
  • For larger files, `file_get_contents()` will end up a `Memory exhausted` fatar error. – Khalilullah Jul 09 '21 at 07:17
29

As for 2020 this is all pretty easy.

2 lines of code to download to your server and 2 lines to upload to browser (if needed).

Assume on you server side (Laravel application) you want some remote file (say a Google home page HTML) to get saved locally onto your server file system. And then make your client browser to download that file:

// Load the file contents into a variable.
$contents = file_get_contents('www.google.com');

// Save the variable as `google.html` file onto
// your local drive, most probably at `your_laravel_project/storage/app/` 
// path (as per default Laravel storage config)
Storage::disk('local')->put('google.html', $contents);

// -- Here your have saved the file from the URL 
// -- to your local Laravel storage folder on your server.
// -- By default this is `your-laravel-project/storage/app` folder.

// Now, if desired, and if you are doing this within a web application's
// HTTP request (as opposite to CLI application)
// the file can be sent to the browser (client) with the response
// that instructs the browser to download the file at client side:

// Get the file path within you local filesystem
$path = Storage::url('google.html');

// Return HTTP response to a client that initiates the file downolad
return response()->download($path);

Look up the Laravel Storage facade documentation for details on disk configuration and put method and Response with file download documentaion for returning file with an HTTP reponse.

Valentine Shi
  • 6,604
  • 4
  • 46
  • 46
6

Why not just use a simple redirect?

return \Redirect::to('https://my-cdn.com/files/image.jpg');
Jamesking56
  • 3,683
  • 5
  • 30
  • 61
  • 3
    This will, well, make a redirect to the external resource - and stay on the page, which is definitely not what I need ;) – lesssugar Aug 05 '16 at 14:42
6

try this script:

// $main_url is path(url) to your remote file
$main_url = "http://dl.aviny.com/voice/marsieh/moharram/92/shab-02/mirdamad/mirdamad-m92-sh2-01.mp3";
header("Content-disposition:attachment; filename=$main_url");
readfile($main_url);

If you do not want end user can see main_url in header try this:

$main_url = "http://dl.aviny.com/voice/marsieh/moharram/92/shab-02/mirdamad/mirdamad-m92-sh2-01.mp3";
$file = basename($main_url);
header("Content-disposition:attachment; filename=$file");
readfile($main_url);
masood
  • 725
  • 8
  • 23
2

The one key thing here for me is being able to test the code. Using Laravel newer https://laravel.com/docs/8.x/http-client I can then use it like this

Http::get($follower->profile_pic_url)->body();

Which looks like this to get the file and save

$contents = Http::get($follower->profile_pic_url)->body();
Storage::disk("local")->put($path, $contents);

and then mock it in a test


Storage::shouldReceive("disk->exists")->once()->andReturn(false);
Storage::shouldReceive("disk->put")->once();
Http::shouldReceive("get->body")->once();
alnutile
  • 21
  • 1
  • 2
-1

You can serve the file to download using a browser with the download() method

Documentation:

Example:

    return Storage::download('file.jpg');    
    return Storage::download('file.jpg', $name, $headers);
    // or
    return response()->download($pathToFile); 
    return response()->download($pathToFile, $name, $headers);
-4

You could pull down the content of the original file, work out its mime type and then craft your own response giving the right headers.

I do this myself using a PDF library, but you could modify to use file_get_contents() to pull down the remote assets:

return Response::make(
        $pdf,
        200,
        array(
            'Content-Description' => 'File Transfer',
            'Cache-Control' => 'public, must-revalidate, max-age=0, no-transform',
            'Pragma' => 'public',
            'Expires' => 'Sat, 26 Jul 1997 05:00:00 GMT',
            'Last-Modified' => ''.gmdate('D, d M Y H:i:s').' GMT',
            'Content-Type' => 'application/pdf', false,
            'Content-Disposition' => ' attachment; filename="chart.pdf";',
            'Content-Transfer-Encoding' => ' binary',
            'Content-Length' => ' '.strlen($pdf),
            'Access-Control-Allow-Origin' => $origin,
            'Access-Control-Allow-Methods' =>'GET, PUT, POST, DELETE, HEAD, PATCH',
            'Access-Control-Allow-Headers' =>'accept, origin, content-type',
            'Access-Control-Allow-Credentials' => 'true')
        );

You need to change $pdf to be the data of the file, Content-Type to contain the mimetype of the file that the data is and Content-Disposition is the filename you want it to appear as.

Not sure whether this will work, mine simply makes the browser popup with a PDF download so I'm not sure whether it'll work for embedding CDN files... Worth a try though.

Jamesking56
  • 3,683
  • 5
  • 30
  • 61
  • Question was about downloading a remote URL. Not a local file. – Artistan Sep 09 '17 at 20:12
  • Actually the question was ambiguous. The text description assumed file download to server. The code snippet suggested the need to upload the file with an HTTP response to the browser (which in turn assumes the file exists on the server's file system, so it has been already downloaded from URL). @Jamesking56 was right about the second part. Why downvote. – Valentine Shi Nov 13 '19 at 07:28
  • @Artistan note the line in my answer _"but you could modify to use file_get_contents() to pull down the remote assets"_ – Jamesking56 Nov 13 '19 at 14:49