Как отдать пользователю файл скриптом
10.07.2010
Теги: HTTP • PHP • Web-разработка • Файл
Зачастую бывает необходимость в том, чтобы сайт умел отдавать файлы не просто на скачивание, а поддерживать возможность скачивания в несколько потоков и докачки файла в случае обрыва соединения.
Для начала попробуем просто отдать файл браузеру:
<?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 • Файл