13

When using readfile() -- using PHP on Apache -- is the file immediately read into Apache's output buffer and the PHP script execution completed, or does the PHP script execution wait until the client finishes downloading the file (or the server times out, whichever happens first)?

The longer back-story:

I have a website with lots of large mp3 files (sermons for a local church). Not all files in the audio archive are allowed to be downloaded, so the /sermon/{filename}.mp3 path is rewritten to really execute /sermon.php?filename={filename} and if the file is allowed to be downloaded then the content type is set to "audio/mpeg" and the file streamed out using readfile(). I've been getting complaints (almost exclusively from iPhone users who are streaming the downloads over 3G) that the files don't fully download, or that they cut off after about 10 or 15 minutes. When I switched from streaming out the file with a readfile() to simply redirecting to the file -- header("Location: $file_url"); -- all of the complaints went away (I even checked with a few users who could reliably reproduce the problem on demand previously).

This leads me to suspect that when using readfile() the PHP script engine is in use until the file is fully downloaded but I cannot find any references which confirm or deny this theory. I'll admit I'm more at home in the ASP.NET world and the dotNet equivalent of readfile() pushes the whole file to the IIS output buffer immediately so the ASP.NET execution pipeline can complete independently of the delivery of the file to the end client... is there an equivalent to this behavior with PHP+Apache?

Revent
  • 2,091
  • 2
  • 18
  • 33
Mike C.
  • 4,917
  • 8
  • 34
  • 38
  • Did you try disabling the php execution time limit while using readfile? That'd also answer your questions - if you allow unlimited execution time and the readfile version DOESN'T abort, then you've got your answer. – Marc B Aug 02 '12 at 22:30
  • I'm running on a shared hosting environment so I don't presume to be able to change such settings. In doing more reading it looks like every possible way of reading a file to output with PHP will leave the script running until the download is complete. Looks like I'll need to re-write this as an ASP.NET app if I want "flush and complete execution while the web server handles getting the file to the client" functionality. – Mike C. Aug 03 '12 at 14:25

7 Answers7

9

You may still have PHP output buffering active while performing the readfile(). Check that with:

if (ob_get_level()) ob_end_clean();

or

while (ob_get_level()) ob_end_clean();

This way theonly remaining output Buffer should be apache's Output Buffer, see SendBufferSize for apache tweaks.

EDIT

You can also have a look at mod_xsendfile (an SO post on such usage, PHP + apache + x-sendfile), so that you simply tell the web server you have done the security check and that now he can deliver the file.

Community
  • 1
  • 1
regilero
  • 29,806
  • 6
  • 60
  • 99
9

a few things you can do (I am not reporting all the headers that you need to send that are probably the same ones that you currently have in your script):

set_time_limit(0);  //as already mention
readfile($filename);
exit(0);

or

passthru('/bin/cat '.$filename);
exit(0);

or

//When you enable mod_xsendfile in Apache
header("X-Sendfile: $filename");

or

//mainly to use for remove files
$handle = fopen($filename, "rb");
echo stream_get_contents($handle);
fclose($handle);

or

$handle = fopen($filename, "rb");
while (!feof($handle)){
    //I would suggest to do some checking
    //to see if the user is still downloading or if they closed the connection
    echo fread($handle, 8192);
}
fclose($handle);
Fabrizio
  • 3,734
  • 2
  • 29
  • 32
  • I have noticed that nobody really knows how `set_time_limit(0);` works. `set_time_limit(0);` has bo be inside a loop because it resets the script timeout to default at each iteration. Putting `set_time_limit(0);` at the beginning of the code block is completly useless. – Viktor Joras Feb 02 '18 at 08:49
6

The script will be running until the user finishes downloading the file. The simplest, most efficient and surely working solution is to redirect the user:

header("Location: /real/path/to/file");
exit;

But this may reveal the location of the files. It's a good idea to password-protect the files that may not be downloaded by everyone anyway with an .htaccess file, but perhaps you use a database to detemine access and this is no option.

Another possible solution is setting the maximum execution time of PHP to 0, which disables the limit:

set_time_limit(0);

Your host may disallow this, though. Also PHP reads the file into the memory first, then goes through Apache's output buffer, and finally makes it to the network. Making users download the file directly is much more efficient, and does not have PHP's limitations like the maximum execution time.

Edit: The reason you get this complaint a lot from iPhone users is probably that they have a slower connection (e.g. 3G).

Luc
  • 5,339
  • 2
  • 48
  • 48
  • I am using header("Location: ... ") now and it does reveal where the files are (I just have to manage two archives now: a web-reachable public file store and the non-reachable private store. Not ideal, but gets the job done). – Mike C. Aug 03 '12 at 14:26
3

downloading files thru php isnt very efficient, using a redirect is the way to go. If you dont want to expose the location of the file, or the file isnt in a public location then look into internal redirects, here is a post that talks about it a bit, Can I tell Apache to do an internal redirect from PHP?

Community
  • 1
  • 1
hackattack
  • 1,087
  • 6
  • 9
  • Redirecting does actually reveal the location. – Luc Aug 02 '12 at 22:38
  • @Luc not if the redirect is internal – hackattack Aug 02 '12 at 22:44
  • Oh, right! My bad :) The only problem I think is that PHP can't do this. And if you're using htaccess already, you can also simply protect the files you don't want to be downloaded by everyone, which enables you to use the real location of the files without redirects. – Luc Aug 02 '12 at 22:45
  • Using `virtual()` as suggested in the link still requires the PHP script to be running until the download is complete, and it may be terminated according to time limits external to PHP (e.g. at Apache/CGI level) that you have no control over on a shared server. – Jake Sep 24 '20 at 00:02
3

Try using stream_copy_to_stream() instead. I find is has fewer problems than readfile().

set_time_limit(0);
$stdout = fopen('php://output', 'w');
$bfname = basename($fname);

header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$bfname\"");

$filein = fopen($fname, 'r');
stream_copy_to_stream($filein, $stdout);

fclose($filein);
fclose($stdout);
Nidesires
  • 66
  • 2
  • Amazingly simple solution that doesn't require the troublesome X-Sendfile and it works great. This was just what I needed after messing with this for over an hour. Thanks! – Todd Hammer Nov 18 '19 at 01:54
  • This avoids exceeding the PHP memory limit for very large files, but the script will still need to be running until the download is complete, and it may be terminated according to time limits external to PHP (e.g. at Apache/CGI level) that you have no control over on a shared server. – Jake Sep 23 '20 at 22:31
0

Under Apache, there is a nice elgant solution not involving php at all:

Just place an .htaccess config file into the folder containing the files to be offered for download with the following contents:

<Files *.*>
ForceType applicaton/octet-stream
</Files>

This tells the Apache to offer all files in this folder (and all its subfolders) for download, instead of directly displaying them in the browser.

ak2002
  • 1
  • 2
-1

See below url

http://php.net/manual/en/function.readfile.php

<?php
$file = 'monkey.gif';

if (file_exists($file)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename='.basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    ob_clean();
    flush();
    readfile($file);
    exit;
}
?>
Abid Hussain
  • 7,724
  • 3
  • 35
  • 53