php
PHP - 파일 다운로드 이어받기
Content-Range 헤더는 파일 다운로드 이어받기 할 때 필요한 부분입니다. 동시에 $_SERVER['HTTP_RANGE'] 변수를 같이 사용하고 이로서 이어받기가 가능해집니다. 헤더부분은 Content-Range: bytes 다운받은 크기/전체크기 로 표현합니다. 다음을 참고하면 이해되리라 봅니다. (참고 문서)
헤더 부분은 다음과 같이 표현됩니다.
이러한 자료를 토대로 파일 이어받기는 다음과 같이 구현할 수 있겠습니다.
출처: http://php.net/manual/en/function.fread.php
출처: http://nc.php.net/manual/vote-note.php?id=50904&page=function.fread&vote=up
출처: http://php.net/manual/ja/function.readfile.php
출처: http://pear.php.net/package/HTTP_Download/docs/latest/__filesource/fsource_HTTP_Download__HTTP_Download-1.1.4HTTPDownload.php.html
Actual result:
--------------
$ telnet dev.conduit-it.com 80
Trying 10.42.84.2...
Connected to dev.conduit-it.com.
Escape character is '^]'.
GET /test.php HTTP/1.1
Host:dev.conduit-it.com
Range:bytes=0-24
Connection:close
HTTP/1.1 206 Partial Content
Date: Fri, 29 Aug 2008 03:43:20 GMT
Content-Range: bytes 0-24/8000
Content-Length: 25
Connection: close
Content-Type: text/html
.........................Connection closed by foreign host.
$ telnet dev.conduit-it.com 80
Trying 10.42.84.2...
Connected to dev.conduit-it.com.
Escape character is '^]'.
GET /test.php HTTP/1.1
Host:dev.conduit-it.com
Range:bytes=0-24,50-74
Connection:close
HTTP/1.1 206 Partial Content
Date: Fri, 29 Aug 2008 03:45:44 GMT
Content-Length: 240
Connection: close
Content-Type: multipart/byteranges; boundary=455911696d6f354a2
--455911696d6f354a2
Content-type: text/html
Content-range: bytes 0-24/8000
.........................
--455911696d6f354a2
Content-type: text/html
Content-range: bytes 50-74/8000
.........................
--455911696d6f354a2--
Connection closed by foreign host.
헤더 부분은 다음과 같이 표현됩니다.
Accept-Ranges: bytes
Content-Length: {filesize}
Content-Range: bytes 10-{filesize-1}/{filesize}
이러한 자료를 토대로 파일 이어받기는 다음과 같이 구현할 수 있겠습니다.
<?php
function download_file($file_name) {
if (!file_exists($file_name)) { die("<b>404 File not found!</b>"); }
$file_extension = strtolower(substr(strrchr($file_name,"."),1));
$file_size = filesize($file_name);
$md5_sum = md5_file($file_name);
//This will set the Content-Type to the appropriate setting for the file
switch($file_extension) {
case "exe": $ctype="application/octet-stream"; break;
case "zip": $ctype="application/zip"; break;
case "mp3": $ctype="audio/mpeg"; break;
case "mpg":$ctype="video/mpeg"; break;
case "avi": $ctype="video/x-msvideo"; break;
//The following are for extensions that shouldn't be downloaded (sensitive stuff, like php files)
case "php":
case "htm":
case "html":
case "txt":
die("<b>Cannot be used for ". $file_extension . " files!</b>");
break;
default: $ctype="application/force-download";
}
if (isset($_SERVER['HTTP_RANGE'])) {
$partial_content = true;
$range = explode("-", $_SERVER['HTTP_RANGE']);
$offset = intval($range[0]);
$length = intval($range[1]) - $offset;
}
else {
$partial_content = false;
$offset = 0;
$length = $file_size;
}
//read the data from the file
$handle = fopen($file_name, 'r');
$buffer = '';
fseek($handle, $offset);
$buffer = fread($handle, $length);
$md5_sum = md5($buffer);
if ($partial_content)
$data_size = intval($range[1]) - intval($range[0]);
else
$data_size = $file_size;
fclose($handle);
// send the headers and data
header("Content-Length: " . $data_size);
header("Content-md5: " . $md5_sum);
header("Accept-Ranges: bytes");
if ($partial_content)
header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $file_size);
header("Connection: close");
header("Content-type: " . $ctype);
header('Content-Disposition: attachment; filename=' . $file_name);
echo $buffer;
flush();
}
?>
출처: http://php.net/manual/en/function.fread.php
<?php
function dl_file_resume($file){
//First, see if the file exists
if (!is_file($file)) { die("<b>404 File not found!</b>"); }
//Gather relevent info about file
$len = filesize($file);
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename,"."),1));
//This will set the Content-Type to the appropriate setting for the file
switch( $file_extension ) {
case "exe": $ctype="application/octet-stream"; break;
case "zip": $ctype="application/zip"; break;
case "mp3": $ctype="audio/mpeg"; break;
case "mpg":$ctype="video/mpeg"; break;
case "avi": $ctype="video/x-msvideo"; break;
//The following are for extensions that shouldn't be downloaded (sensitive stuff, like php files)
case "php":
case "htm":
case "html":
case "txt": die("<b>Cannot be used for ". $file_extension ." files!</b>"); break;
default: $ctype="application/force-download";
}
//Begin writing headers
header("Pragma: public");
header("Expires: 0");
header("Cache-Control:");
header("Cache-Control: public");
header("Content-Description: File Transfer");
//Use the switch-generated Content-Type
header("Content-Type: $ctype");
$filespaces = str_replace("_", " ", $filename);
//if your filename contains underscores, you can replace them with spaces
$header='Content-Disposition: attachment; filename='.$filespaces.';';
header($header );
header("Content-Transfer-Encoding: binary");
$size=filesize($file);
//check if http_range is sent by browser (or download manager)
if(isset($_ENV['HTTP_RANGE'])) {
list($a, $range)=explode("=",$_ENV['HTTP_RANGE']);
//if yes, download missing part
str_replace($range, "-", $range);
$size2=$size-1;
header("Content-Range: $range$size2/$size");
$new_length=$size2-$range;
header("Content-Length: $new_length");
//if not, download whole file
} else {
$size2=$size-1;
header("Content-Range: bytes 0-$size2/$size");
header("Content-Length: ".$size2);
}
//open the file
$fp=fopen("$file","r");
//seek to start of missing part
fseek($fp,$range);
//start buffered download
while(!feof($fp)) {
//reset time limit for big files
set_time_limit();
print(fread($fp,1024*8));
flush();
}
fclose($fp);
exit;
}
?>
/*
사용 예제
<?php
dl_file_resume("somefile.mp3");
?>
*/
출처: http://nc.php.net/manual/vote-note.php?id=50904&page=function.fread&vote=up
<?php
function smartReadFile($location, $filename, $mimeType='application/octet-stream'){
if(!file_exists($location))
{
header ("HTTP/1.0 404 Not Found");
return;
}
$size=filesize($location);
$time=date('r',filemtime($location));
$fm=@fopen($location,'rb');
if(!$fm)
{
header ("HTTP/1.0 505 Internal server error");
return;
}
$begin=0;
$end=$size;
if(isset($_SERVER['HTTP_RANGE']))
{
if(preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches))
{
$begin=intval($matches[0]);
if(!empty($matches[1]))
$end=intval($matches[1]);
}
}
if($begin>0||$end<$size)
header("HTTP/1.0 206 Partial Content");
else
header('HTTP/1.0 200 OK');
header("Content-Type: $mimeType");
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Accept-Ranges: bytes');
header('Content-Length:'.($end-$begin));
header("Content-Range: bytes $begin-$end/$size");
header("Content-Disposition: inline; filename=$filename");
header("Content-Transfer-Encoding: binary\n");
header("Last-Modified: $time");
header('Connection: close');
$cur=$begin;
fseek($fm,$begin,0);
while(!feof($fm)&&$cur<$end&&(connection_status()==0))
{ print fread($fm,min(1024*16,$end-$cur));
$cur+=1024*16;
}
}
?>
/*
사용 예제
<?php
smartReadFile("/tmp/filename","myfile.mp3","audio/mpeg");
?>
*/
출처: http://php.net/manual/ja/function.readfile.php
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* HTTP::Download
*
* PHP versions 4 and 5
*
* @category HTTP
* @package HTTP_Download
* @author Michael Wallner <mike@php.net>
* @copyright 2003-2005 Michael Wallner
* @license BSD, revised
* @version CVS: $Id: Download.php 304423 2010-10-15 13:36:46Z clockwerx $
* @link http://pear.php.net/package/HTTP_Download
*/
// {{{ includes
/**
* Requires PEAR
*/
require_once 'PEAR.php';
/**
* Requires HTTP_Header
*/
require_once 'HTTP/Header.php';
// }}}
// {{{ constants
/**#@+ Use with HTTP_Download::setContentDisposition() **/
/**
* Send data as attachment
*/
define('HTTP_DOWNLOAD_ATTACHMENT', 'attachment');
/**
* Send data inline
*/
define('HTTP_DOWNLOAD_INLINE', 'inline');
/**#@-**/
/**#@+ Use with HTTP_Download::sendArchive() **/
/**
* Send as uncompressed tar archive
*/
define('HTTP_DOWNLOAD_TAR', 'TAR');
/**
* Send as gzipped tar archive
*/
define('HTTP_DOWNLOAD_TGZ', 'TGZ');
/**
* Send as bzip2 compressed tar archive
*/
define('HTTP_DOWNLOAD_BZ2', 'BZ2');
/**
* Send as zip archive
*/
define('HTTP_DOWNLOAD_ZIP', 'ZIP');
/**#@-**/
/**#@+
* Error constants
*/
define('HTTP_DOWNLOAD_E_HEADERS_SENT', -1);
define('HTTP_DOWNLOAD_E_NO_EXT_ZLIB', -2);
define('HTTP_DOWNLOAD_E_NO_EXT_MMAGIC', -3);
define('HTTP_DOWNLOAD_E_INVALID_FILE', -4);
define('HTTP_DOWNLOAD_E_INVALID_PARAM', -5);
define('HTTP_DOWNLOAD_E_INVALID_RESOURCE', -6);
define('HTTP_DOWNLOAD_E_INVALID_REQUEST', -7);
define('HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE', -8);
define('HTTP_DOWNLOAD_E_INVALID_ARCHIVE_TYPE', -9);
/**#@-**/
// }}}
/**
* Send HTTP Downloads/Responses.
*
* With this package you can handle (hidden) downloads.
* It supports partial downloads, resuming and sending
* raw data ie. from database BLOBs.
*
* <i>ATTENTION:</i>
* You shouldn't use this package together with ob_gzhandler or
* zlib.output_compression enabled in your php.ini, especially
* if you want to send already gzipped data!
*
* @access public
* @version $Revision: 304423 $
*/
class HTTP_Download
{
// {{{ protected member variables
/**
* Path to file for download
*
* @see HTTP_Download::setFile()
* @access protected
* @var string
*/
var $file = '';
/**
* Data for download
*
* @see HTTP_Download::setData()
* @access protected
* @var string
*/
var $data = null;
/**
* Resource handle for download
*
* @see HTTP_Download::setResource()
* @access protected
* @var int
*/
var $handle = null;
/**
* Whether to gzip the download
*
* @access protected
* @var bool
*/
var $gzip = false;
/**
* Whether to allow caching of the download on the clients side
*
* @access protected
* @var bool
*/
var $cache = true;
/**
* Size of download
*
* @access protected
* @var int
*/
var $size = 0;
/**
* Last modified
*
* @access protected
* @var int
*/
var $lastModified = 0;
/**
* HTTP headers
*
* @access protected
* @var array
*/
var $headers = array(
'Content-Type' => 'application/x-octetstream',
'Pragma' => 'cache',
'Cache-Control' => 'public, must-revalidate, max-age=0',
'Accept-Ranges' => 'bytes',
'X-Sent-By' => 'PEAR::HTTP::Download'
);
/**
* HTTP_Header
*
* @access protected
* @var object
*/
var $HTTP = null;
/**
* ETag
*
* @access protected
* @var string
*/
var $etag = '';
/**
* Buffer Size
*
* @access protected
* @var int
*/
var $bufferSize = 2097152;
/**
* Throttle Delay
*
* @access protected
* @var float
*/
var $throttleDelay = 0;
/**
* Sent Bytes
*
* @access public
* @var int
*/
var $sentBytes = 0;
/**
* Startup error
*
* @var PEAR_Error
* @access protected
*/
var $_error = null;
// }}}
// {{{ constructor
/**
* Constructor
*
* Set supplied parameters.
*
* @access public
* @param array $params associative array of parameters
* <strong>one of:</strong>
* <ul>
* <li>'file' => path to file for download</li>
* <li>'data' => raw data for download</li>
* <li>'resource' => resource handle for download</li>
* </ul>
* <strong>and any of:</strong>
* <ul>
* <li>'cache' => whether to allow cs caching</li>
* <li>'gzip' => whether to gzip the download</li>
* <li>'lastmodified' => unix timestamp</li>
* <li>'contenttype' => content type of download</li>
* <li>'contentdisposition' => content disposition</li>
* <li>'buffersize' => amount of bytes to buffer</li>
* <li>'throttledelay' => amount of secs to sleep</li>
* <li>'cachecontrol' => cache privacy and validity</li>
* </ul>
*
* 'Content-Disposition' is not HTTP compliant, but most browsers
* follow this header, so it was borrowed from MIME standard.
*
* It looks like this:
* "Content-Disposition: attachment; filename=example.tgz".
*
* @see HTTP_Download::setContentDisposition()
*/
function HTTP_Download($params = array())
{
$this->HTTP = &new HTTP_Header;
$this->_error = $this->setParams($params);
}
// }}}
// {{{ public methods
/**
* Set parameters
*
* Set supplied parameters through its accessor methods.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param array $params associative array of parameters
*
* @see HTTP_Download::HTTP_Download()
*/
function setParams($params)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
foreach((array) $params as $param => $value){
$method = 'set'. $param;
if (!method_exists($this, $method)) {
return PEAR::raiseError(
"Method '$method' doesn't exist.",
HTTP_DOWNLOAD_E_INVALID_PARAM
);
}
$e = call_user_func_array(array(&$this, $method), (array) $value);
if (PEAR::isError($e)) {
return $e;
}
}
return true;
}
/**
* Set path to file for download
*
* The Last-Modified header will be set to files filemtime(), actually.
* Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_FILE) if file doesn't exist.
* Sends HTTP 404 or 403 status if $send_error is set to true.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param string $file path to file for download
* @param bool $send_error whether to send HTTP/404 or 403 if
* the file wasn't found or is not readable
*/
function setFile($file, $send_error = true)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
$file = realpath($file);
if (!is_file($file)) {
if ($send_error) {
$this->HTTP->sendStatusCode(404);
}
return PEAR::raiseError(
"File '$file' not found.",
HTTP_DOWNLOAD_E_INVALID_FILE
);
}
if (!is_readable($file)) {
if ($send_error) {
$this->HTTP->sendStatusCode(403);
}
return PEAR::raiseError(
"Cannot read file '$file'.",
HTTP_DOWNLOAD_E_INVALID_FILE
);
}
$this->setLastModified(filemtime($file));
$this->file = $file;
$this->size = filesize($file);
return true;
}
/**
* Set data for download
*
* Set $data to null if you want to unset this.
*
* @access public
* @return void
* @param $data raw data to send
*/
function setData($data = null)
{
$this->data = $data;
$this->size = strlen($data);
}
/**
* Set resource for download
*
* The resource handle supplied will be closed after sending the download.
* Returns a PEAR_Error (HTTP_DOWNLOAD_E_INVALID_RESOURCE) if $handle
* is no valid resource. Set $handle to null if you want to unset this.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param int $handle resource handle
*/
function setResource($handle = null)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if (!isset($handle)) {
$this->handle = null;
$this->size = 0;
return true;
}
if (is_resource($handle)) {
$this->handle = $handle;
$filestats = fstat($handle);
$this->size = isset($filestats['size']) ? $filestats['size'] : -1;
return true;
}
return PEAR::raiseError(
"Handle '$handle' is no valid resource.", HTTP_DOWNLOAD_E_INVALID_RESOURCE
);
}
/**
* Whether to gzip the download
*
* Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
* if ext/zlib is not available/loadable.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param bool $gzip whether to gzip the download
*/
function setGzip($gzip = false)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if ($gzip && !PEAR::loadExtension('zlib')){
return PEAR::raiseError(
'GZIP compression (ext/zlib) not available.',
HTTP_DOWNLOAD_E_NO_EXT_ZLIB
);
}
$this->gzip = (bool) $gzip;
return true;
}
/**
* Whether to allow caching
*
* If set to true (default) we'll send some headers that are commonly
* used for caching purposes like ETag, Cache-Control and Last-Modified.
*
* If caching is disabled, we'll send the download no matter if it
* would actually be cached at the client side.
*
* @access public
* @return void
* @param bool $cache whether to allow caching
*/
function setCache($cache = true)
{
$this->cache = (bool) $cache;
}
/**
* Whether to allow proxies to cache
*
* If set to 'private' proxies shouldn't cache the response.
* This setting defaults to 'public' and affects only cached responses.
*
* @access public
* @return bool
* @param string $cache private or public
* @param int $maxage maximum age of the client cache entry
*/
function setCacheControl($cache = 'public', $maxage = 0)
{
switch ($cache = strToLower($cache))
{
case 'private':
case 'public':
$this->headers['Cache-Control'] = $cache .', must-revalidate, max-age='. abs($maxage);
return true;
break;
}
return false;
}
/**
* Set ETag
*
* Sets a user-defined ETag for cache-validation. The ETag is usually
* generated by HTTP_Download through its payload information.
*
* @access public
* @return void
* @param string $etag Entity tag used for strong cache validation.
*/
function setETag($etag = null)
{
$this->etag = (string) $etag;
}
/**
* Set Size of Buffer
*
* The amount of bytes specified as buffer size is the maximum amount
* of data read at once from resources or files. The default size is 2M
* (2097152 bytes). Be aware that if you enable gzip compression and
* you set a very low buffer size that the actual file size may grow
* due to added gzip headers for each sent chunk of the specified size.
*
* Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_PARAM) if $size is not
* greater than 0 bytes.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param int $bytes Amount of bytes to use as buffer.
*/
function setBufferSize($bytes = 2097152)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if (0 >= $bytes) {
return PEAR::raiseError(
'Buffer size must be greater than 0 bytes ('. $bytes .' given)',
HTTP_DOWNLOAD_E_INVALID_PARAM
);
}
$this->bufferSize = abs($bytes);
return true;
}
/**
* Set Throttle Delay
*
* Set the amount of seconds to sleep after each chunck that has been
* sent. One can implement some sort of throttle through adjusting the
* buffer size and the throttle delay. With the following settings
* HTTP_Download will sleep a second after each 25 K of data sent.
*
* <code>
* Array(
* 'throttledelay' => 1,
* 'buffersize' => 1024 * 25,
* )
* </code>
*
* Just be aware that if gzipp'ing is enabled, decreasing the chunk size
* too much leads to proportionally increased network traffic due to added
* gzip header and bottom bytes around each chunk.
*
* @access public
* @return void
* @param float $secondsAmount of seconds to sleep after each
* chunk that has been sent.
*/
function setThrottleDelay($seconds = 0)
{
$this->throttleDelay = abs($seconds) * 1000;
}
/**
* Set "Last-Modified"
*
* This is usually determined by filemtime() in HTTP_Download::setFile()
* If you set raw data for download with HTTP_Download::setData() and you
* want do send an appropiate "Last-Modified" header, you should call this
* method.
*
* @access public
* @return void
* @param int unix timestamp
*/
function setLastModified($last_modified)
{
$this->lastModified = $this->headers['Last-Modified'] = (int) $last_modified;
}
/**
* Set Content-Disposition header
*
* @see HTTP_Download::HTTP_Download
*
* @access public
* @return void
* @param string $disposition whether to send the download
* inline or as attachment
* @param string $file_name the filename to display in
* the browser's download window
*
* <b>Example:</b>
* <code>
* $HTTP_Download->setContentDisposition(
* HTTP_DOWNLOAD_ATTACHMENT,
* 'download.tgz'
* );
* </code>
*/
function setContentDisposition(
$disposition = HTTP_DOWNLOAD_ATTACHMENT, $file_name = null){
$cd = $disposition;
if (isset($file_name)) {
$cd .= '; filename="' . $file_name . '"';
} elseif ($this->file) {
$cd .= '; filename="' . basename($this->file) . '"';
}
$this->headers['Content-Disposition'] = $cd;
}
/**
* Set content type of the download
*
* Default content type of the download will be
* 'application/x-octetstream'.
* Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE) if
* $content_type doesn't seem to be valid.
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param string $content_type content type of file for download
*/
function setContentType($content_type = 'application/x-octetstream') {
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/', $content_type)){
return PEAR::raiseError(
"Invalid content type '$content_type' supplied.",
HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
);
}
$this->headers['Content-Type'] = $content_type;
return true;
}
/**
* Guess content type of file
*
* First we try to use PEAR::MIME_Type, if installed, to detect the content
* type, else we check if ext/mime_magic is loaded and properly configured.
*
* Returns PEAR_Error if:
* o if PEAR::MIME_Type failed to detect a proper content type
* (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
* o ext/magic.mime is not installed, or not properly configured
* (HTTP_DOWNLOAD_E_NO_EXT_MMAGIC)
* mime_content_type() couldn't guess content type or returned
* a content type considered to be bogus by setContentType()
* (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
*/
function guessContentType()
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if (class_exists('MIME_Type') ||
@include_once 'MIME/Type.php') {
if (PEAR::isError($mime_type =
MIME_Type::autoDetect($this->file))) {
return PEAR::raiseError($mime_type->getMessage(),
HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE);
}
return $this->setContentType($mime_type);
}
if (!function_exists('mime_content_type')) {
return PEAR::raiseError(
'This feature requires ext/mime_magic!',
HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
);
}
if (!is_file(ini_get('mime_magic.magicfile'))) {
return PEAR::raiseError(
'ext/mime_magic is loaded but not properly configured!',
HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
);
}
if (!$content_type = @mime_content_type($this->file)) {
return PEAR::raiseError(
'Couldn\'t guess content type with mime_content_type().',
HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
);
}
return $this->setContentType($content_type);
}
/**
* Send
*
* Returns PEAR_Error if:
* o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
* o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
*
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param bool $autoSetContentDisposition Whether to set the
* Content-Disposition header if it isn't already.
*/
function send($autoSetContentDisposition = true)
{
$error = $this->_getError();
if ($error !== null) {
return $error;
}
if (headers_sent()) {
return PEAR::raiseError(
'Headers already sent.',
HTTP_DOWNLOAD_E_HEADERS_SENT
);
}
if (!ini_get('safe_mode')) {
@set_time_limit(0);
}
if ($autoSetContentDisposition &&
!isset($this->headers['Content-Disposition'])) {
$this->setContentDisposition();
}
if ($this->cache) {
$this->headers['ETag'] = $this->generateETag();
if ($this->isCached()) {
$this->HTTP->sendStatusCode(304);
$this->sendHeaders();
return true;
}
} else {
unset($this->headers['Last-Modified']);
}
if (ob_get_level()) {
while (@ob_end_clean());
}
if ($this->gzip) {
@ob_start('ob_gzhandler');
} else {
ob_start();
}
$this->sentBytes = 0;
// Known content length?
$end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
if ($end != '*' && $this->isRangeRequest()) {
$chunks = $this->getChunks();
if (empty($chunks)) {
$this->HTTP->sendStatusCode(200);
$chunks = array(array(0, $end));
} elseif (PEAR::isError($chunks)) {
ob_end_clean();
$this->HTTP->sendStatusCode(416);
return $chunks;
} else {
$this->HTTP->sendStatusCode(206);
}
} else {
$this->HTTP->sendStatusCode(200);
$chunks = array(array(0, $end));
if (!$this->gzip && count(ob_list_handlers()) < 2
&& $end != '*') {
$this->headers['Content-Length'] = $this->size;
}
}
$this->sendChunks($chunks);
ob_end_flush();
flush();
return true;
}
/**
* Static send
*
* @see HTTP_Download::HTTP_Download()
* @see HTTP_Download::send()
*
* @static
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param array $params associative array of parameters
* @param bool $guess whether HTTP_Download::guessContentType()
* should be called
*/
function staticSend($params, $guess = false)
{
$d = &new HTTP_Download();
$e = $d->setParams($params);
if (PEAR::isError($e)) {
return $e;
}
if ($guess) {
$e = $d->guessContentType();
if (PEAR::isError($e)) {
return $e;
}
}
return $d->send();
}
/**
* Send a bunch of files or directories as an archive
*
* Example:
* <code>
* require_once 'HTTP/Download.php';
* HTTP_Download::sendArchive(
* 'myArchive.tgz',
* '/var/ftp/pub/mike',
* HTTP_DOWNLOAD_TGZ,
* '',
* '/var/ftp/pub'
* );
* </code>
*
* @see Archive_Tar::createModify()
* @deprecated use HTTP_Download_Archive::send()
* @static
* @access public
* @return mixed Returns true on success or PEAR_Error on failure.
* @param string $name name the sent archive should have
* @param mixed $files files/directories
* @param string $type archive type
* @param string $add_path path that should be prepended to the files
* @param string $strip_path path that should be stripped from the files
*/
function sendArchive($name, $files, $type=HTTP_DOWNLOAD_TGZ, $add_path='', $strip_path='')
{
require_once 'HTTP/Download/Archive.php';
return HTTP_Download_Archive::send($name, $files, $type,
$add_path, $strip_path);
}
// }}}
// {{{ protected methods
/**
* Generate ETag
*
* @access protected
* @return string
*/
function generateETag()
{
if (!$this->etag) {
if ($this->data) {
$md5 = md5($this->data);
} else {
$mtime = time();
$ino = 0;
$size = mt_rand();
extract(is_resource($this->handle) ?
fstat($this->handle) : stat($this->file));
$md5 = md5($mtime .'='. $ino .'='. $size);
}
$this->etag = '"' . $md5 . '-' . crc32($md5) . '"';
}
return $this->etag;
}
/**
* Send multiple chunks
*
* @access protected
* @return mixed Returns true on success or PEAR_Error on failure.
* @param array $chunks
*/
function sendChunks($chunks)
{
if (count($chunks) == 1) {
return $this->sendChunk(current($chunks));
}
$bound = uniqid('HTTP_DOWNLOAD-', true);
$cType = $this->headers['Content-Type'];
$this->headers['Content-Type'] =
'multipart/byteranges; boundary=' . $bound;
$this->sendHeaders();
foreach ($chunks as $chunk){
$this->sendChunk($chunk, $cType, $bound);
}
#echo "\r\n--$bound--\r\n";
return true;
}
/**
* Send chunk of data
*
* @access protected
* @return mixed Returns true on success or PEAR_Error on failure.
* @param array $chunk start and end offset of the chunk to send
* @param string $cType actual content type
* @param string $bound boundary for multipart/byteranges
*/
function sendChunk($chunk, $cType = null, $bound = null)
{
list($offset, $lastbyte) = $chunk;
$length = ($lastbyte - $offset) + 1;
$range = $offset . '-' . $lastbyte . '/'
. (($this->size >= 0) ? $this->size : '*');
if (isset($cType, $bound)) {
echo "\r\n--$bound\r\n",
"Content-Type: $cType\r\n",
"Content-Range: bytes $range\r\n\r\n";
} else {
if ($lastbyte != '*' && $this->isRangeRequest()) {
$this->headers['Content-Length'] = $length;
$this->headers['Content-Range'] = 'bytes '. $range;
}
$this->sendHeaders();
}
if ($this->data) {
while (($length -= $this->bufferSize) > 0) {
$this->flush(substr($this->data, $offset, $this->bufferSize));
$this->throttleDelay and $this->sleep();
$offset += $this->bufferSize;
}
if ($length) {
$this->flush(substr($this->data, $offset, $this->bufferSize + $length));
}
} else {
if (!is_resource($this->handle)) {
$this->handle = fopen($this->file, 'rb');
}
fseek($this->handle, $offset);
if ($lastbyte == '*') {
while (!feof($this->handle)) {
$this->flush(fread($this->handle, $this->bufferSize));
$this->throttleDelay and $this->sleep();
}
} else {
while (($length -= $this->bufferSize) > 0) {
$this->flush(fread($this->handle, $this->bufferSize));
$this->throttleDelay and $this->sleep();
}
if ($length) {
$this->flush(fread($this->handle, $this->bufferSize + $length));
}
}
}
return true;
}
/**
* Get chunks to send
*
* @access protected
* @return array Chunk list or PEAR_Error on invalid range request
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.
* html#sec14.35
*/
function getChunks()
{
$end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
// Trying to handle ranges on content with unknown length is too
// big of a mess (impossible to determine if a range is valid)
if ($end == '*') {
return array();
}
$ranges = $this->getRanges();
if (empty($ranges)) {
return array();
}
$parts = array();
$satisfiable = false;
foreach (explode(',', $ranges) as $chunk){
list($o, $e) = explode('-', trim($chunk));
// If the last-byte-pos value is present, it MUST be greater than
// or equal to the first-byte-pos in that byte-range-spec, or the
// byte- range-spec is syntactically invalid. The recipient of a
// byte-range- set that includes one or more syntactically invalid
// byte-range-spec values MUST ignore the header field that
// includes that byte-range- set.
if ($e !== '' && $o !== '' && $e < $o) {
return array();
}
// If the last-byte-pos value is absent, or if the value is
// greater than or equal to the current length of the entity-body,
// last-byte-pos is taken to be equal to one less than the current
// length of the entity- body in bytes.
if ($e === '' || $e > $end) {
$e = $end;
}
// A suffix-byte-range-spec is used to specify the suffix of the
// entity-body, of a length given by the suffix-length value. (That
// is, this form specifies the last N bytes of an entity-body.) If
// the entity is shorter than the specified suffix-length, the
// entire entity-body is used.
if ($o === '') {
// If a syntactically valid byte-range-set includes at least
// one suffix-byte-range-spec with a non-zero suffix-length,
// then the byte-range-set is satisfiable.
$satisfiable |= ($e != 0);
$o = max($this->size - $e, 0);
$e = $end;
} elseif ($o <= $end) {
// If a syntactically valid byte-range-set includes at least
// one byte- range-spec whose first-byte-pos is less than the
// current length of the entity-body, then the byte-range-set
// is satisfiable.
$satisfiable = true;
} else {
continue;
}
$parts[] = array($o, $e);
}
// If the byte-range-set is unsatisfiable, the server SHOULD return a
// response with a status of 416 (Requested range not satisfiable).
if (!$satisfiable) {
$error = PEAR::raiseError(
'Error processing range request',
HTTP_DOWNLOAD_E_INVALID_REQUEST);
return $error;
}
//$this->sortChunks($parts);
return $this->mergeChunks($parts);
}
/**
* Sorts the ranges to be in ascending order
*
* @param array &$chunks ranges to sort
*
* @return void
* @access protected
* @static
* @author Philippe Jausions <jausions@php.net>
*/
function sortChunks(&$chunks)
{
$sortFunc = create_function('$a,$b',
'if ($a[0] == $b[0]) {
if ($a[1] == $b[1]) {
return 0;
}
return (($a[1] != "*" && $a[1] < $b[1])
|| $b[1] == "*") ? -1 : 1;
}
return ($a[0] < $b[0]) ? -1 : 1;');
usort($chunks, $sortFunc);
}
/**
* Merges consecutive chunks to avoid overlaps
*
* @param array $chunks Ranges to merge
*
* @return array merged ranges
* @access protected
* @static
* @author Philippe Jausions <jausions@php.net>
*/
function mergeChunks($chunks)
{
do {
$count = count($chunks);
$merged = array(current($chunks));
$j = 0;
for ($i = 1; $i < count($chunks); ++$i) {
list($o, $e) = $chunks[$i];
if ($merged[$j][1] == '*') {
if ($merged[$j][0] <= $o) {
continue;
} elseif ($e == '*' || $merged[$j][0] <= $e) {
$merged[$j][0] = min($merged[$j][0], $o);
} else {
$merged[++$j] = $chunks[$i];
}
} elseif ($merged[$j][0]<=$o && $o<=$merged[$j][1]){
$merged[$j][1] = ($e == '*')? '*' :
max($e, $merged[$j][1]);
}elseif($merged[$j][0] <= $e && $e<=$merged[$j][1]){
$merged[$j][0] = min($o, $merged[$j][0]);
}else{
$merged[++$j] = $chunks[$i];
}
}
if ($count == count($merged)) {
break;
}
$chunks = $merged;
} while (true);
return $merged;
}
/**
* Check if range is requested
*
* @access protected
* @return bool
*/
function isRangeRequest()
{
if (!isset($_SERVER['HTTP_RANGE'])
|| !count($this->getRanges())) {
return false;
}
return $this->isValidRange();
}
/**
* Get range request
*
* @access protected
* @return array
*/
function getRanges()
{
return preg_match(
'/^bytes=((\d+-|\d+-\d+|-\d+)(, ?(\d+-|\d+-\d+|-\d+))*)$/',
@$_SERVER['HTTP_RANGE'], $matches)?$matches[1]:array();
}
/**
* Check if entity is cached
*
* @access protected
* @return bool
*/
function isCached()
{
return (
(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
$this->lastModified == strtotime(current($a = explode(
';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])))) ||
(isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
$this->compareAsterisk('HTTP_IF_NONE_MATCH', $this->etag))
);
}
/**
* Check if entity hasn't changed
*
* @access protected
* @return bool
*/
function isValidRange()
{
if (isset($_SERVER['HTTP_IF_MATCH']) &&
!$this->compareAsterisk('HTTP_IF_MATCH', $this->etag)) {
return false;
}
if (isset($_SERVER['HTTP_IF_RANGE']) &&
$_SERVER['HTTP_IF_RANGE'] !== $this->etag &&
strtotime($_SERVER['HTTP_IF_RANGE']) !== $this->lastModified) {
return false;
}
if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
$lm = current($a = explode(';', $_SERVER['HTTP_IF_UNMODIFIED_SINCE']));
if (strtotime($lm) !== $this->lastModified) {
return false;
}
}
if (isset($_SERVER['HTTP_UNLESS_MODIFIED_SINCE'])) {
$lm = current($a = explode(';', $_SERVER['HTTP_UNLESS_MODIFIED_SINCE']));
if (strtotime($lm) !== $this->lastModified) {
return false;
}
}
return true;
}
/**
* Compare against an asterisk or check for equality
*
* @access protected
* @return bool
* @param string key for the $_SERVER array
* @param string string to compare
*/
function compareAsterisk($svar, $compare)
{
foreach (array_map('trim', explode(',', $_SERVER[$svar])) as $request) {
if ($request === '*' || $request === $compare) {
return true;
}
}
return false;
}
/**
* Send HTTP headers
*
* @access protected
* @return void
*/
function sendHeaders()
{
foreach ($this->headers as $header => $value) {
$this->HTTP->setHeader($header, $value);
}
$this->HTTP->sendHeaders();
/* NSAPI won't output anything if we did this */
if (strncasecmp(PHP_SAPI, 'nsapi', 5)) {
if (ob_get_level()) {
ob_flush();
}
flush();
}
}
/**
* Flush
*
* @access protected
* @return void
* @param string $data
*/
function flush($data = '')
{
if ($dlen = strlen($data)) {
$this->sentBytes += $dlen;
echo $data;
}
ob_flush();
flush();
}
/**
* Sleep
*
* @access protected
* @return void
*/
function sleep()
{
if (OS_WINDOWS) {
com_message_pump($this->throttleDelay);
} else {
usleep($this->throttleDelay * 1000);
}
}
/**
* Returns and clears startup error
*
* @return NULL|PEAR_Errorstartup error if one exists
* @access protected
*/
function _getError()
{
$error = null;
if (PEAR::isError($this->_error)) {
$error = $this->_error;
$this->_error = null;
}
return $error;
}
// }}}
}
?>
출처: http://pear.php.net/package/HTTP_Download/docs/latest/__filesource/fsource_HTTP_Download__HTTP_Download-1.1.4HTTPDownload.php.html
0 댓글