views:

572

answers:

5

Hello,

I'm using forced download to download mostly zips and mp3s on site i did (http://pr1pad.kissyour.net) - to track downloads in google analytics, in database and to hide real download path:

It's this:

extending CI model

... - bunch of code

function _fullread ($sd, $len) {
 $ret = '';
 $read = 0;
 while ($read < $len && ($buf = fread($sd, $len - $read))) {
  $read += strlen($buf);
  $ret .= $buf;
 }
 return $ret;
}

function download(){    
    /* DOWNLOAD ITSELF */

    ini_set('memory_limit', '160M');
    apache_setenv('no-gzip', '1');
    ob_end_flush();

    header("Pragma: public");
    header("Expires: 0");
    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
    header("Cache-Control: public",FALSE);
    header("Content-Description: File Transfer");
    header("Content-type: application/octet-stream");
     if (isset($_SERVER['HTTP_USER_AGENT']) && 
      (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false))
      header('Content-Type: application/force-download'); //IE HEADER
    header("Accept-Ranges: bytes");
    header("Content-Disposition: attachment; filename=\"" . basename("dir-with-    files/".$filename) . "\";");
    header("Content-Transfer-Encoding: binary");
    header("Content-Length: " . filesize("dir-with-files/".$filename));

    // Send file for download
    if ($stream = fopen("dir-with-files/$filename", 'rb')){
     while(!feof($stream) && connection_status() == 0){
      //reset time limit for big files
      set_time_limit(0);
      print($this->_fullread($stream,1024*16));
      flush();
     }
     fclose($stream);
    }
}

It's on LAMP with CI 1.7.2 - It's my own method put together from various how-tos all over the internet, because during developement, these problems came up: - server limit. ini_set haven't helped, so I used buffered _fullread instead normal fread, which was used insted of @readonly - ob_end_flush(), because site is did in CI1.7.2 and i needed to clean buffer

Now... It doesn't work. It did, then it stopped showing expected size/download time - I tried to clean it up and while I was cleaning up the code, something happened, I don't know what and in any previous version - it haven't worked (no change in settings whatsoever) - edit: don't work = outputs everything into browser window.

So I said, screw it, I'll look here.

So, I basically look for script or function, which i can put to my output model and will do:

  • Call force-download (in Chrome start download, in IE,FF,Safari open the modal open/save/cancel)
  • Show size of file and estimated dl time (that's up to browser, i know, but first, browser must know filesize
  • WORK (tested & confirmed!) in IE6,7,8, FF3, Opera, Chrome & and safari on PC + Mac (Linux... I don't really care) - that's for header part
  • on server, I have also something like 56MB memory limit, which i can't add to, so that's also important

Thank you in advance.

Edit: Now I feel more screwed then ever/before, since I tried to force download with .htaccess - while it worked, it had few minor/major (pick yours) problems

  • it showed full path (minor for me)
  • it waits until whole download is finished (showing as "connecting") and then just show it's downloading - and downloads in one second (major for me)

Now, although I deleted .htaccess, it still waits until download is complete (just as if it was downloading to cache first) and it just get's connected and show open/save dialog.

A: 

print($this->_fullread($stream,1024*16));

I assume _fullread is within a class? If the code looks like the above then $this-> wouldn't work.

Does it output the file contents to the screen if you commented out all of the header stuff?

fire
Hello, yes it's in class - it's actually extended CI model. And no, it outputs to browser WITH all the header stuff.
Adam Kiss
I also edited code so you know :)
Adam Kiss
A: 

Just a shot in the dark... every header that I send in my 'force download' code (which is not as well tested as yours) is the same as yours, except I call: header("Cache-Control: private",false);

instead of: header("Cache-Control: public",FALSE);

I don't know if that will help or not.

Narcissus
A: 

If you are going to do this sort of "Echo it out with php" method, then you will not be able to show a remaining time, or an expected size to your users. Why? Because if the browser tries to resume your download in the middle, you have no way of handling that case in PHP.

If you have a normal file download, Apache is capable of supporting resumed downloads over HTTP, but in the case a download is paused, Apache has no way of figuring out where in your script things were executing when a client asks for the next chunk.

Essentially, when a browser pauses a download, it will terminate the connection to the webserver entirely. When you resume the download, the connection is reopened, and the request contains a flag saying "Start from byte number X". But to the webserver looking at your PHP above, where does byte X come from?

While in theory it might be possible for the server to identify where to resume your script in the event of an interrupted download, Apache does not attempt to figure out where to resume. As a result, the header sent to the browser states that the server does not support resume, which turns off the expected filesize and time limit items in most major browsers.

EDIT: It seems you might be able to handle this case, but it's going to take a LOT of code on your part. See http://www.php.net/manual/en/function.fread.php#84115 .

Billy ONeal
it did show estimated time, since part of headers is filesize - that part, however did not work. I agree, that making download http-resumable may be a bit overkill for this one :)
Adam Kiss
+1  A: 

