fpassthru

(PHP 3, PHP 4, PHP 5)

fpassthru -- 输出文件指针处的所有剩余数据

说明

int fpassthru ( resource handle )

将给定的文件指针从当前的位置读取到 EOF 并把结果写到输出缓冲区。

如果发生错误, fpassthru() 返回 FALSE。 否则 fpassthru() 返回从 handle 读取并传递到输出的字符数目。

文件指针必须是有效的,并且必须指向一个由 fopen()fsockopen() 成功打开的文件。

如果已经向文件写入数据,就必须调用 rewind() 来将文件指针指向文件头。

如果既不修改文件也不在特定位置检索,只想将文件的内容下载到输出缓冲区,应该使用 readfile(),这样可以省去 fopen() 调用。

注: 当在 Windows 系统中将 fpassthru() 用于二进制文件时,要确保在用 fopen() 打开文件时在 mode 中附加了 b 来将文件以二进制方式打开。

鼓励在处理二进制文件时使用 b 标志,即使系统并不需要,这样可以使脚本的移植性更好。

例子 1. 对二进制文件使用 fpassthru()

<?php

// 以二进制格式打开文件
$name = './img/ok.png'
$fp = fopen($name, 'rb');

// 发送合适的报头
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));

// 发送图片并终止脚本
fpassthru($fp);
exit;

?>

参见 readfile()fopen()popen()fsockopen()


add a note add a note User Contributed Notes
file downloads verified by session vars
22-Oct-2005 11:36
here is my code, i tried several combinations, but most of them didnt work, and had all kinds of unnecessary headers in them, etc. this has additional good features, such as it stops sending the file if the connection stops (hopefully it does anyways), and it fixes IE filename problems when sending files that contain more than one dot in them by using a simple  preg_replace (IE likes to terminate the filename and messes everything up):

<?
function send_file($path) {
  
session_write_close();
  
ob_end_clean();
   if (!
is_file($path) || connection_status()!=0)
       return(
FALSE);

  
//to prevent long file from getting cut off from    //max_execution_time

  
set_time_limit(0);

  
$name=basename($path);

  
//filenames in IE containing dots will screw up the
   //filename unless we add this

  
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
      
$name = preg_replace('/\./', '%2e', $name, substr_count($name, '.') - 1);

  
//required, or it might try to send the serving    //document instead of the file

  
header("Cache-Control: ");
  
header("Pragma: ");
  
header("Content-Type: application/octet-stream");
  
header("Content-Length: " .(string)(filesize($path)) );
  
header('Content-Disposition: attachment; filename="'.$name.'"');
  
header("Content-Transfer-Encoding: binary\n");

   if(
$file = fopen($path, 'rb')){
       while( (!
feof($file)) && (connection_status()==0) ){
           print(
fread($file, 1024*8));
          
flush();
       }
      
fclose($file);
   }
   return((
connection_status()==0) and !connection_aborted());
}

?>
20-Sep-2005 12:12
I've gathered some useful info regarding sessions and large file downloads. I do not take credit for the function, just putting it in one spot for people.

The function:

<?
function send_file($name) {
  
ob_end_clean();
  
$path = "/directory/path/".$name;
   if (!
is_file($path) or connection_status()!=0) return(FALSE);
  
header("Cache-Control: no-store, no-cache, must-revalidate");
  
header("Cache-Control: post-check=0, pre-check=0", false);
  
header("Pragma: no-cache");
  
header("Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT");
  
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
  
header("Content-Type: application/octet-stream");
  
header("Content-Length: ".(string)(filesize($path)));
  
header("Content-Disposition: inline; filename=$name");
  
header("Content-Transfer-Encoding: binary\n");
   if (
$file = fopen($path, 'rb')) {
       while(!
feof($file) and (connection_status()==0)) {
           print(
fread($file, 1024*8));
          
flush();
       }
      
fclose($file);
   }
   return((
connection_status()==0) and !connection_aborted());
}
?>

And now how to use it...

<?php
session_start
();

//Validate the user
if($_SESSION[login] == 1){

  
//Close the session to allow for header() to be sent
  
session_write_close();

   if(!
send_file("testfile.zip")) {
       die (
"file transfer failed");
   } else {
      
//Log the download
  
}
}else{
   echo
"Not logged in";
}
?>

