I always used the function become_file_download from the open source BalPHP , which you can connect and play your project right away. This allows:
- Instant File Download
- Easily specify options such as content type, cache life, buffer size.
- Support for multi-part transactions providing faster downloads that will not kill your server for large file transfers.
- Supports pause / resume functions.
- And etags for caching:
You can find the latest version here: http://github.com/balupton/balphp/blob/master/trunk/lib/core/functions/_files.funcs.php#L75
And here it is copied and pasted from August 27, 2010:
/** * Become a file download, should be the last script that runs in your program * * http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt * * @version 3, July 18, 2009 (Added suport for data) * @since 2, August 11, 2007 * * @author Benjamin "balupton" Lupton < contact@balupton.com > - {@link http://www.balupton.com/} * * @param string $file_path * @param string $content_type * @param int $buffer_size * @param string $file_name * @param timestamp $file_time * * @return boolean true on success, false on error */ function become_file_download ( $file_path_or_data, $content_type = NULL, $buffer_size = null, $file_name = null, $file_time = null, $expires = null ) { // Prepare if ( empty($buffer_size) ) $buffer_size = 4096; if ( empty($content_type) ) $content_type = 'application/force-download'; // Check if we are data $file_descriptor = null; if ( file_exists($file_path_or_data) && $file_descriptor = fopen($file_path_or_data, 'rb') ) { // We could be a file // Set some variables $file_data = null; $file_path = $file_path_or_data; $file_name = $file_name ? $file_name : basename($file_path); $file_size = filesize($file_path); $file_time = filemtime($file_path); $etag = md5($file_time . $file_name); } elseif ( $file_name !== null ) { // We are just data $file_data = $file_path_or_data; $file_path = null; $file_size = strlen($file_data); $etag = md5($file_data); if ( $file_time === null ) $file_time = time(); else $file_time = ensure_timestamp($file_time); } else { // We couldn't find the file header('HTTP/1.1 404 Not Found'); return false; } // Prepare timestamps $expires = ensure_timestamp($expires); // Set some variables $date = gmdate('D, d MYH:i:s') . ' GMT'; $expires = gmdate('D, d MYH:i:s', $expires) . ' GMT'; $last_modified = gmdate('D, d MYH:i:s', $file_time) . ' GMT'; // Say we can go on forever set_time_limit(0); // Check relevance $etag_relevant = !empty($_SERVER['HTTP_IF_NONE_MATCH']) && trim(stripslashes($_SERVER['HTTP_IF_NONE_MATCH']), '\'"') === $etag; $date_relevant = !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $file_time; // Handle download if ( $etag_relevant || $date_relevant ) { // Not modified header('HTTP/1.0 304 Not Modified'); header('Status: 304 Not Modified'); header('Pragma: public'); header('Cache-Control: private'); header('ETag: "' . $etag . '"'); header('Date: ' . $date); header('Expires: ' . $expires); header('Last-modified: ' . $last_modified); return true; } elseif ( !empty($_SERVER['HTTP_RANGE']) ) { // Partial download /* * bytes=0-99,500-1499,4000- */ // Explode RANGE list($size_unit,$ranges) = explode($_SERVER['HTTP_RANGE'], '=', 2); // Explode RANGES $ranges = explode(',', $ranges); // Cycle through ranges foreach ( $ranges as $range ) { // We have a range /* * All bytes until the end of document, except for the first 500 bytes: * Content-Range: bytes 500-1233/1234 */ // Set range start $range_start = null; if ( !empty($range[0]) && is_numeric($range[0]) ) { // The range has a start $range_start = intval($range[0]); } else { $range_start = 0; } // Set range end if ( !empty($range[1]) && is_numeric($range[1]) ) { // The range has an end $range_end = intval($range[1]); } else { $range_end = $file_size - 1; } // Set the range size $range_size = $range_end - $range_start + 1; // Set the headers header('HTTP/1.1 206 Partial Content'); header('Pragma: public'); header('Cache-Control: private'); header('ETag: "' . $etag . '"'); header('Date: ' . $date); header('Expires: ' . $expires); header('Last-modified: ' . $last_modified); header('Content-Transfer-Encoding: binary'); header('Accept-Ranges: bytes'); header('Content-Range: bytes ' . $range_start . '-' . $range_end . '/' . $file_size); header('Content-Length: ' . $range_size); header('Content-Type: ' . $content_type); if ( $content_type === 'application/force-download' ) header('Content-Disposition: attachment; filename=' . urlencode($file_name)); // Handle our data transfer if ( !$file_path ) { // We are using file_data echo substr($file_data, $range_start, $range_end - $range_start); } else { // Seek to our location fseek($file_descriptor, $range_start); // Read the file $remaining = $range_size; while ( $remaining > 0 ) { // 0-6 | buffer = 3 | remaining = 7 // 0,1,2 | buffer = 3 | remaining = 4 // 3,4,5 | buffer = 3 | remaining = 1 // 6 | buffer = 1 | remaining = 0 // Set buffer size $buffer_size = min($buffer_size, $remaining); // Output file contents echo fread($file_descriptor, $buffer_size); flush(); ob_flush(); // Update remaining $remaining -= $buffer_size; } } } } else { // Usual download // header('Pragma: public'); // header('Cache-control: must-revalidate, post-check=0, pre-check=0'); // header('Expires: '. gmdate('D, d MYH:i:s').' GMT'); // Set headers header('HTTP/1.1 200 OK'); header('Pragma: public'); header('Cache-Control: private'); header('ETag: "' . $etag . '"'); header('Date: ' . $date); header('Expires: ' . $expires); header('Last-modified: ' . $last_modified); header('Content-Transfer-Encoding: binary'); header('Accept-Ranges: bytes'); header('Content-Length: ' . $file_size); header('Content-Type: ' . $content_type); if ( $content_type === 'application/force-download' ) header('Content-Disposition: attachment; filename=' . urlencode($file_name)); // Handle our data transfer if ( !$file_path ) { // We are using file_data echo $file_data; } else { // Seek to our location // Read the file $file_descriptor = fopen($file_path, 'r'); while ( !feof($file_descriptor) ) { // Output file contents echo fread($file_descriptor, $buffer_size); flush(); ob_flush(); } } } // Close the file if ( $file_descriptor ) fclose($file_descriptor); // Done return true; }
It also depends on another plug and play function called secure_timestamp , which you can find here: http://github.com/balupton/balphp/blob/master/trunk/lib/core/functions/_datetime.funcs.php#L31
function ensure_timestamp ( $value = null ) { $result = null; if ( $value === null ) $result = time(); elseif ( is_numeric($value) ) $result = $value; elseif ( is_string($value) ) $result = strtotime($value); else throw new Exception('Unknown timestamp type.'); return $result; }