There's one thing I find weird: You are calling ob_end_flush() at the start of the function. This actually cleans the output buffer, but it also outputs everything to the client first (I assume including Content-Headers set by CodeIgniter). Change the call to ob_end_clean(), it clears the buffer and discards it. This will give you a clean start for generating your own headers.

Another tip:

Instead of reading the file as a stream and passing it on block-wise, you could give this function a try:

// ...
if (file_exists("dir-with-files/$filename")) {
   readfile($file);
}

This takes care of nearly everything.

Cassy
I Agree, use readfile it wont lead you to memory throubles. An i also recomend to use dirname(__FILE__).'/../../thatpathwithfiles/' everytime you assemble a path.
useless
It is **proved** that readfile (as i wrote) lead me to memory troubles already - i'm not sure, but it might to try allocate enough memory for file - what server does not support.
Adam Kiss
+1 for `ob_end_clean()`
Adam Kiss
A: 

So, I used this code (It's modified version of resumable http download found on internet)

function _output_file($file, $path)
{
    $size = filesize($path.$file);

    @ob_end_clean(); //turn off output buffering to decrease cpu usage

    // required for IE, otherwise Content-Disposition may be ignored
    if(ini_get('zlib.output_compression'))
    ini_set('zlib.output_compression', 'Off');

    header('Content-Type: application/force-download');
    header('Content-Disposition: attachment; filename="'.basename($file).'"');
    header("Content-Transfer-Encoding: binary");
    header('Accept-Ranges: bytes');

    /* The three lines below basically make the 
    download non-cacheable */
    header("Cache-control: no-cache, pre-check=0, post-check=0");
    header("Cache-control: private");
    header('Pragma: private');
    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");

    // multipart-download and download resuming support
    if(isset($_SERVER['HTTP_RANGE']))
    {
        list($a, $range) = explode("=",$_SERVER['HTTP_RANGE'],2);
        list($range) = explode(",",$range,2);
        list($range, $range_end) = explode("-", $range);
        $range=intval($range);
        if(!$range_end) {
            $range_end=$size-1;
        } else {
            $range_end=intval($range_end);
        }

        $new_length = $range_end-$range+1;
        header("HTTP/1.1 206 Partial Content");
        header("Content-Length: $new_length");
        header("Content-Range: bytes $range-$range_end/$size");
    } else {
        $new_length=$size;
        header("Content-Length: ".$size);
    }

    /* output the file itself */
    $chunksize = 1*(1024*1024); //you may want to change this
    $bytes_send = 0;
    if ($file = fopen($path.$file, 'rb'))
    {
        if(isset($_SERVER['HTTP_RANGE']))
        fseek($file, $range);

        while
            (!feof($file) && 
             (!connection_aborted()) && 
             ($bytes_send<$new_length) )
        {
            $buffer = fread($file, $chunksize);
            print($buffer); //echo($buffer); // is also possible
            flush();
            $bytes_send += strlen($buffer);
        }
    fclose($file);
    } else die('Error - can not open file.');

die();
}

and then in model:

function download_file($filename){
    /*
        DOWNLOAD
    */
    $path = "datadirwithmyfiles/"; //directory

    //track analytics

    include('includes/Galvanize.php'); //great plugin
    $GA = new Galvanize('UA-XXXXXXX-7');
    $GA->trackPageView();

    $this->_output_file($filename, $path);

}

It works as expected in all mentiond browser on Win / MAC - so far, no problems with it.

Adam Kiss