This lets a user download a file while still being able to browse your site.
jonathan at corporacionlinux dot cl
05-Aug-2005 11:58
While trying to "passthru" a file to the browser via PHP, but using the FEOF loop, the script tried to buffer the entire file before passing it to the browser. This is my original script. When calling it with a 15M PHP memory limit and a 16M file, apache killed the script.

<?php
   $name
= $tempDir . $_GET["file"];
  
$fd = fopen($name, 'rb');
  
   if(
$fd == false)
  
       die(
"<font color=red>ERROR: File not found.</font>");
  
  
// send the right headers
  
header("Cache-Control: ");// leave blank to avoid IE errors
  
header("Pragma: ");// leave blank to avoid IE errors
  
header("Content-type: application/octet-stream");
  
header("Content-Disposition: attachment; filename=\"" . $_GET["file"] . "\"");
  
header("Content-length:".(string)(filesize($name)));
  
sleep(1);

  
session_write_close();
  
ob_flush();
  
flush();
  
         while(!
feof($fd)) {
    
              
$buffer = fread($fd, 2048);
               print
$buffer;
         }
    
        
fclose ($fd);
         exit;
?>

Apache error log read:
Allowed memory size of 15728640 bytes exhausted (tried to allocate 10240 bytes)

I tried everything, including a flush() inside the loop. But the solution was forcing the flush other way:

<?php

   $buffer
= fread($fd, 32 * 1024);

?>

voila... works just fine for me.
herbert dot fischer at NOSPAM dot gmail dot com
22-Jul-2005 11:08
readfile and fpassthru are about 55% slower than doing a loop with "feof/echo fread".

I did an apache benchmark on Linux using PHP to read-and-show a 4MB JPG on browser and here are the results:

* Concurrency Level: 200
* Complete requests: 1000
* Document Length: 4646204 bytes
* Requests per second:
           - direct request to JPG: 72.07
           - fpassthru: 11.28
           - fread: 20.33
           - readfile: 11.84

* Concurrency Level: 200
* Complete requests: 1000
* Document Length: 4646204 bytes
* Requests per second:
           - direct request to JPG: 64.44
           - fpassthru: 6.84
           - fread: 19.45
           - readfile: 8.76

If reading thru PHP is needed, I suggest using fread. If you can bypass PHP and do direct linking, than it's prefered.
eamonn at easywebdev.com
04-Feb-2005 09:18
I was creating an application to download software products in zip, tar.gz and tar.bz2 formats using fpassthru and whilst the downloads functioned the resulting files were all corrupted.

I traced this down to zlib.output_compression being turned on.
If you exerience this then add ini_set("zlib.output_compression", "Off"); just before you send your headers.
webmaster at hardcorehoneyz dot com
16-Dec-2004 09:20
In relation to using sessions and fpassthru together.

Try adding: session_write_close()

somewhere near the top of the download script, before you start sending the video, and that should take care of it.

I've implemented and tested session_write_close() and it works like a dream. Other links can now be clicked and loaded whilst a big file is being passed using fpassthru.

Big thanks to Greg for this tip. What a helpful community we live in :0)
webmaster at hardcorehoneyz dot com
06-Dec-2004 09:33
I believe the following problem is a result of using sessions and fpassthru together.

I have a subscription based site which protects large video files (WMV format between 100-120MB) by storing them beneath web root. Downloading a video file requires the user to click a HTML link which requests a PHP script e.g. download-video.php?video_id=123. If the user is valid (session vars created from sucessful login) the script then creates the necessary headers to trigger a 'Save As' download box, opens the file from beneath web root and sends it using fpassthru.

The problem is as follows:

The user should be able to click other links on the site whilst a file is downloading. But when they do so, the requested page won't load until the download is complete.

As this download script is a seperate PHP request, the user should be able to load other pages on the site whilst the file is downloading.

At time of writing, I've tried almost everything to remove this bug. There must be a problem with using a PHP script rather than a direct web server link to download files.
lbaudrillard at hotmail dot com
16-Nov-2004 04:45
The way the PHP page is generated (buffered or not, and how if buffered) has an impact of the download function made using fpassthru (or fread, ...). I mean a download function may work just fine when it is called from a simple php file (no buffering here):

<?php
  
