PHP 實作支援 http 續傳功能及如何進行測試

檔案續傳在下載大檔案的時候,非常的實用,在實作時,需要伺服器端及客戶端程到兩者的互相搭配才行。

HTTP 續傳的原理,簡單說明如下:

用戶透過在 request 中加上 Range: bytes=0-499 的 header 給伺服器指定要取得檔案第 0 到 499 個檔案 position 的資料,(共 500 bytes, 第一個 byte 是 postion = 0, 包含第 499 position 位置的資料),在伺服器的回應中,會有

網站內文的程式碼,如下,用戶可以透過使用 Range header 指定伺服器下載檔案指定的檔案片段資料 Content-Range: bytes 0-499/1000000 告知客戶端程式,此次回傳的資料是位於檔案 position 0 到 position 499 的資料,檔案的總長度為 1000000 bytes。客戶端就可以按照對應的位置,把資料寫回檔案中,完成檔案的續傳。

也可以在 request 中指定 Range: bytes=500- 的方式,不指定結束的 position,代表要抓取由 500 position 開始到檔案結束的所有資料。

之前使用 PHP 撰寫檔案下載的程式,但是想要增加支援大檔案的續傳,發現下列這篇文章,內附的範例程式碼如下:

Output a file with HTTP range header in PHP

<?php
  $filename=$_GET['filename'];
  $location='media/'.$filename;
  
  $extension = substr(strrchr($filename,'.'),1);
  if ($extension == "mp3") {
    $mimeType = "audio/mpeg";
  } else if ($extension == "ogg") {
    $mimeType = "audio/ogg";
  }
  if (!file_exists($location))
  {
    header ("HTTP/1.1 404 Not Found");
    return;
  }
  
  $size  = filesize($location);
  $time  = date('r', filemtime($location));
  
  $fm = @fopen($location, 'rb');
  if (!$fm)
  {
    header ("HTTP/1.1 505 Internal server error");
    return;
  }
  
  $begin  = 0;
  $end  = $size - 1;
  
  if (isset($_SERVER['HTTP_RANGE']))
  {
    if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches))
    {
    $begin  = intval($matches[1]);
    if (!emptyempty($matches[2]))
    {
      $end  = intval($matches[2]);
    }
    }
  }
  if (isset($_SERVER['HTTP_RANGE']))
  {
    header('HTTP/1.1 206 Partial Content');
  }
  else
  {
    header('HTTP/1.1 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) + 1));
  if (isset($_SERVER['HTTP_RANGE']))
  {
    
    header("Content-Range: bytes $begin-$end/$size");
  }
  header("Content-Disposition: inline; filename=$filename");
  header("Content-Transfer-Encoding: binary");
  header("Last-Modified: $time");
  
  $cur  = $begin;
  fseek($fm, $begin, 0);
  
  while(!feof($fm) && $cur <= $end && (connection_status() == 0))
  {
    print fread($fm, min(1024 * 16, ($end - $cur) + 1));
    $cur += 1024 * 16;
  }

在 linux 伺服器上進行測試時,可以使用

curl -v -i 'http://yoursite.com/' -H 'Range: bytes=0-499' > ouput_test.mp3

的方式進行檔案續傳的測試,-i 是在下載回來的結果中,同時顯示 http response header 的部分,若是不要顯示 header,可以去除 -i 參數, 或是改用 -v,只把執行的 request header 及 response header 顯示在 console,不顯示在 output_test.mp3 檔案中

若是不用取得 http response body 的部分,只要 header ,可以使用 -I 參數,實際上是使用 http HEAD command 去詢問 web server 取得結果。
如果使用 -I 透過 http HEAD command 取得檔案資料,則 Content-Length header 不一定會出現,這個是看 web server 如何處理,如果是靜態檔案,可以會有這個 header,如果是碰到像 php 這樣動態檔案的話,就不會出現 Content-Length 了,若是想要在 response 中取得原來檔案的完整大小,可以改取 Content-Range header, 如下:

Content-Range: bytes 0-499/1000000

後方的 /1000000 代表的就是原檔案大小為 1000000 bytes (1M)

附上我修改後的程式碼,供參考:

<?php
    
    $file_name_not_secure = $_GET["file_name1"]; //remember to remove '/'
    
    $file_name = str_replace("\r", "", $file_name_not_secure); //replace '\r' for security issue
    $file_name = str_replace("\n", "", $file_name_not_secure); //replace '\n' for security issue
    $file_name = str_replace("/", "", $file_name_not_secure);  //replace '/' for security issue
    
    $file_name_urlencoded = rawurlencode($file_name);
    
    $file_path = "/download/" . $file_name;
    
    if (!file_exists($file_path)) {
        header("HTTP/1.1 404 Not Found");
        return;
    }
    
    $file_size = filesize($file_path);
    
    $start = 0;
    $end = $file_size-1; //because the range spec is zero based.
    
    $dlFile = @fopen($file_path, 'rb');
    if (!$dlFile) {
        header ("HTTP/1.1 500 Internal server error");
        return;
    }
    
    $partialContent = false;
    if (isset($_SERVER['HTTP_RANGE'])) {
        $partialContent = true;
    
        preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);  //note: bytes=-500 are not supported
        $start = intval($matches[1]);
        if (!empty($matches[2])) {
            $end = intval($matches[2]);
        }
    }
    
    if ($partialContent) {
        header('HTTP/1.1 206 Partial Content');
    } else {
        header('HTTP/1.1 200 OK');
    }
    
    //control proxy not cache this file
    header('Pragma public');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Cache-Control: private', false);
    
    $time  = date('r', filemtime($file_path));  //use RFC 2822 format, Example: Thu, 21 Dec 2000 16:01:07 +0200
    
    header("Last-Modified: $time");
    header('Content-Type: audio/x-wav');
    header('Accept-Ranges: bytes');  //tell http client that the site support Range header
    
    header("Content-Range: bytes {$start}-{$end}/{$file_size}");
    
    if ($partialContent) {
        header('Content-Length: ' . ($end-$start) + 1);
    } else {
        header('Content-Length: ' . $file_size);
    }
    
    //if the filename contains UTF-8 characters
    $user_agent = $_SERVER['HTTP_USER_AGENT'];
    $pos_IE60 = strpos($user_agent, "MSIE 6.0");
    $pos_IE70 = strpos($user_agent, "MSIE 7.0");
    $pos_safari = strpos($user_agent, "Safari");
    if ( $pos_IE60 > 0 || $pos_IE70 > 0 ) {
        header('Content-Disposition: attachment; filename=' . $file_name_urlencoded . '');
    } else if ( $pos_safari > 0 ) {
        header('Content-Disposition: attachment; filename=' . $file_name . '');
    } else {
        header('Content-Disposition: attachment; filename*=UTF-8\'\'' . $file_name_urlencoded . '');
    }
    header('Content-Transfer-Encoding: binary');
    
    //output file content
    $cur = $start;
    fseek($dlFile, $start, 0);  //skip to the right position
    while(!feof($dlFile) && $cur <= $end && (connection_status() == 0)) {
        print fread($dlFile, min(1024 * 16, ($end - $cur) + 1));
        $cur += 1024 * 16;
    }
    
?>

參考資料:
206 / PARTIAL CONTENT

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 變更 )

Twitter picture

You are commenting using your Twitter account. Log Out / 變更 )

Facebook照片

You are commenting using your Facebook account. Log Out / 變更 )

Google+ photo

You are commenting using your Google+ account. Log Out / 變更 )

連結到 %s

%d 位部落客按了讚: