29

I have a php script on a server to send files to recipents: they get a unique link and then they can download large files. Sometimes there is a problem with the transfer and the file is corrupted or never finishes. I am wondering if there is a better way to send large files

Code:

$f = fopen(DOWNLOAD_DIR.$database[$_REQUEST['fid']]['filePath'], 'r');
while(!feof($f)){
    print fgets($f, 1024);
}
fclose($f);

I have seen functions such as

http_send_file
http_send_data

But I am not sure if they will work.

What is the best way to solve this problem?

Regards
erwing

Milan Babuškov
  • 59,775
  • 49
  • 126
  • 179
Erwing
  • 301
  • 1
  • 3
  • 4
  • 2
    Part of this problem might be solved by supporting `Range` headers, so browsers can pause and resume downloads. Here's a question dealing with that: http://stackoverflow.com/questions/157318/resumable-downloads-when-using-php-to-send-the-file – grossvogel Nov 08 '10 at 19:01
  • Also take a look at [this](http://stackoverflow.com/a/6527829/1469208) and [this](http://stackoverflow.com/a/21354337/1469208) SO answers. – trejder Feb 21 '14 at 13:06
  • See it here -- https://stackoverflow.com/questions/47827768/how-to-download-large-files-with-php/47827769#47827769 – Oleg Uryutin Dec 15 '17 at 07:40

13 Answers13

16

Chunking files is the fastest / simplest method in PHP, if you can't or don't want to make use of something a bit more professional like cURL, mod-xsendfile on Apache or some dedicated script.

$filename = $filePath.$filename;

$chunksize = 5 * (1024 * 1024); //5 MB (= 5 242 880 bytes) per one chunk of file.

if(file_exists($filename))
{
    set_time_limit(300);

    $size = intval(sprintf("%u", filesize($filename)));

    header('Content-Type: application/octet-stream');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: '.$size);
    header('Content-Disposition: attachment;filename="'.basename($filename).'"');

    if($size > $chunksize)
    { 
        $handle = fopen($filename, 'rb'); 

        while (!feof($handle))
        { 
          print(@fread($handle, $chunksize));

          ob_flush();
          flush();
        } 

        fclose($handle); 
    }
    else readfile($path);

    exit;
}
else echo 'File "'.$filename.'" does not exist!';

Ported from richnetapps.com / NeedBee. Tested on 200 MB files, on which readfile() died, even with maximum allowed memory limit set to 1G, that is five times more than downloaded file size.

BTW: I tested this also on files >2GB, but PHP only managed to write first 2GB of file and then broke the connection. File-related functions (fopen, fread, fseek) uses INT, so you ultimately hit the limit of 2GB. Above mentioned solutions (i.e. mod-xsendfile) seems to be the only option in this case.

EDIT: Make yourself 100% that your file is saved in utf-8. If you omit that, downloaded files will be corrupted. This is, because this solutions uses print to push chunk of a file to a browser.

trejder
  • 17,148
  • 27
  • 124
  • 216
  • This is a prefectly working solution, used and tested in many of my projects. So, I assume, that the downvoter, how isn't even able to express in comment, why he/she is downvoting, simply had a bad day or can't even understand this answer. Pity in both cases... – trejder Oct 22 '14 at 12:44
  • @user2864740 Maybe it does in your PHP, but not in mine and not in an official one. Tell me, where, exactly where in [`readfile` doc](http://php.net/manual/en/function.readfile.php) you see any mention about chunking? Where? There's only a mention about **user-defined** `readfile_chunked` function, which source code looks... **nearly exactly like in above example**. Check, before you state your false claims! – trejder Jul 08 '15 at 07:23
  • From the documentation: "Note: *readfile() will **not** present any memory issues, even when sending large files, on its own*. If you encounter an out of memory error ensure that output buffering is off with ob_get_level()." This implies that internally readfile is streaming (aka "chunking"); and that the problem with any streamed approach is still the output buffering, if active. – user2864740 Jul 08 '15 at 07:28
  • I wouldn't say, that this description implies or even suggests an internal "chunking" at all! And beside, usng pure `readfile()` on large files, dies on client-side -- browser terminating connection, that takes too long, not on memory issues. If `readfile()` is chinking files, then it most likely does it for just the reading file on server, but it still sends entire file as **one** request response. And this dies on most browsers. Chunking in my solution cause to download file in multiple responses and allows browser to handle downloads of even large files. – trejder Jul 08 '15 at 10:27
  • It is only possible to claim '[no] memory issue' when using an approach that takes O(1) memory - and for downloading a file this implies the operation is streaming. Anyway, how does this code cause 'multiple responses'? It is still a *single* HTTP request. If output buffering is disable both approaches will also work the same on the upside as the file read has to be written somewhere (directly to the HTTP response stream).. and thus read somewhere (on the client). – user2864740 Jul 08 '15 at 17:09
  • I suspect that the code that used readfile - that prompted this and similar solutions - failed to correctly invoke [ob_clean_end](http://php.net/manual/en/function.ob-end-clean.php) and thus didn't disable the output buffer which would lead to the problems incorrectly attributed to readfile. The proposed code work without ending the output buffer because it explicitly flushes it; but this is nothing against readfile, only improper buffer state configuration. – user2864740 Jul 08 '15 at 17:15
  • When you say your file is saved in utf-8, are you referring to the file being downloaded? If yes, how do I know my file is in utf-8? – Gellie Ann Apr 20 '21 at 09:03
  • Can this also be used for zip files? It doesn't seem to work – Gellie Ann Apr 20 '21 at 10:53
  • 1
    @GellieAnn You're referring to 7 years old answer. I am no longer PHP developer. The ` header('Content-Type: application/octet-stream');` and `header('Content-Transfer-Encoding: binary');` suggests pure binary connection though, so in theory there should be absolutely no problem in downloading ZIP files this way. But I cannot verify that in practice. Also note that ZIP file are not meant to be larger than 2 GB and PHP's library may not be able to process larger files (many ZIP clients are not able to; other uses some weird tricks to handle ZIP files that large). Maybe here is the problem. – trejder Apr 21 '21 at 08:35
  • 1
    @GellieAnn When talking about UTF-8 encoding I was most likely talking about .php file itself (with above code) those seven years ago. Not about files that you're going to download this way. Downloaded files were meant to be binary. Binary files cannot be encoded with text encoding like UTF-8. – trejder Apr 21 '21 at 08:36
  • 1
    @trejder My zip file is less than 1GB so there might be something else. I might look at it again sometime but for now I ended up not zipping the files. Your 7 year old code still works. Thanks! Also thank you for answering my questions, appreciate it. – Gellie Ann Apr 24 '21 at 12:41
12

If you are sending truly large files and worried about the impact this will have, you could use the x-sendfile header.

From the SOQ using-xsendfile-with-apache-php, an howto blog.adaniels.nl : how-i-php-x-sendfile/

PJunior
  • 2,649
  • 1
  • 33
  • 29
garrow
  • 3,459
  • 1
  • 21
  • 24
  • 3
    There is a ["**large file download**" PHP script here](http://phpsnips.com/579/PHP-download-script-for-Large-File-Downloads) that can handle around **2GB** of many file types like exe, mp3, mp4, pdf etc. This script is worth for downloading smaller to medium file sizes via PHP script. It also describes about **X-sendfile** for really massive file downloads like more than 5GB. Check out the link. – webblover Jun 26 '14 at 08:21
  • @webblover - your link is dead. can you repost? – Ben Jan 19 '17 at 14:38
  • 3
    @Ben sorry for that. 'phpsnips' website had removed that snippet. You may download it from Github directly: https://github.com/saleemkce/downloadable/blob/master/download.php – webblover Jan 20 '17 at 10:07
  • "**Large File download**" script **REPOSTED** just above here. – webblover Jan 20 '17 at 10:22
7

Best solution would be to rely on lighty or apache, but if in PHP, I would use PEAR's HTTP_Download (no need to reinvent the wheel etc.), has some nice features, like:

  • Basic throttling mechanism
  • Ranges (partial downloads and resuming)

See intro/usage docs.

grossvogel
  • 6,694
  • 1
  • 25
  • 36
  • 1
    Thanks for pointing to HTTP_Download, exactly what I was looking for and works perfectly when downloading CD images. – SaschaM78 Nov 14 '13 at 10:01
  • 1
    I had to fix a couple deprecated reference errors and non-static method usage, but in 5 minutes i got it up and running, so it's nothing too hard. It may be because i have an outdated pear library, but it's worth to notice. – STT LCU Aug 26 '14 at 07:28
  • 1
    This didn't age very well. HTTP_Download is obsolete in 2020 and cannot even be ran in php7 because it uses `&new` object instance creation method which gives an error in PHP 7. I wasted a lot of time to get pear working and install the HTTP_Download extension, just to see the package is deprecated. – brett Nov 14 '20 at 13:49
6

We've been using this in a couple of projects and it works quite fine so far:

/**
 * Copy a file's content to php://output.
 *
 * @param string $filename
 * @return void
 */
protected function _output($filename)
{
    $filesize = filesize($filename);

    $chunksize = 4096;
    if($filesize > $chunksize)
    {
        $srcStream = fopen($filename, 'rb');
        $dstStream = fopen('php://output', 'wb');

        $offset = 0;
        while(!feof($srcStream)) {
            $offset += stream_copy_to_stream($srcStream, $dstStream, $chunksize, $offset);
        }

        fclose($dstStream);
        fclose($srcStream);   
    }
    else 
    {
        // stream_copy_to_stream behaves() strange when filesize > chunksize.
        // Seems to never hit the EOF.
        // On the other handside file_get_contents() is not scalable. 
        // Therefore we only use file_get_contents() on small files.
        echo file_get_contents($filename);
    }
}
Andreas Baumgart
  • 2,647
  • 1
  • 25
  • 20
4

For downloading files the easiest way I can think of would be to put the file in a temporary location and give them a unique URL that they can download via regular HTTP.

As part generating these links you could also remove files that were more than X hours old.

Andrew Grant
  • 58,260
  • 22
  • 130
  • 143
  • I don't like this answer because it requires additional use of crons or such to remove the old files, and that adds another layer of complexity and another point of failure to the system, but I'm not going to vote it down because it is a valid answer. – UnkwnTech Feb 28 '09 at 00:26
  • @Unkwntech - no need for cron, as mentioned while generating new files you can also discard older ones. Lots of websites perform cron-like tasks as part of another call. – Andrew Grant Feb 28 '09 at 00:41
  • @Andrew Grant, you are correct that it could be done without a CRON, but I still feel that it adds an extra point of failure, whereas with my answer I don't think that it is adding an extra failure point. – UnkwnTech Feb 28 '09 at 00:54
  • 3
    But what you get out of it is punting the complexity of the actual HTTP communication to the server, where it belongs (things like expire headers, range downloads, caching, etc.). *I* would rather clean up a few files with a script than reimplement HTTP in my application. – Will Hartung Feb 28 '09 at 01:14
  • 1
    I agree with Will. Browsers these days have sophisticated downloaders that can pause and resume downloads using built-in features of http. – grossvogel Nov 08 '10 at 18:55
3

Create a symbolic link to the actual file and make the download link point at the symbolic link. Then, when the user clicks on the DL link, they'll get a file download from the real file but named from the symbolic link. It takes milliseconds to create the symbolic link and is better than trying to copy the file to a new name and download from there.

For example:

<?php

// validation code here

$realFile = "Hidden_Zip_File.zip";
$id = "UserID1234";

if ($_COOKIE['authvalid'] == "true") {
    $newFile = sprintf("myzipfile_%s.zip", $id); //creates: myzipfile_UserID1234.zip

    system(sprintf('ln -s %s %s', $realFile, $newFile), $retval);

    if ($retval != 0) {
        die("Error getting download file.");
    }

    $dlLink = "/downloads/hiddenfiles/".$newFile;
}

// rest of code

?>

<a href="<?php echo $dlLink; ?>Download File</a>

That's what I did because Go Daddy kills the script from running after 2 minutes 30 seconds or so....this prevents that problem and hides the actual file.

You can then setup a CRON job to delete the symbolic links at regular intervals....

This whole process will then send the file to the browser and it doesn't matter how long it runs since it's not a script.

Hummdis
  • 57
  • 5
1
header("Content-length:".filesize($filename));
header('Content-Type: application/zip'); // ZIP file
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="downloadpackage.zip"');
header('Content-Transfer-Encoding: binary');
ob_end_clean();
readfile($filename);
exit();
ahmed
  • 498
  • 6
  • 13
1

When I have done this in the past I've used this:

set_time_limit(0); //Set the execution time to infinite.
header('Content-Type: application/exe'); //This was for a LARGE exe (680MB) so the content type was application/exe
readfile($fileName); //readfile will stream the file.

These 3 lines of code will do all the work of the download readfile() will stream the entire file specified to the client, and be sure to set an infinite time limit else you may be running out of time before the file is finished streaming.

UnkwnTech
  • 88,102
  • 65
  • 184
  • 229
  • Of course, if this isn't an exe, the mime type should be different. – configurator Feb 28 '09 at 00:30
  • 2
    Also, never-ever-ever combine set_time_limit(0) with ignore_user_abort() or the script could be running forever. – Pim Jager Feb 28 '09 at 09:39
  • 2
    Very true, unless of course you want the script to continue running after the user dies. – UnkwnTech Feb 28 '09 at 09:47
  • 6
    That would leave evidence that you killed the user. Not exactly a good idea. – Matt Jan 22 '10 at 21:45
  • readfile() still uses a lot of memory -- big files and high traffic will exhaust php with this method – tmsimont Jul 08 '11 at 16:56
  • 5
    This DOES NOT WORK FOR large files. The question is specifically about LARGE files. Down vote. – Mike Starov Nov 17 '11 at 19:57
  • DOES NOT WORK FOR LARGE FILES! Downwote if you can. Try solution here http://stackoverflow.com/questions/8031301/php-readfile-not-working-for-me-and-i-dont-know-why – Miha Trtnik Feb 22 '12 at 15:46
  • As other already mentioned, this is of course not true. The `readfile` function does not stream anything. It just tries to read entire file into memory and throw whole into browser. This will die on anything larger than 100 MB. And it will die "brutally", i.e. no error message, only your user downloads HTML page with error about exhausting memory, instead of real file. For real streaming of large files you have to implement something more sophisticated than these three lines. For example, file chunking. See [my example](http://stackoverflow.com/a/21936954/1469208) below. – trejder Feb 22 '14 at 19:40
  • @trejder Incorrect; readfile does not read everything into memory. The file contents might end up in the output buffer cache, if the cache is not properly disabled - but this is not related to readfile. – user2864740 Jul 08 '15 at 17:24
1

If you are using lighttpd as a webserver, an alternative for secure downloads would be to use ModSecDownload. It needs server configuration but you'll let the webserver handle the download itself instead of the PHP script.

Generating the download URL would look like that (taken from the documentation) and it could of course be only generated for authorized users:

<?php

  $secret = "verysecret";
  $uri_prefix = "/dl/";

  # filename
  # please note file name starts with "/" 
  $f = "/secret-file.txt";

  # current timestamp
  $t = time();

  $t_hex = sprintf("%08x", $t);
  $m = md5($secret.$f.$t_hex);

  # generate link
  printf('<a href="%s%s/%s%s">%s</a>',
         $uri_prefix, $m, $t_hex, $f, $f);
?>

Of course, depending on the size of the files, using readfile() such as proposed by Unkwntech is excellent. And using xsendfile as proposed by garrow is another good idea also supported by Apache.

Community
  • 1
  • 1
lpfavreau
  • 12,871
  • 5
  • 32
  • 36
0

I have had same problem, my problem solved by adding this before starting session session_cache_limiter('none');

Nawras
  • 181
  • 1
  • 12
0

I'm not sure this is a good idea for large files. If the thread for your download script runs until the user has finished the download, and you're running something like Apache, just 50 or more concurrent downloads could crash your server, because Apache isn't designed to run large numbers of long-running threads at the same time. Of course I might be wrong, if the apache thread somehow terminates and the download sits in a buffer somewhere whilst the download progresses.

0

This is tested on files of a size 200+ MB on a server that has 256MB memory limit.

header('Content-Type: application/zip');
header("Content-Disposition: attachment; filename=\"$file_name\"");
set_time_limit(0);
$file = @fopen($filePath, "rb");
while(!feof($file)) {
  print(@fread($file, 1024*8));
  ob_flush();
  flush();
}
Alex
  • 374
  • 1
  • 4
  • 13
0

I have used the following snippet found in the comments of the php manual entry for readfile:

function _readfileChunked($filename, $retbytes=true) {
    $chunksize = 1*(1024*1024); // how many bytes per chunk
    $buffer = '';
    $cnt =0;
    // $handle = fopen($filename, 'rb');
    $handle = fopen($filename, 'rb');
    if ($handle === false) {
        return false;
    }
    while (!feof($handle)) {
        $buffer = fread($handle, $chunksize);
        echo $buffer;
        ob_flush();
        flush();
        if ($retbytes) {
            $cnt += strlen($buffer);
        }
    }
    $status = fclose($handle);
    if ($retbytes && $status) {
        return $cnt; // return num. bytes delivered like readfile() does.
    }
    return $status;
}
periklis
  • 10,102
  • 6
  • 60
  • 68
  • Another unnecessary implementation. See my comments on http://stackoverflow.com/a/21936954/2864740 for why. – user2864740 Jul 08 '15 at 17:19
  • That is incorrect. I know it by experience. Using ob_end_clean() as you state in that coments didn't solve the problem for me, while the chunked solution did. Key is that readfile doesn't allow files close to or larger than the PHP memory limit to be downloaded. This is a simple solution that works, and as such, you shouldn't downrate it. – Juangui Jordán May 20 '16 at 14:05