function download($file) { ... }
  
  
$filename = "/tmp/test.zip";
  
download($filename);
?>   

but may fails "in the real life" when the page is buffered:

<?php
   ob_start
("ob_gzhandler");
   ...
   require_once(
download.php);
   ...
  
$filename = "/files/file.zip";
  
download($filename);
?>

In my particular case, only Firefox 1.0 English did not perform the download, because of the ob_start("ob_gzhandler"). Replacing it by ob_start() solved the problem.

Hope that helps
Laurent from Paris, France
phpnet at -remove-me-uchange dot co dot uk
03-Nov-2004 09:11
I've modified the example given by straz at -removethispart-mac dot com to count each byte of the file out.  This can then be compared with the filesize once the file sending is complete to determine whether the file was sent succesfully or not.

Of course, this doesn't guarantee that the user actually recieved the file successfully though will let us know if something goes wrong half way through reading/sending the file at our end.

<?
/* fpassthru is apparantly a memory-hog. Use this instead */
  
while(!feof($fp)) {
      
$buf = fread($fp, 4096);
       echo
$buf;
      
$bytesSent+=strlen($buf);    /* We know how many bytes were sent to the user */
  
}
?>

I've then got this code to update my database to say that the file was downloaded successfully.

<?
  
if($bytesSent==filesize($file)) {
      
/* Do some cool stuff here! */
  
}
?>
nexz2004 at yahoo dot com
21-May-2004 03:30
also it is possible to make your php script resume downloads, to do this you need to check $_SERVER['HTTP_RANGE'] which may contain something like this
 "bytes=10-" - resume from position 10, and to end of file

when sending response it is also needed to send with headers
Accept-Ranges: bytes
Content-Length: {filesize}
Content-Range: bytes 10-{filesize-1}/{ffilesize}

hope its usefull
axx at axxess dot ca
16-Feb-2004 05:04
I have also perused this list of examples which I am sure work for that person, but, as others have mentioned here, do not work for me or (anyone else).

So what I did was try out all of these examples, check other sources of information, and put together what I think to be an example of what works on 'more than a few' systems.  The following example works for me wherever I need to create a download using fpassthru(), which works with IE6 (among other browsers):

<?
/*/
Download a file using fpassthru()
/*/
$fileDir = "/home/pathto/myfiles"; // supply a path name.
$fileName = "myfile.zip"; // supply a file name.
$fileString=$fileDir.'/'.$fileName; // combine the path and file
// translate file name properly for Internet Explorer.
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")){
 
$fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
}
// make sure the file exists before sending headers
if(!$fdl=@fopen($fileString,'r')){
   die(
"Cannot Open File!");
} else {
 
header("Cache-Control: ");// leave blank to avoid IE errors
 
header("Pragma: ");// leave blank to avoid IE errors
 
header("Content-type: application/octet-stream");
 
header("Content-Disposition: attachment; filename=\"".$fileName."\"");
 
header("Content-length:".(string)(filesize($fileString)));
  
sleep(1);
  
fpassthru($fdl);
}
?>

All that should require editing is the $fileDir and $fileName variables.  Upload the file and point to it with your browser to see if the script will prompt you for a download.

NOTE : Regarding File Types : Leaving the 'Content-type' header as-is should allow you to download pretty much any file.  I have tested it on some of the more popular file types including zip, css, php, inc, htm, png, gif and jpg.  During these tests, I did note that if I selected 'cancel' or 'open' when prompted to download either a gif or jpg, that it would indeed cancel or open in my image browser as it should, but subsequent attempts at 'downloading only' yielded a web page view of the image.  Closing the window and opening a new one reset this, allowing me save a jpeg or gif to the hard drive directly.  I believe the problem lies in the way the caching headers are treated, since if any info is specified in the 'cache-control' header, the browser download fails completely (in IE, anyways).

Enjoy! Mail me if it works!  ;-)
mm at tbwachiat dot com
17-Jan-2004 09:00
I've tried all of these renditions of this elusive task.  NONE of them have worked for me.  And when i say work, i mean where i can click some sort of link and have a file Save As... dialog box come up on MSIE 6.0.  In every other browser i've tried (Safari,Firebird,Netscape pc and mac) all have worked where it downloads to my desktop or asks me to save it in a certain place.

