Содержание:
Для того, чтобы контролировать скачивание файлов с HTTP-сервера средствами CGI можно применять несколько способов:
При явном перенаправлении на странице помещается ссылка вида:
<a href="download.cgi?file=foofile">foofile</a>
При этом подразумевается, что CGI-метод «download.cgi» разберёт строку запроса формата GET, выделит из парамтера «file» имя скачиваемого файла и осуществит перенаправление на реальное размещение файла «foofile» с помощью поля «Location» в HTTP-ответе:
header ( 'Location: real.foofile.path' ); die ();
При этом появляется возможность только подсчитать примерное количество попыток загрузки файла. Дело в том, что подобным образом невозможно определить финал загрузки (скачал ли клиент файл до конца или отказался) и невозможно пресечь прямую загрузку файла в обход публикуемой ссылки. Поскольку идёт явное перенаправление, навигатор клиента переходит на новую ссылку «real.foofile.path» и скачивает оттуда файл напрямую. Ничто не мешает пользователю в следующий раз зайти по прямому адресу, который выдал навигатор.
Второй метод более сложный и более гибкий. Суть его заключается в том, что реальное размещение файла маскируется в теневой папке, а в публичной папке ничего не размещается. При этом CGI-метод «download.cgi» объявляется обработчиком ошибки «404 HTTP_NOT_FOUND» (файл не найден) в публичной папке, и на него возлагается ответственность за траспорт содержимого файла клиенту. В этом случае «download.cgi» должен распознать имя реального файла по адресному запросу клиента и самостоятельно передать содержимое «foofile» клиенту. CGI-метод не просто получает возможность определить конец загрузки, он становится оператором соединения.
Оператор соединения может фильтровать обращения к файлу:
Для этого пригодятся переменные HTTP-сервера:
Чтобы разобрать адресный запрос достаточно нескольких функций parse_url и pathinfo:
$URI = parse_url ( $_SERVER['REQUEST_URI'] ); /* $URI['scheme'] - протокол (например, "http") $URI['host'] - имя узла $URI['port'] - номер порта соединения $URI['user'] - имя пользователя при открытом авторизованном подключении $URI['pass'] - пароль пользователя при открытом авторизованном подключении $URI['path'] - полный путь к запрашиваемому файлу $URI['query'] - GET-запрос (после знака "?" в адресе) $URI['fragment'] - имя фрагмента в документе (после знака "#" в адресе) */ $Path = pathinfo ( $URI['path'] ); /* $Path['dirname'] - путь к файлу (без имени) $Path['basename'] - имя файла (с расширением $Path['extension'] - расширение (без точки) */
Так как оператор соединения по сути получает управление при возникновении ошибки «404 HTTP_NOT_FOUND», то для корректного установления соединения необходимо сформировать правильный ответ клиенту. В противном случае навигатор клиента получит в HTTP-заголовке ответа «Status: 404 Not Found», и он будет иметь полное право считать, что получил не содержимое запрашиваемого файла, а только текст, поясняющий ошибку. При этом пользователь увидит в окне навигатора часть запрашиваемого файла в виде текста, что вряд ли его обрадует.
Поэтому надо обязательно сообщить, что на самом деле никакой ошибки нет, и файл на месте. Для этого надо сформировать подходящий HTTP-заголовок ответа.
// Краткий перечень mime-типов $foofileCType = 'application/octet-stream'; $CTypes = array ( 'pdf' => 'application/pdf', 'exe' => 'application/octet-stream', 'zip' => 'application/zip', 'doc' => 'application/msword', 'xls' => 'application/vnd.ms-excel', 'ppt' => 'application/vnd.ms-powerpoint', 'gif' => 'image/gif', 'png' => 'image/png', 'jpe' => 'jpeg', 'jpg' => 'image/jpg', ); if ( isset ( $CTypes[$Path['extension']] ) $foofileCType == $CTypes[$Path['extension']]; $foofile = $Path['basename']; // запрашиваемое имя $realFooFilePath = ... // реальный путь к файлу header ( 'HTTP/1.1 200 OK', true, 200 ); header ( 'Status: 200 OK' ); header ( 'Pragma: ' ); header ( 'Cache-control: must-revalidate, post-check=0, pre-check=0' ); header ( 'Content-length: ' . fileSize ( $realFooFilePath ) ); header ( 'Last-Modified: ' . gmdate ( 'r', fileatime ( $realFooFilePath ) ) . ' GMT'); header ( 'Content-disposition: attachment; filename="' . $foofile . '"' ); header ( 'Content-type: ' . $foofileCType ); header ( 'Content-transfer-encoding: binary');
Самый примитивный, но не менее эффективный способ контроля скорости передачи заключается в порционной выдаче содержимого файла, чередующейся с паузой. «Скважность» порций определяет суммарную пропускную способность соединения. Например, надо обеспечить скорость соединения N Кбайт/с. Оператор соединения читает из «foofile» N Кбайт данных, пишет их в поток вывода и ждёт 1 секунду. При этом итоговая средняя скорость будет чуть меньше N Кбайт/с.
@set_time_limit ( 600 ); // максимальное процессорное время на выполнение сценария (с) $SpeedLimitBlockSize = N << 10; // N - желаемая скорость передачи (Кбайт/с) $OK = false; if ( $fp = fopen ( $realFooFilePath, 'rb' ) ) // 'rb' - обязательно для бинарных файлов { while ( !feof ( $fp ) ) { $buffer = fread ( $fp, $SpeedLimitBlockSize ); print $buffer; flush (); sleep ( 1 ); } flush (); $OK = feof ( $fp ); fclose ( $fp ); }
Стоит отметить, что HTTP-сервис Apache версии 2.x.x полностью буферизирует вывод, поэтому предложенный метод контроля скорости работать не будет. Сервис будет ждать, пока «download.cgi» не выдаст полностью всё содержимое и только потом запишет его в выходной поток.
Считается хорошим тоном, если сервис позволяет докачивать файлы в случае оборванных соединений. Поэтому при реализации оператора соединения необходимо предусмотреть вариант восстановления загрузки с заданного места. С точки зрения протокола HTTP происходит следующее:
if ( isset ( $_SERVER['HTTP_RANGE'] ) ) { if ( preg_match ( '/bytes=([0-9]+)-/i', $_SERVER['HTTP_RANGE'], $range ) && isset ( $range[1] ) ) { $rangePosition = intval ( $range[1] ); $rangeResponse = $rangePosition . '-' . fileSize ( $realFooFilePath ) - 1; header ( 'HTTP/1.1 206 Partial content', true , 206); header ( 'Status: 206 Partial content' ); header ( 'Content-range: bytes ' . $rangeResponse . '/' . fileSize ( $realFooFilePath ) ); header ( 'Last-Modified: ' . gmdate ('r', fileatime ( $realFooFilePath ) ) . ' GMT'); header ( 'Content-disposition: attachment; filename="' . $foofile . '"' ); header ( 'Content-type: ' . $foofileCType ); header ( 'Content-transfer-encoding: binary'); } }
После этого достаточно встать на позицию «$rangePosition» и выдать в поток содержимое файла.
Аналогично рассмотренному выше подходу, названному «оператор соединения», реализуется управление структурой виртуальных документов и виртуальных папок. В отличие от традиционной организации струкутры сайта, когда в домашней директории создаются соответствующие папки и файлы (пути к которым транслируются навигатору клиента относительно домашней директории), виртуальные папки и файлы могут вовсе не существовать. Специализированный обработчик ошибки «404 HTTP_NOT_FOUND» (назовём его «диспетчер запросов») анализирует строку запроса и формирует соответствующий ответ. При этом навигатор клиента работает в штатном режиме и пользователь не замечает разницы между реальными папками и файлами HTTP-сервера и виртуальными.
Диспетчер запросов должен иметь информацию о виртуальной структуре, для этого вполне подойдёт ассоциативный массив PHP, в котором в качестве ключей будут выступать вирутальный пути к вирутальным документам. Например, пусть необходимо сформировать следующую структуру:
Во-первых, необходимо выделить путь из строки запроса:
$URI = parse_url ( $_SERVER['REQUEST_URI'] ); $URIpath = str_replace ( '\\', '/', $URI['path'] );
Затем, с помощью ассоциативного массива виртуальной структуры определить правильность запроса и сформировать ответ.
$ShadowPath = 'shadow/'; $Structure = array ( '/' => 'general.inc', // значение --- теневое размещение файла '/index.htm' => 'general.inc', '/products/' => 'products.inc', '/products/index.htm' => 'products.inc', '/products/product1/' => 'product.1.inc', '/products/product1/index.htm' => 'product.1.inc', '/products/product1/price.htm' => 'product.1.price.inc', '/products/product2/' => 'product.2.inc', '/products/product2/index.htm' => 'product.2.inc', '/products/product2/support.htm' => 'product2.support.2.inc', '/about/' => 'about.inc', '/about/index.htm' => 'about.inc', ); if ( isset ( $Structure[$URIpath] ) ) { $FileName = $ShadowPath . $Structure[$URIpath]; $FileTime = filemtime ( $FileName ); if ( isset ( $headers['If-Modified-Since'] ) && strtotime ( $headers['If-Modified-Since'] ) == $FileTime ) { // Файл не менялся header ( 'Last-Modified: ' . gmdate ( 'r', $FileTime) . ' GMT', true, 304 ); } else { // Файл изменился со времени последнего обращения header ( 'Last-Modified: ' . gmdate ( 'r', $FileTime) . ' GMT', true, 200 ); include ( $FileName ); } flush (); } else { // Вывод диагностической страницы с ошибкой или иная обработка. // ... }
К сожалению, так получается, что обработчик ошибки «404 HTTP_NOT_FOUND» не получает значения переменных $_GET и $_POST, поэтому теряется возможность использования интерактивных форм на сайте. Обходное решение, хоть и не совсем изящное, но существует. Предлагается во всех формах в качестве обработчика POST запросов явно указать URI диспетчера. При этом надо сделать копию данных POST в окружение $_SESSION и перенаправить навигатор пользователя на адрес, с которого были посланы данные POST, но уже с установленным окружением.
session_start (); $_SESSION['name'] = 'foo.site'; if ( !isset ( $_SESSION['initiated'] ) ) { session_regenerate_id (); $_SESSION ['initiated'] = true; } if( $_SERVER['REQUEST_METHOD'] == 'POST' ) { $_SESSION['_POST'] = $_POST; // сохраняем копию данных POST в окружение SESSION header ( 'Location: ' . $_SERVER['HTTP_REFERER'] ); // перенаправляем обратно, чтобы клиент не увидел изменений в адресной строке die (); } else { if ( is_array( $_SESSION['_POST'] ) ) { $_POST = $_SESSION['_POST']; // обратно восстанавливаем данные POST unset ( $_SESSION['_POST'] ); } }
Для восстановления данных GET-запроса подобный метод был бы рассточительным, поэтому данные GET можно просто-напросто прочитать вручную.
function MakeGetArray ( $query ) { function ExtractParam ( & $query, $Start, $End ) { global $_GET; $temp = substr ( $query, $Start, $End ); if ( 1 < ( $EqPos = strpos ( $temp, '=', 0 ) ) ) $_GET [substr ( $temp, 0, $EqPos ) ] = substr ( $temp, $EqPos + 1, strlen ( $temp ) - $EqPos ); } $Start = 0; $AmpPos = strpos ( $query, '&', $Start ); if ( 2 < $AmpPos ) { ExtractParam ( $query, $Start, $AmpPos ); $Start = $AmpPos + 1; } ExtractParam ( $query, $Start, strlen ( $query ) ); } // заполняем массив $_GET MakeGetArray ( $URI['query'] )