Content-Range 헤더는 파일 다운로드 이어받기 할 때 필요한 부분입니다. 동시에 $_SERVER['HTTP_RANGE'] 변수를 같이 사용하고 이로서 이어받기가 가능해집니다. 헤더부분은 Content-Range: bytes 다운받은 크기/전체크기 로 표현합니다. 다음을 참고하면 이해되리라 봅니다. (참고 문서)

 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 댓글