on MSIE 6.0.  the file i'm trying to download appears in it's own window. it's an image. BUT, the only thing i can do with it is SAVE IT AS A BMP. ugh.

I'm using the fpassthru function because i have files that must not be served by the webserver.
The Otter
11-Jan-2004 01:57
In reply to spam at flatwan dot net
This might save someone some time. I created a program to list some rather large files and create links for the end user to click on in order to download them (using the php function fpassthru()).

The problem I was having was it would make it half way through the download (about 377 megs) and the script would terminate and the download would stop.

After doing some shotgun troubleshooting I discovered the php config option 'max_execution_time = 30'. Upon changing it to 'max_execution_time = -1' the files >370 megs can be downloaded without the script aborting.

The best way to do this would to be:
<?php
@ignore_user_abort();
@
set_time_limit(0);
?>
This only changes these settings for the script that calls them. (Thanks to (I don't remember who) who wrote a form mail script that used these two lines)
spam at flatwan dot net
19-Nov-2003 08:07
This might save someone some time. I created a program to list some rather large files and create links for the end user to click on in order to download them (using the php function fpassthru()).

The problem I was having was it would make it half way through the download (about 377 megs) and the script would terminate and the download would stop.

After doing some shotgun troubleshooting I discovered the php config option 'max_execution_time = 30'. Upon changing it to 'max_execution_time = -1' the files >370 megs can be downloaded without the script aborting.

Jon
arabold AT nero DOT com
15-May-2003 07:11
Here's a summary the different headers you need to set to make downloads *always* work with IE and Mozilla:

[SNIP]
  $disposition = "inline"; // "inline" to view file in browser or "attachment" to download to hard disk
  $mime = "image/jpeg"; // or whatever the mime type is
  $name = "foo.jpg"; // file name
  $path = "/path/to/foo.jpg"; // full path and file name
 
  if (isset($_SERVER["HTTPS"])) {
     /**
       * We need to set the following headers to make downloads work using IE in HTTPS mode.
       */
     header("Pragma: ");
     header("Cache-Control: ");
     header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
     header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
     header("Cache-Control: no-store, no-cache, must-revalidate"); // HTTP/1.1
     header("Cache-Control: post-check=0, pre-check=0", false);
  }
     else if ($disposition == "attachment") {
         header("Cache-control: private");
  }
  else {
     header("Cache-Control: no-cache, must-revalidate");
     header("Pragma: no-cache");
  }
  header("Content-Type: $mime");
  header("Content-Disposition:$disposition; filename=\"".trim(htmlentities($name))."\"");
  header("Content-Description: ".trim(htmlentities($name)));
  header("Content-Length: ".(string)(filesize($path)));
  header("Connection: close");
[/SNIP]

This way all kinds of download work for me. Hope that helps
DarkAngela_ at hotmail dot com
08-May-2003 07:48
Just a little thing more from the ssharma's script (thx to him for his great help ...) :

Don't forget to put the fopen with the "rb" argument and not just with the "r"
or you won't be able to make the script work with all pdf file.

My final script (working for Open and Save on a 1.9 Mb complex PDF file) :

<?php
//The filename is stored in the $produitFilename variable in my script (the only thing you need)

// You need to specify the REAL path for your file and not the URL
$fullPath    = getcwd()."./directory_where_the_file_is/".$produitFilename;

if (
$fd = fopen ($fullPath, "rb")) {
  
$fsize    =filesize($fullPath);
  
$fname    = basename ($fullPath);

  
header("Pragma: ");
  
header("Cache-Control: ");
  
header("Content-type: application/octet-stream");
  
header("Content-Disposition: attachment; filename=\"".$fname."\"");
  
header("Content-length: $fsize");

  
fpassthru($fd);
}
?>

Have fun and thx u all 4 ur great help ...

Simon (from Paris - France)
Omega2k at web dot de
27-Mar-2003 09:07
To throttle download-speed of specific files this works fine in my board hosted on my local machine:

//#######################################
 $big_file=filesize($completeFilePath)/1024; //size of file in kb
 header('Content-Type: '.$mime_type);
 header('Content-disposition: '.$content_disp.'filename="'.$attachment_name.'"');
 header('Cache-Control: no-cache');
 header('Pragma: no-cache');
 header('Expires: 0');
 header('Content-Length: '.(string)(filesize($completeFilePath)));
 $fp=fopen($completeFilePath,'r');
 while(!feof($fp)) {
     $buffer = fread($fp, 1024*6); //speed-limit 6kb/s
     if ($big_file>32 &&
     $extension!="jpg" &&
     $extension!="jpeg" &&
     $extension!="gif" &&
     $extension!="png" &&
     $extension!="txt")
     sleep(1); //if filesize>32kb and no smallfile like jpg,gif or so - wait 1 second
     print $buffer;
 }
 fclose($fp);
 header ("Connection: close");
//#######################################

I think it's the easiest way to slow down downloading files without using a loop or for-next - this really saves performace of php and is quite exact by using 1024*number_of_kb in one second...

Thats all

Greetings, omega2k.dynu.com
brett at NOSPAM dot brettbrewer dot com
05-Feb-2003 06:49
The above method worked for me after trying everything else imaginable to get Explorer to download a file via PHP. However, I had to change the content-length line. No need to "stringify" the $size variable as in the above post. The method below works for both small and very large file (tested on files larger than 30MB with no probs)...

<?php
$distribution
="/path/to/a/file.exe"
if ($fd = fopen ($distribution, "r")){

        
$size=filesize($distribution);
        
$fname = basename ($distribution);

//This is some really weak code I used just to redirect to the file before I fixed
//this problem...it makes the browser handle the download via Apache instead of PHP
//but it would be really easy to then find out the true location of the file

               //header("Location: $distribution");
             //fclose ($fd);
             //exit;

//below is a much better way to do it...

    
header("Pragma: ");
    
header("Cache-Control: ");
    
header("Content-type: application/octet-stream");
    
header("Content-Disposition: attachment; filename=\"".$fname."\"");
    
header("Content-length: $size");

         while(!
feof($fd)) {
              
$buffer = fread($fd, 2048);
               print
$buffer;
         }
        
fclose ($fd);
         exit;
}
?>

Good luck.
Brett Brewer.
ssharma at remove_me dot odc dot net
13-Dec-2002 10:01
I was having a hard time getting IE to download stuff through an ssl (https) link.  I finally found a bug report that said you need to clear out the Pragma and Cache-Control headers or else it won't send.  Here's the important part of my download script.

$contentType='application/octet-stream';
header("Pragma: ");
header("Cache-Control: ");
header("Content-type: $contentType");
header("Content-Disposition: attachment; filename=\"".$filename."\"");
header("Content-length:".(string)(filesize($fullfilepath)));

$fd=fopen($fullfilepath,'r');
fpassthru($fd);

This works perfectly over http and https in both Mozilla and IE6.  Obviously $filename just has the name of the file and $fullfilepath has the complete file system path to the file.
weezernation dot com
10-Dec-2002 12:56
When using readfile()  -or fopen() and fpassthru() - make sure that if you are dealing with large files that are located on your server, use absolute paths and not URL's! Otherwise, the file will essentially be downloaded twice - the script will access the file from your web server itself, and then output it to the client, doubling the bandwidth. I made this mistake in a download script I made that included files on other servers, when the file was on mine, I forgot to use absolute paths. So, even for good practice if your files you're accessing are small, use absolute paths whenever possible. Seems obvious, but don't forget about it.
shaun at nospam dot phplabs dot com
30-Nov-2002 08:41
Note that if you use these two headers from a previous example:

header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');

before sending a file to the browser, the "Open" option on Internet Explorer's file download dialog will not work properly. If the user clicks "Open" instead of "Save," the target application will open an empty file, because the downloaded file was not cached. The user will have to save the file to their hard drive in order to use it.

Make sure to leave these headers out if you'd like your visitors to be able to use IE's "Open" option.
john at bvstudios dot com
22-Nov-2002 04:05
In reply to:

"3. Through no amount of futzing of headers was I able to get the filename to be set properly when the actual transfer was initiated via a refresh (META or via headers).  I don't know if this is also an MSIE only issue or not.  If 'download.php?dl=now' (for example) had a refresh back to 'download.php', such that it was intended to show some information (e.g. install instructions) as well as launch the download, then the MSIE insisted that the downloaded file was supposed to be named 'download.php?dl=now' or 'download.php', ignoring the filename in the headers."

I recently had the exact same issue.  What I found is that this was due to my session initialization on the page.  For some reason doing a session_start() caused the script to try and download itself, not what I was indicating through various header() calls.

The solution was to move the download portion above the session initialization.  At first glance this may seem dangerous, but I only process it if there are POST vars and the script is reloading itself.  This way I know the form was submitted by that page and before they can submit it, they have to have a session!  Adding an .htaccess rule to deny all for the directory where the files are stored also helps because then only my script can access the files.
mikek at nospam dot muonics dot c o m
05-Nov-2002 06:07
Found a workaround to another headache that just cropped up tonight.  Apparently Opera 6.1 on Linux (unsure of other versions/platforms) has problems downloading files using the above methods if you have enabled compression via zlib.output_compression in php.ini.

It seems that Opera sees that the actual transfer size is less than the size in the "Content-length" header for the download and decides that the transfer was incomplete or corrupted.  It then either continuously retries the download or else leaves you with a corrupted file.

Solution:  Make sure your download script/section is off in its own directory. and add the following to your .htaccess file for that directory:

php_flag zlib.output_compression off
claude_minette at hotmail dot com
30-Oct-2002 07:51
This code works fine with a download manager... maybe not the best solution, but the only one that works with IE!!!!!

It forces download, but gif file don't want to be downloaded!!! so I need to simply display them in browser...

NB $file is the result of a query on the file table...

require_once("auth.inc.php");
$attachment = (strstr($HTTP_USER_AGENT, "MSIE")) ? "" : " attachment"; // IE 5.5 fix.
//Content of file
if (!headers_sent()){
   $ficexp=explode('.',$file["orig_name"]);
   $ext=$ficexp[sizeof($ficexp)-1];
   if ($ext!='gif'){
           header('Cache-Control: no-cache, must-revalidate');
           header('Pragma: no-cache');
           header("Content-Type: application/force-download");
           header("Content-Length: ".filesize("files/".$file["save_name"]));
           header("Content-Disposition: ".$attachment."; filename=".$file["orig_name"]);
   }
   $fn=fopen("files/".$file["save_name"], "rb");
   fpassthru($fn);
}
else {
   MessageBox('Headers already sent, cannot force download!');
}

Min's
mikek at muonics dot nospam dot c dot o
07-Oct-2002 06:44
Found a workaround for the MSIE cache bug that puts brackets around dotted items I posted about a while back (e.g. "somefile1.0-xyz.zip" becoming "somefile[1][0]-xyz.zip").

It turns out if you encode all but the last dot as %2e, then MSIE won't do this.  If you encode all of them (including the last dot), then MSIE sticks an extra bracketed number at the end of the file (e.g. "somefile1.0-xyz.zip[1]").  Unfortunately, however, some other browsers then want to save the file with the %2e in the filename instead of the dots.

if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
{
   $fileName = preg_replace('/\./', '%2e', $fileName,
       substr_count($fileName, '.') - 1);
}

Viola.  Properly named files.  This works at least with MSIE 6.0.
mikek at muonics dot nospam dot c dot o
04-Oct-2002 11:48
If your downloaded files are getting corrupted, one of the scripts included/required in your download script or page may have whitespace around the <?php ?> tags.  A common enough problem, but most often recognized when header() fails, due to headers already being sent, but one worth mention here.

This one bit me just recently with my download script.  Somewhere along the way adding functionality to my website, I wound up with a space (not a blank line, which I usually spot right away, but a single space character) after the closing ?> tag in one of the require()'d files.  Oddly enough, all the downloads seemed to work ok, but the files were corrupted: that space character wound up at the beginning of each file.
mikek at muonics dot nospam dot c dot o dot m
16-Sep-2002 10:07
Replies to the above (10-Sep-2002 05:38 and 10-Sep-2002 06:17) anon posts:

10-Sep-2002 05:38 -- Using temporary redirects in the manner you describe only work if you have the files in a location which are directly accessible by a regular HTTP request (temporarily or not).  This isn't an effective solution for me, because it is a specific requirement of my implementation that certain files are accessible only through PHP: user permissions for downloading files are handled via PHP and a user/license database instead of traditional .htaccess methods.  In the past (before PHP) I used CGI scripts to create temporary per-downloader symlinks to the files and generated .htaccess files, along with htpasswd/group files, but it was a sloppy method because the symlinks would linger (or be deleted prematurely), and wasn't conducive to online order processing etc.  This method (via fpassthru()) is much better: there is no direct access to any file, by design.

10-Sep-2002 06:17 - My implementation does set the Content-Length header.  It does not help.  Some browsers' download dialogs still linger for 15-30 seconds after the download is complete, leading some users to think the download has stalled and to reinitiate it.  In those browsers where this has been a problem and I've tested it, the "Connection: close" header has worked great.  It's not abused: the download manager implementation is such that the header is sent only for HTTP requests that directly result in the download occuring.  It might be worthwhile investigating whether or not there is a php hook into the server's watchdog timer so that it can be set on a per-script basis and use that; but setting it to a low value globally seems just as bad as sending "Connection: close" for every page would be.
10-Sep-2002 08:17
Note that the above comment about the "Connection: close" header is incorrect: it does not guarantee that the connection will be closed immediately after the transfer is complete. Instead, it informs the client that it can no longer use the existing HTTP connection to perform other HTTP requests on the same server, and that the client MUST close the connection as soon as it has finished handling the current request.

If the client (for example an old HTTP proxy) is using HTTP/1.0, it may not recognize this header, and could could the connection open; the web server should detect this and close the connection and ignore any further request attempt on that connection.

HTTP/1.1 clients MUST honor this header and close their connection as soon as they detect the end of the answer.

In any case, the web server will initiate a watchdog after script completion, and will force the deconnection after about 15 to 30 seconds if the client does not honor this header.

The exact time to wait for the "socket closed by remote" event is configurable in the web server.

It is generally smaller when the "Connection: close" header has been sent by the server, than when no "Connection:close" has been sent (in which case the connection persists for longer time, to let the client navigate on the server without enduring new connection costs in terms of: connection delays, number of socket control blocks in final wait state, number of used ports).

Don't abuse "Connection: close" on your server for every hosted page: this creates more incoming TCP connection attempts than necessary, and slows the navigation on your site. Use it only if your script cannot generate explicit content length in the result header, as the client will have difficulties to determine the end of the results.

If you want to save connection resources to your server, always send an Explicit "Content-Length" header within your script, or use the "chunked" transfer-encoding to explicitly send the result by delimited fragments (if the client is using HTTP/1.1, it MUST support this chunked transfer encoding, per specification). See RFC2616 for details.
10-Sep-2002 07:38
fpassthru() works best for small files. In download manager scripts, it's best to determine the URL of the file to download (you may generate it locally in your session data if you need so), and then use HTTP __temporary__ redirects (302 status code, with a "Location:" header specifying the effective download URL).

This saves your web server from maintaining PHP scripts running for long times during the file downloadn and instead the download will be managed directly by the web server without scripting support (consequence: less memory resources used by parallel downloads)...
me at gavinadams dot org
30-Aug-2002 10:06
Interesting results using fpassthru() vs. fread() under UNIX.

Using fread(fp, length) to read from a valid, open pointer, in which the filename has a special character (single quote, comma, open paren, etc) fails on the read (no debug statements written after that). However, using fpassthru() works like a champ.

Thanks for the helpful notes on IE session info, have seen this before but didn't know what was causing it.
mikek at muonics dot nospam dot c dot o dot m
25-Aug-2002 12:14
A few notes on using fpassthru() to php-driven download links that pop up a "Save As.." dialog:

1. I found that the download progress dialog was remaining up for several seconds after the transfer was completed, before telling the user it was complete.  This was fixed by adding the following header:

header ("Connection: close");

This will cause the connection to be closed as soon as the transfer is complete, rather than waiting for a timeout.

2. If you have multiple periods in the filename, you might wind up with a filename with numbers in brackets (such as myfile-[1][0]-windows.zip when you put myfile-1.0-windows.zip in the headers) with MSIE.  According to Microsoft's KB, his is a "known" bug having to due with MSIE's cache and there's no workaround that I was able to find.

3. Through no amount of futzing of headers was I able to get the filename to be set properly when the actual transfer was initiated via a refresh (META or via headers).  I don't know if this is also an MSIE only issue or not.  If 'download.php?dl=now' (for example) had a refresh back to 'download.php', such that it was intended to show some information (e.g. install instructions) as well as launch the download, then the MSIE insisted that the downloaded file was supposed to be named 'download.php?dl=now' or 'download.php', ignoring the filename in the headers.
-
25-Jul-2002 07:37
If you trying to output a user-written file on a page for verifying, editing, etc, you'll want to use fopen(), fread(), htmlentities() to avoid malicious code. Text from fpassthru, while not parsed per se can still mess up the display of a page (or at least it did for me!) --mt.
josh at trutwins dot homeip dot net
24-Apr-2002 08:03
I could not get the above examples to work.  This is what I used instead:

header("Content-Disposition: attachment; filename=$file");
header("Content-Description: Image File");
$fd = fopen($file,'r');
fpassthru($fd);
php at brayra dot com
05-Apr-2002 04:25
Here is a final working copy that won't freak out Microsoft Explorer if you are using sessions. Thanks to everyone else who came before. This is not as simple as I thought it would be.

the user would pass a call to the page:
http://mysite/getfile.php?file=products.pdf

include 'base.inc'; // inlcude base code, start session  and manage users

// This loads the file global from the post/get variables
// For security reasons register globals is disabled
LoadPostGet('file');

$filename = '/data/files/' . $file;
if(file_exists($filename)){
  $FILECMD = '/usr/bin/file';
  $contentType = '';
  $fp=popen("$FILECMD -bin $filename", 'r');
  if (!$fp) $contentType='application/octet-stream';
  else {
   while($string=fgets($fp, 1024)) $contentType .= $string;
   pclose($fp);
  }
  if(strpos($HTTP_SERVER_VARS['HTTP_USER_AGENT'], 'MSIE')){
   // IE cannot download from sessions without a cache
   header('Cache-Control: public');
  }
  header("Content-type: $contentType");
  header("Content-Disposition:inline; filename=\"".$file."\"");
  header("Content-length:".(string)(filesize($filename)));
  $fd=fopen($filename,'rb');
  while(!feof($fd)) {
   print fread($fd, 4096);
  }
  fclose($fd);
}else{
  print "File Not Found";
}
mirko at mcaserta dot com
23-Jan-2002 09:07
Update to the above. This also sets the correct mime type for the file you're sending. It's a small hack since it relies on the "file" system command but it should work well.

<?
// full path to the file command
$FILECMD='/usr/bin/file';
// directory where the file resides
$fileDir='/home/mcaserta';
// full file name
$fileName='test.sh';

// END CONFIG

$completeFilePath=$fileDir.'/'.$fileName;
$fp=popen("$FILECMD -bin $completeFilePath", 'r');

if (!
$fp) $contentType='application/octet-stream';
else {
  while(
$string=fgets($fp, 1024)) $contentType .= $string;
 
pclose($fp);
}

header('Content-type: '.($contentType));
header('Content-Disposition: inline; filename="'.($fileName).'"');
header('Content-length: '.(string)(filesize($completeFilePath)));
$fd=fopen($completeFilePath,'r');
fpassthru($fd);
?>
mirko at mcaserta dot com
17-Jan-2002 03:34
To display a "Save as..." box which displays the name of the file you're sending to the user:

<?
$completeFilePath
=$fileDir.'/'.$fileName;
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header("Content-type: application/octet-stream\nContent-Disposition: inline; filename=\"".$fileName."\"\nContent-length: ".(string)(filesize($completeFilePath)));
$fd=fopen($completeFilePath,'r');
fpassthru($fd);
?>

The first two header calls are only needed to make sure the client doesn't cache the document.
straz at -removethispart-mac dot com
15-Jan-2002 04:22
I wrote a page which authenticates the user, then calls fpassthru() to download an Acrobat document. It worked great up to about 1MB, but for larger files, the script was dying in the middle. My ISP told me they were killing my script because it was a memory hog. I tried readfile() instead, to no avail.

I replaced the fpassthru() with this workaround. It works great:

 while(!feof($fn)) {
   $buffer = fread($fn, 4096);
   print $buffer;
 }
cgriffin at websales dot com
31-Oct-1999 07:56
If you open a new file, write to it and then call fpassthru() it doesn't work. You need to call rewind() first to set the file pointer to the begining of the file.