Как отдать пользователю файл скриптом

10.07.2010

Теги: HTTPPHPWeb-разработкаФайл

Зачастую бывает необходимость в том, чтобы сайт умел отдавать файлы не просто на скачивание, а поддерживать возможность скачивания в несколько потоков и докачки файла в случае обрыва соединения.

Для начала попробуем просто отдать файл браузеру:

<?php
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($filename).'"');
readfile($filename);
?>

В этом примере мы сформировали два заголовка для браузера, первый из которых сообщает ему о типе содержимого (в данном случае — поток каких-то байтов), а второй заставляет его выдать нам окно с именем файла для его сохранения на локальном диске.

С поддержкой докачки

Заголовок Accept-Ranges: bytes, отправленный сервером, сообщает клиенту о том, что он может запрашивать данные с сервера фрагментами, указывая их смещение в байтах.

Зная эту возможность, браузер может передать серверу смещение в байтах, с которого необходимо начать передачу файла. Для этого браузер посылает заголовок Range:

Range: bytes=500-

где 500 — смещение в байтах от начала файла.

Сервер в свою очередь устанавливает переменную окружения HTTP_RANGE и должен отправить заголовок

HTTP/1.1 206 Partial Content

который дает клиенту понять, что отдается часть контента. Далее сервер должен отдать клиенту ту часть контента, которую тот запрашивал с соответствующими заголовками:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="имя_файла"
Last-Modified: время_модификации_файла
Accept-Ranges: bytes
Content-Length: длина_отдаваемой_части
Content-Range: bytes от-до/размер

Поясню последний заголовок на примере: имеем файл размером 10000 байт, отдаем все, кроме первых 500 байт. Тогда заголовок будет выглядеть так:

Content-Range: bytes 500-9999/10000
<?php
// если файла нет
if (!file_exists($filename)) {
  header ('HTTP/1.0 404 Not Found');
  exit;
}
 
// получим размер файла
$fsize = filesize($filename);
// дата модификации файла для кеширования
$ftime = date("D, d M Y H:i:s T", filemtime($filename));
// смещение от начала файла
$range = 0;
 
// пробуем открыть
$handle = @fopen($filename, "rb");

// если не удалось
if (!$handle){
  header ('HTTP/1.0 403 Forbidden');
  exit;
}
 
// если запрашивающий агент поддерживает докачку
if ( isset($_SERVER['HTTP_RANGE']) ) {
  $range = $_SERVER['HTTP_RANGE'];
  $range = str_replace('bytes=', '', $range);
  $range = str_replace('-', '', $range);
  // смещаемся по файлу на нужное смещение
  if ($range) fseek($handle, $range);
}
 
// если есть смещение
if ($range) {
  header('HTTP/1.1 206 Partial Content');
} else {
  header('HTTP/1.1 200 OK');
}
 
header('Content-Disposition: attachment; filename="'.basename($filename).'"');
header('Content-Type: application/octet-stream');
header('Last-Modified: '.$ftime);
header('Accept-Ranges: bytes');
header('Content-Length: '.($fsize - $range));
header('Content-Range: bytes '.$range.'-'.($fsize - 1).'/'.$fsize);
 
fpassthru($handle);
 
fclose($handle);
?>

В несколько потоков

Если клиент скачивает в несколько потоков, он будет отправлять нам заголовки вида (для файла длиной 10000 байт):

  • Range: bytes=0-499 — первые 500 байт
  • Range: bytes=500-999 — вторые 500 байт
  • Range: bytes=-500 или Range: bytes=9500- — последние 500 байт
  • Range: bytes=500- или Range: bytes=-9500 — все, кроме первых 500 байт
  • Range: bytes=0-0 — только первый байт

а мы должны отвечать так

  • Content-Range: bytes 0-499/10000 — отдаем первые 500 байт
  • Content-Range: bytes 500-999/10000 — отдаем вторые 500 байт
  • Content-Range: bytes 9500-9999/10000 — отдаем последние 500 байт
  • Content-Range: bytes 500-9999/10000 — отдаем все, кроме первых 500 байт
  • Content-Range: bytes 0-0/10000 — отдаем только первый байт
<?php
// если файла нет
if (!file_exists($filename)) {
  header ('HTTP/1.0 404 Not Found');
  exit;
}
 
// получим размер файла
$fsize = filesize($filename);
// дата модификации файла для кеширования
$ftime = date("D, d M Y H:i:s T", filemtime($filename));
 
// пробуем открыть
$handle = @fopen($filename, "rb");

// если не удалось
if (!$handle){
  header ('HTTP/1.0 403 Forbidden');
  exit;
}

// если запрашивающий агент поддерживает докачку
if ( isset($_SERVER['HTTP_RANGE']) ) {
  $range = $_SERVER['HTTP_RANGE']; 
  $range = str_replace( 'bytes=', '', $range );   
  $range = explode( '-', $range );
  if ( $range[0]=='0' && $range[1]=='0' ) { // если bytes=0-0
    $start = $stop = 0;
  } elseif ( !strlen( $range[0] ) ) { // если bytes=-500
    $start = $fsize - (int)$range[1]; 
    $stop = $fsize - 1; 
  } else { // если bytes=500-999 или bytes=500-
    $stop = (int)$range[1]; 
    if ( !$stop ) $stop = $fsize - 1; // bytes=500-
    $start = (int)$range[0]; 
    if ( $start ) fseek( $fd, $start ); 
  } 
  $length = $stop - $start + 1; 
  header('HTTP/1.1 206 Partial Content');
  header('Content-Disposition: attachment; filename="'.basename($filename).'"');
  header('Content-Type: application/octet-stream');
  header('Last-Modified: '.$ftime);
  header('Accept-Ranges: bytes');
  header('Content-Length: ' . $length); 
  header('Content-Range: bytes '.$start.'-'.$stop.'/'.$fsize);
  echo fread($handle, $length);
} else { // запрашивающий агент не поддерживает докачку
  header('HTTP/1.1 200 OK' );
  header('Content-Disposition: attachment; filename="'.basename($filename).'"');
  header('Content-Type: application/octet-stream');
  header('Last-Modified: '.$ftime);
  header('Accept-Ranges: bytes');
  header('Content-Length: '.$fsize); 
  fpassthru($handle);
}

fclose($handle);
?>

P.S. Этот код неполон, поскольку не обрабатывает мультидиапазонные запросы, когда клиент требует от сервера сразу несколько фрагментов:

Range: bytes=0-499,500-999,1000-1499

Поиск: $_SERVER • 206 • Accept-Ranges • Content • Content-Range • HTTP • HTTP_RANGE • PHP • Partial • Range • Web-разработка • bytes • Файл

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.