Форум: Форум PHPФорум ApacheФорум Регулярные ВыраженияФорум MySQLHTML+CSS+JavaScriptФорум FlashРазное
Новые темы: 0000000
Объектно-ориентированное программирование на PHP. Авторы: Кузнецов М.В., Симдянов И.В. PHP на примерах (2 издание). Авторы: Кузнецов М.В., Симдянов И.В. MySQL 5. В подлиннике. Авторы: Кузнецов М.В., Симдянов И.В. PHP 5/6. В подлиннике. Авторы: Кузнецов М.В., Симдянов И.В. Самоучитель PHP 5 / 6 (3 издание). Авторы: Кузнецов М.В., Симдянов И.В.
ВСЕ НАШИ КНИГИ
Консультационный центр SoftTime

Форум PHP

Выбрать другой форум

 

Здравствуйте, Посетитель!

вид форума:
Линейный форум Структурный форум

тема: Скачка файлов с возможностью докачки
 
 автор: Лена   (22.02.2009 в 20:33)   письмо автору
 
 

Если закачивать файл с возможностью докачки, можно ли как-то управлять закачкой отдельных фрагментов?
Например, если пользователь скачал 1 и 4 фрагмент и неожиданно отключился, может ли сервер зафиксировать то, что 1 и 4 фрагменты уже есть, значит, при повторном подключении 1 и 4 фрагменты уже не давать, а только оставшиеся 2 и 3? И где тогда это фиксируется?
Была мысль сохранить 1 и 4 фрагмент в кэше, но в RFC пишется, что кэш не поддерживает заголовки Range и Content-Range, отклики 206 (Partial) не кэшируются. Как тогда быть?
И есть ли способ сообщить пользователю о том, сколько ему осталось закачать? Например, что-то типа такого: "Вы уже скачали 25% книги, чтобы скачать остальное, вам понадобится 2 минуты".

  Ответить  
 
 автор: Trianon   (22.02.2009 в 20:37)   письмо автору
 
   для: Лена   (22.02.2009 в 20:33)
 

Вообще-то при частичной докачке клиент сам запрашивает (Range: bytes... ), какой фрагмент ему нужен.
Клиент же считает прогноз по времени.
Серверу остается лишь отдать (206 Partial content и Content-range:...) запрошенный фрагмент.

  Ответить  
 
 автор: Лена   (22.02.2009 в 20:52)   письмо автору
 
   для: Trianon   (22.02.2009 в 20:37)
 

Весь текст делится на равные фрагменты, например 0-100, 100-200 и т.д. В Content-Range: bytes и будут отмечены эти фрагменты(+ полный размер файла). А если какой-то фрагмент оборвется посредине? Как это сообщается серверу?

>Клиент же считает прогноз по времени.
Как посчитать, сколько времени осталось, чтобы закачать весь фрагмент?

  Ответить  
 
 автор: Trianon   (22.02.2009 в 21:06)   письмо автору
 
   для: Лена   (22.02.2009 в 20:52)
 

нет. не так.

1. сервер сообщает клиенту полем Accept-Ranges: bytes что он в принципе непротив обслуживать диапазонные запросы.
2. Клиент выдает запрос с полем Range: bytes; nnnnn-
где nnnnn - позиция , с которой начинается недокачаный участок.
Выбор участка целиком и полностью право и обязанность клиента. Как поделит - так и захочет. как захочет - так и попросит. Как попросит - так сервер должен будет выполнить.
Синтаксис на самом деле более сложный , но приведенная форма самая переносимая. Означает она, что фрагмент берется до конца файла, и случчего клиент прервет соединение, если нарвется на уже закачанный участок.



3. сервер отвечает
HTTP/n.n 206 Partial content
Content-Length: lllll
Content-Range: bytes nnnnn-mmmmm/sssss
Content-Type: application/octet-stream

фрагмент

где lllll - длина фрагмента
mmmmm - позиция конца фрагмента - 1
sssss - полный размер файла

mmmmm при верхнем запросе , понятное дело, равна sssss-1

  Ответить  
 
 автор: Лена   (22.02.2009 в 21:52)   письмо автору
 
   для: Trianon   (22.02.2009 в 21:06)
 

>где nnnnn - позиция , с которой начинается недокачаный участок
не пойму, откуда клиент узнает эту позицию? Как он может ее узнать, если процессом закачки управляет сервер?

>случчего клиент прервет соединение, если нарвется на уже закачанный участок.
а может быть так, что этот закачанный участок будет находиться до позиции nnnnn, которую определил клиент?

  Ответить  
 
 автор: Trianon   (22.02.2009 в 22:51)   письмо автору
 
   для: Лена   (22.02.2009 в 21:52)
 

>>где nnnnn - позиция , с которой начинается недокачаный участок
>не пойму, откуда клиент узнает эту позицию? Как он может ее узнать, если процессом закачки управляет сервер?

процессом закачки управляет клиент.

>>случчего клиент прервет соединение, если нарвется на уже закачанный участок.
>а может быть так, что этот закачанный участок будет находиться до позиции nnnnn, которую определил клиент?

Вполне.
Типичный случай закачки длинного файла каким нибудь многопоточным менеджером закачки, вроде flashget.
К примеру файл 1000000 байт длиной и в настройках менеджера указано тащить в 5 потоков.

Менеджер устанавливает пять соединений, указывая в их get-запросах такие поля:
Range: bytes; 0-
Range: bytes; 200000-
Range: bytes; 400000-
Range: bytes; 600000-
Range: bytes; 800000-

Серверу ничего не остается как ответить вот такими кусками.

200 OK (поскольку это весь файл)
Content-Length: 1000000

206 Partial content,
Content-Length: 800000
Content-range: bytes; 200000-999999/1000000

206 Partial content,
Content-Length: 600000
Content-range: bytes; 400000-999999/1000000

206 Partial content,
Content-Length: 400000
Content-range: bytes; 600000-999999/1000000

206 Partial content,
Content-Length: 200000
Content-range: bytes; 800000-999999/1000000

В графике загрузки прекрасно видно что происходит дальше.

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

  Ответить  
 
 автор: Лена   (23.02.2009 в 17:23)   письмо автору
3.4 Кб
 
   для: Trianon   (22.02.2009 в 22:51)
 

Вот что получилось (см. аттач), там с комментариями, должно быть понятно.
Старалась делать пошагово, как вы описали.
Но нажимаю на ссылку - выскакивает окно:
"IE не удается загрузить ... из ...
Не удается открыть этот узел Интернета. Узел недоступен или не найден...."

  Ответить  
 
 автор: Trianon   (23.02.2009 в 20:44)   письмо автору
 
   для: Лена   (23.02.2009 в 17:23)
 

Ошибка в определении $to в строке 52 и 58 . Не все форматы range учитываются.

Ошибка в строке 74 . Вы уменьшаете переменную для одной строки, а назад её не корректируете.
А потом пользуетесь на 85-й. С ошибкой.
Ошибка в fread в 90 строке - теряется считанный фрагмент.

  Ответить  
 
 автор: Лена   (23.02.2009 в 23:14)   письмо автору
 
   для: Trianon   (23.02.2009 в 20:44)
 

>Ошибка в определении $to в строке 52 и 58 . Не все форматы range учитываются.
Я написала, если диапазон задан и если значения диапазона не заданы.
Может, вот это еще нужно было учесть? Если конечный элемент не задан, считываем до конца файла: if(!$to) $to = $size;

>Ошибка в fread в 90 строке - теряется считанный фрагмент.
Получается, все считанное надо писать в переменную. А если фрагменты будут перекрывать друг друга? Например, один кусок будет 200-399, второй 200-599(это если оборвалась связь).

  Ответить  
 
 автор: Trianon   (23.02.2009 в 23:24)   письмо автору
 
   для: Лена   (23.02.2009 в 23:14)
 

есть три формата задания диапазона
aaaa-bbbb : все байты с aaaa и по bbbb
aaaa- : все байты с aaaa и до конца файла
-ssss : это не от начала и по ssss, как многие (в том числе и я когда-то) заблуждаются.
это означает последние ssss байт файла. Хвостовой фрагмент.

>Получается, все считанное надо писать в переменную.

Почему в переменную? echo надо делать.

>А если фрагменты будут перекрывать друг друга?

Например, один кусок будет 200-399, второй 200-599(это если оборвалась связь).

Если это два разных запроса - совершенно не должно трогать. Клиент сам разберется, откуда что и куда.

Если два кустка в одном ... в принципе в HTTP/1.1 есть мультидиапазонные запросы, но Вы их, по-моему, реализовывать не собираетесь. Вообще, они больше для транспортных уровней характерны, а не для прикладных.

  Ответить  
 
 автор: Лена   (24.02.2009 в 23:35)   письмо автору
 
   для: Trianon   (23.02.2009 в 23:24)
 

>-ssss : это не от начала и по ssss, как многие (в том числе и я когда-то) заблуждаются.

Не пойму. Поэтому и не могу необходимое условие поставить.
Если пользователь запросил диапазон такой:
-499
Согласно моему коду, $from здесь не определено, $to = 499. Почему это вы назвали хвостовым фрагментом, если нужно считать с какого-то байта($from ) до 499, а если в файле 800 байт, это уже будет не хвостовой фрагмент.
И еще одно. А если $from = 499, получается, что вообще ничего считано не будет?

  Ответить  
 
 автор: Trianon   (24.02.2009 в 23:47)   письмо автору
 
   для: Лена   (24.02.2009 в 23:35)
 

>>-ssss : это не от начала и по ssss, как многие (в том числе и я когда-то) заблуждаются.
>
>Не пойму. Поэтому и не могу необходимое условие поставить.
>Если пользователь запросил диапазон такой:
>-499
>Согласно моему коду, $from здесь не определено, $to = 499. Почему это вы назвали хвостовым фрагментом, если нужно считать с какого-то байта($from ) до 499, а если в файле 800 байт, это уже будет не хвостовой фрагмент.

потому что -499 означает вовсе не то, что from неопределен, а to равен 499.
-499 означает что сервер должен отдать 499 последних байт.
Лена, Вы RFC-2616 глядели (хотя бы в переводе) или только по подсказкам алгоритм строите?

suffix-byte-range-spec = "-" suffix-length
suffix-length = 1*DIGIT

Спецификация suffix-byte-range-spec используется для задания суффикса тела объекта с длиной, заданной значением suffix-length. (То есть, эта форма специфицирует последние N байтов тела объекта.) Если объект короче заданной длины суффикса, то в качестве суффикса используется все тело объекта.

  Ответить  
 
 автор: Лена   (25.02.2009 в 00:01)   письмо автору
 
   для: Trianon   (24.02.2009 в 23:47)
 

>Лена, Вы RFC-2616 глядели
Читала выборочно и быстро. А надо было подряд.
Ушла еще раз читать. Когда осилю, составлю код. Все равно я его добью.
Спасибо за помощь.

  Ответить  
 
 автор: Trianon   (25.02.2009 в 00:08)   письмо автору
 
   для: Лена   (25.02.2009 в 00:01)
 

Пожалуй, я оставлю здесь ссылки на главы перевода RFC-2616, связанные с темой, и номера параграфов оригинального документа.

9.2.7. 206 Partial Content (Частичное содержимое)
--- 10.2.7 206 Partial Content

(не описан в переводе)
--- 10.4.17 416 Requested Range Not Satisfiable

12.10.4 Комбинирование байтовых фрагментов
--- 13.5.4 Combining Byte Ranges

13.5. Поле Accept-Ranges
--- 14.5 Accept-Ranges

13.17. Отрывок содержимого
--- 14.16 Content-Range

13.27. Заголовок If-Range
--- 14.27 If-Range

13.36. Фрагмент (обе главы)
14.35 Range

16.2. Тип среды Интернет "multipart/byteranges"
--- 19.2 Internet Media Type multipart/byteranges

  Ответить  
 
 автор: Лена   (25.02.2009 в 00:22)   письмо автору
 
   для: Trianon   (25.02.2009 в 00:08)
 

Спасибо.
Самое интересное, что кроме вас никто по докачке файлов толкового кода не сделал.
Сложная задача.

  Ответить  
 
 автор: Лена   (26.02.2009 в 14:33)   письмо автору
8 Кб
 
   для: Лена   (25.02.2009 в 00:22)
 

Сделала. В аттаче.
Скрипт работает.
Если что не правильно записано, говорите.

  Ответить  
 
 автор: Trianon   (26.02.2009 в 14:57)   письмо автору
 
   для: Лена   (26.02.2009 в 14:33)
 

Мне трудно оценить. У меня выползет куча неопределенных переменных.
case $ext[$i]: где $ext[$i] - массив , я не понял...
С вызовом fpassthru у Вас какая-то ерунда.
Не понял , зачем нужно после вывода хвостового фрагмента делать вывод его длины.
И с IF_RANGE тоже странно как-то. То ли он у Вас md5-метка, то ли время модификации...

Отработка 304 Not modified какая-то куцая.
что за заголовки после него выводятся?
И как заблокирован вывод тела файла?

Аналогично с 412.

  Ответить  
 
 автор: Лена   (26.02.2009 в 16:14)   письмо автору
 
   для: Trianon   (26.02.2009 в 14:57)
 

>У меня выползет куча неопределенных переменных.
У меня все данные про файл хранятся в базе (имя, путь, расширение и т.д.) Потом в скрипте я эти данные использую. Вот почему у вас неопределенные переменные

>case $ext[$i]: где $ext[$i] - массив , я не понял...
Это не учла, забыла. Уже исправила, спасибо.

>С вызовом fpassthru у Вас какая-то ерунда.
Правда ерунда. Думала, что fpassthru по-другому работает. На примере проверила, поняла.

>И с IF_RANGE тоже странно как-то.
Формат заголовка такой:
If-Range = "If-Range" ":" (entity-tag | HTTP-date)
Заголовок может содержать или метку, или время последней модификации. Я учла оба способа. Ведь по первым двум символам сервер может отличить дату от метки.
Записала условием: if($_SERVER['HTTP_IF_RANGE'] == $etag || strtotime($_SERVER['HTTP_IF_RANGE']) >= $last_mod || !$_SERVER['HTTP_IF_RANGE']){
Что здесь неправильного?

>И как заблокирован вывод тела файла?
Это о чем? Не поняла.

Про 304 и 412 еще почитаю и подумаю, как сделать.

  Ответить  
 
 автор: Trianon   (26.02.2009 в 18:45)   письмо автору
 
   для: Лена   (26.02.2009 в 16:14)
 

>>У меня выползет куча неопределенных переменных.
>У меня все данные про файл хранятся в базе (имя, путь, расширение и т.д.) Потом в скрипте я эти данные использую. Вот почему у вас неопределенные переменные

Да нет, не поэтому.
К примеру if($_SERVER['HTTP_IF_RANGE'] == $etag сразу же выдаст нотайс, если поля IF_RANGE не окажется в заголовке запроса. И т.п.



>>И с IF_RANGE тоже странно как-то.
>Формат заголовка такой:
>If-Range = "If-Range" ":" (entity-tag | HTTP-date)
>Заголовок может содержать или метку, или время последней модификации. Я учла оба способа. Ведь по первым двум символам сервер может отличить дату от метки.
Может :) Но не святым же духом. Это задача программиста, то есть Ваша. :)

>Записала условием: if($_SERVER['HTTP_IF_RANGE'] == $etag || strtotime($_SERVER['HTTP_IF_RANGE']) >= $last_mod || !$_SERVER['HTTP_IF_RANGE']){
>Что здесь неправильного?
Условный блок должен начинаться с if(isset($_SERVER['HTTP_IF_RANGE']
Что до самой логики, то
а) хеш считается не от имени файла, а от его содержимого. И лучше бы он был посчитан один раз - это довольно ресурсоемкая операция.
б) дата модификации должна указываться и восприниматься по UTC (gmdate)
в) if(!$etag && -- etag у Вас никогда нулем не будет - смысла продолжения строки я не понял.
г) if(strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) <= $last_mod при отсутствии поля
IF_MODIFIED_SINCE приведет к тому, что будет выдан 304 Not modified
... ну и так далее...



>>И как заблокирован вывод тела файла?
>Это о чем? Не поняла.

Вы выдаете header 304 Not Modified
потом
header('Content-Type: '. $mime);
header('Content-Disposition: attachment; filename=' . basename($filename));
header("Last-Modified: " . gmdate("D M Y H:i:s T",$last_mod));
потом что?
Потом идет нотайс по неопределенному $from, $to
пара полей
header("HTTP/1.1 200 OK");
header('Content-Length:' . $size);
и цикл вывода файла.
При том что тело выдаваться не должно.
кстати, следующая строка
//узнаем, запросил клиент полный файл либо кусок
if ($from > 0 || $to < $size) {

>

  Ответить  
 
 автор: Лена   (28.02.2009 в 20:25)   письмо автору
9.7 Кб
 
   для: Trianon   (26.02.2009 в 18:45)
 

Новый вариант. В аттаче.
А вот заблокировать не получилось.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 11:33)   письмо автору
 
   для: Лена   (28.02.2009 в 20:25)
 


<?php
include_once("configs/dbopen.php");
//получаем нужную книгу по ссылке
if(isset($_GET['id_book'])){
  
//выбираем название книги, путь и расширение файла из базы
  
$sql "SELECT * FROM books WHERE id =" $_GET['id_book'];
  
$res mysql_query($sql);
  if(!
$res) exit ("Error in $sql:" mysql_error());
  while(
$row mysql_fetch_assoc($res)){
    
//print_r($row);
    //определяем расширение
    
$extension $row['type'];
    
$ext = array('zip''rar',
                        array(
'gz''tgz'),
              array(
'jpg','jpe','jpeg'), 'gif''png''txt''pdf', );
    
$stream = array('application/x-zip''application/x-rar-compressed',
            
'application/x-gzip''image/jpeg''image/gif''image/png',
            
'text/plain''application/pdf');
    for(
$i=0;$i<count($ext);$i++){
      switch(
$extension){
        case 
$ext[$i]:
          if(
is_array($ext[$i])){
              foreach(
$ext[$i] as $key => $ei)
                  
$mime $stream[$i][$key];
          }else
              
$mime $stream[$i];
           break;
        default:
          
$mime 'application/octet-stream';
          break;
      }
    }

    
$filename $row['path'] . "/"$row['title'] . "." $row['type'];

    
//проверка на существование и воспроизведение нужного файла
    
if (!file_exists($filename) && !is_readable($filename))
        exit(
'File does not exist or don`t readable');

    
//сервер сообщает клиенту, что непротив обслуживать байт-диапазонные запросы
    
header('Accept-Ranges: bytes');
    
$f fopen($filename'rb');
      
//проверка, открылся ли файл
    
if(!$f){
      
header ("HTTP/1.0 505 Internal server error");
      exit();
    }
    
flock($fLOCK_EX);

    
fseek($f0SEEK_END); //указатель - в конец файла, получаем позицию указателя
    
$size ftell($f);

    
//метка объекта, хеш всего файла
    
$etag_server md5_file($filename);

    
//время последней модификации файла
    
$last_mod filemtime($filename);

    
//клиент запросил докачку
    
if (isset($_SERVER['HTTP_RANGE'])) {
      
//проверяем, что лежит в $_SERVER['HTTP_RANGE']: bytes=20-50/длина файла
      
if (!preg_match('/^bytes=(?:\d+-\d+|-\d+|\d+-)(?:,(?:\d+-\d+|-\d+|\d+-))*//\d+$/',
                                          
$_SERVER['HTTP_RANGE']))
          return 
false;

      
// условные запросы
      
if(isset($_SERVER['HTTP_IF_RANGE']) && $_SERVER['HTTP_IF_RANGE'] == $etag_server
                  
|| strtotime($_SERVER['HTTP_IF_RANGE']) >= $last_mod)
      {
        
//определяем, чем являются первые символы - меткой или датой
        
if(!preg_match('/^\d{2}\s\d{2}\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT/',
                            
$_SERVER['HTTP_IF_RANGE']))
        {
          
//если это метка, проверяем, совпадает ли метка объекта с текущей
          
if(isset($_SERVER['HTTP_IF_MATCH']) && $_SERVER['HTTP_IF_MATCH'] != "*"){
            
$etag_client $_SERVER['HTTP_IF_MATCH'];
            
//метка не совпала
            
if($etag_client !== $etag_server){
              
//надо присвоить новую метку
              
header("ETag: \""$etag_server ."\"");
              
// зафиксировать последнее изменение (сейчас)
              
header ("Last-Modified: " gmdate("d M Y H:i:s \G\M\T"time()));
              
//страница не будет меняться 10 минут
              
header("Expires: " gmdate("d M Y H:i:s \G\M\T"time()+60*10));
            }
          }else{
            
header("HTTP/1.1 412 Precondition Failed");
            exit();
          }
        }else{
          
//если это дата, проверяем время модификации
          
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
                      && 
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_mod)
          {
            
header ("Date: " gmdate ("D, d M Y H:i:s \G\M\T"));
            
header("HTTP/1.1 304 Not Modified"); //файл не поменялся
          
}else{
            if(
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) > $last_mod){
              
//файл изменился, присваиваем новую метку
              
header("ETag: \""$etag_server ."\"");
              
// последнее изменение - сейчас
              
header ("Last-Modified: " gmdate("d M Y H:i:s \G\M\T"time()));
              
//страница не будет меняться 10 минут
              
header("Expires: " gmdate("d M Y H:i:s \G\M\T"time()+60*10));
            }
          }
        }
        
//определяем диапазон
        
$range=substr($_SERVER['HTTP_RANGE'], strpos($_SERVER['HTTP_RANGE'], '=')+1);
        
$range substr($rangestrpos($range,0,"/"));
        
$range explode("-"$range);
        
$from $range[0];
        
$to $range[1];
        
//если первая позиция не задана, берем ее с нуля
        
if($from 0$from 0;
        if(
$to$to = ++ $to;
        
//если не задана позиция конца
        
else $to $size;

        
//первые байты, диапазон типа 521000-
        
if($range[1]==''){
          
$from 0;
          
$to $range[0];
        }
        
//последние байты, диапазон типа -300000
        
if($range[0]==''){
          
$from $range[1];
          
$to $size;
        }
        
//узнаем, запросил клиент полный файл либо кусок
        
if ($from || $to $size) {
          
//первый и последний байт куска
          
$start $from;
          
$end $to 1;
          
header('HTTP/1.1 206 Partial Content');
          
//метка
          
header("ETag: \""$etag_client ."\"");
          
//длина диапазона, который считывается
           
$part $to $from;
          
header('Content-Length:' $part);
          
header('Content-Range: bytes ' $start "'-'" $end "'/'" $size);
        }else{
          
// если клиент не запросил докачку или byte-range-spec некорректна
          
header("HTTP/1.1 200 OK");
          
header('Content-Length:' $size);
        }
      }else
        
header("HTTP/1.1 416 Requested Range Not Satisfiable");
    }else{
      
//клиент запросил весь файл
      
$range = array(0,$size);
      
$from $range[0];
      
$to $range[1];
      
$start $from;
    }
    
//отправляем заголовки и для целого файла, и для частичной загрузки
    
header('Content-Type: '$mime);
    
header('Content-Disposition: attachment; filename=' basename($filename));

    
//ставим указатель на начальную позицию, которую задал клиент
    
fseek($f$startSEEK_SET);
    
//выдача файла
    
while(!feof($f)) {
      if(
$to == $size){
        
//выводим остаток до конца файла
        
fpassthru($f);
      }else{
        
//выводим начало и середину
        
$read fread($f256000);
        echo 
$read;
        
flush();
      }
    }
    
flock($fLOCK_UN);
    
fclose($f);
  }
//while($row =
  //счетчик скачиваний
  
$sql_update "UPDATE books SET count_download = count_download + 1 WHERE id ="
                          
$_GET['id_book'];
  
mysql_query($sql_update);
}
//if(isset($_GET
?>



а теперь можно будет и посмотреть...

  Ответить  
 
 автор: Trianon   (01.03.2009 в 11:44)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

1. блок определения mime работает как-то странно.
У меня он возвращает такой результат:

zip => application/octet-stream
rar => application/octet-stream
gz => application/octet-stream
tgz => application/octet-stream
jpg => application/octet-stream
jpe => application/octet-stream
jpeg => application/octet-stream
gif => application/octet-stream
png => application/octet-stream
txt => application/octet-stream
pdf => application/pdf

это вполне работоспособно, но ... ей-богу , достигается куда меньшими усилиями.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 11:47)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

2. Вычисление хеша файла.

    flock($f, LOCK_EX);

    fseek($f, 0, SEEK_END); //указатель - в конец файла, получаем позицию указателя
    $size = ftell($f);

    //метка объекта, хеш всего файла
    $etag_server = md5_file($filename);

Мы сперва блокируем файл, а потом пытаемся запросить вычисление хеша.
и как функция md5_file станет работать с заблокированным файлом?


Кроме того, как я уже сказал, операция md5_file() довольно ресурсооемкая. Лучше бы хранить посчитанное значение хеша прямо в таблице.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 11:56)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

3. проверка формата Range:
      if (!preg_match('/^bytes=(?:\d+-\d+|-\d+|\d+-)(?:,(?:\d+-\d+|-\d+|\d+-))*//\d+$/',
                                          $_SERVER['HTTP_RANGE']))
          return false;

заканчивается каким-то return, хотя по идее достаточно всего лишь считать поле Range отсутствующим, и выдать полный контент.
Кроме того, шаблон выглядит ... странно.
В нем явно вырисовываются следы возможности обработки мультидиапазонных запросов, в то время как сам скрипт явно к этому не готов.
При чем именно следы. Синтаксис явно нарушен...
Такое впечатление, что РБНФ прямо воткнута в конец шаблона.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 12:04)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

4.
      // условные запросы
      if(isset($_SERVER['HTTP_IF_RANGE']) && $_SERVER['HTTP_IF_RANGE'] == $etag_server
                  || strtotime($_SERVER['HTTP_IF_RANGE']) >= $last_mod)

в этом фрагменте strtotime() будет работать снеопределенным элементом HTTP_IF_RANGE массива $_SERVER

что-то тут не так.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 12:21)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

5.
        //определяем, чем являются первые символы - меткой или датой
        if(!preg_match('/^\d{2}\s\d{2}\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT/',
                            $_SERVER['HTTP_IF_RANGE']))


Слова стандарта "Сервер может отличить корректную дату HTTP от любой формы метки объекта, рассмотрев не более двух символов" - это не повод проверять всю дату на соответствие,
тем более что как клиентская, так и серверная часть должна быть достаточно толлерантна к форматам даты.
на самом деле, поскольку в первых двух буквах названия дня недели встречается хотя бы один из нешестнадцатеричных символов, достаточно написать что-то вроде
$ifr = trim($_SERVER['HTTP_IF_RANGE']);
$iftag = $ifdate = '';
if(ctype_digit($ifr)) $iftag = $ifr; else  $ifdate = strtoftime($ifr); 

  Ответить  
 
 автор: Trianon   (01.03.2009 в 12:23)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

6.


              header("ETag: \"". $etag_server ."\"");
              // зафиксировать последнее изменение (сейчас)
              header ("Last-Modified: " . gmdate("d M Y H:i:s \G\M\T", time()));
              //страница не будет меняться 10 минут
              header("Expires: " . gmdate("d M Y H:i:s \G\M\T", time()+60*10));


Все эти параметры должны выдаваться в любом заголовке отклика. А не только под условиями.
Кстати, почему в качестве last-modified выдается текущее время, если файл имеет явное время модификации?

Опять же
            header("HTTP/1.1 412 Precondition Failed");
            exit();

перед exit еще по идее должны следовать Content-type и прочие характеристики объекта.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 12:34)   письмо автору
 
   для: Trianon   (01.03.2009 в 11:33)
 

7
        //первые байты, диапазон типа 521000-
        if($range[1]==''){
          $from = 0;
          $to = $range[0];
        }

это как?

  Ответить  
 
 автор: Лена   (01.03.2009 в 17:00)   письмо автору
 
   для: Trianon   (01.03.2009 в 12:34)
 

1. Этот блок не так должен работать. Как должен, сама сделаю и покажу.
Получается, если скрипт большой, надо куски отдельно проверять. А здесь я не правильно проверила. Просто загружала файлы с разными расширениями, а надо было не так делать.

2.
>Лучше бы хранить посчитанное значение хеша прямо в таблице.
В таблице базы? Но если файл большой и таких файлов много? Это ж как базу раздует...

3. С этим согласна.
4. Подумаю и переделаю.
5. Вот до этого не додумалась :)
6.
>Кстати, почему в качестве last-modified выдается текущее время, если файл имеет явное время модификации?
Потому что файл изменился и нужно зафиксировать время, когда он изменился. Изменился сейчас - ставим время на данный момент.

7.
>это как?
Если диапазон такой: 521000- , мы должны взять первые байты тела объекта, тогда начинаем брать с нулевого байта и до 521000, значит $from = 0; $to = $range[0]; - или в этом случае $to = 521000.
if($range[1]==''){ - если элемент массива(который мы получали explode), пустой, то...
Аналогичным образом определяла диапазон типа -9000, когда нужно взять последние байты тела объекта.


Остальное буду исправлять.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 17:11)   письмо автору
 
   для: Лена   (01.03.2009 в 17:00)
 

2. 32 байта на запись? Мелочь.
Как вариант можно сами файлы держать под именами, равными md5. Или md5 с seq-суффиксом.

6. но изменился-то он именно тогда, когда filemtime, а не позже?

7.
>Если диапазон такой: 521000- , мы должны взять первые байты тела объекта,
нет. см. (22.02.2009 в 22:51)
521000- означает "Взять с позиции 521000 и до конца файла".
И это, кстати 99.9% всех издаваемых диапазонных запросов.

>Аналогичным образом определяла диапазон типа -9000, когда нужно взять последние байты тела объекта.
а здесь верно.
-9000 означает взять последние 9000 байт файла.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 17:16)   письмо автору
 
   для: Trianon   (01.03.2009 в 17:11)
 

>Получается, если скрипт большой, надо куски отдельно проверять
Безусловно. неплохо его еще и на функции делить.

Я заметил ошибку по тексту, и не поверив в результат, решил этот фрагмент проверить.

  Ответить  
 
 автор: Trianon   (01.03.2009 в 17:45)   письмо автору
 
   для: Trianon   (01.03.2009 в 17:11)
 

опять же, учтите , что
------------------------------------------------------
13.29. Поле Last-Modified

Поле заголовка объекта Last-Modified указывает на дату и время, при которых, по мнению исходного сервера, данный объект был модифицирован.

Last-Modified = "Last-Modified" ":" HTTP-date

Пример его использования

Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT

Точное значение этого заголовка зависит от реализации исходного сервера и природы ресурса. Для файлов, это может быть дата последней модификации файловой системы. Для объектов с динамическими встроенными частями это может время последней модификации одной из встроенных компонент. Для шлюзов баз данных это может быть метка последней модификации рекорда. Для виртуальных объектов это может быть время последнего изменения внутреннего состояния.

Исходный сервер не должен посылать дату Last-Modified, которая позже, чем время формирования сообщения сервера. В таких случаях, когда последняя модификация объекта указывает некоторое на время в будущем, сервер должен заменить дату на время формирования сообщения.

Исходный сервер должен получить значение Last-Modified объекта как можно ближе по времени к моменту генерации значения Date отклика. Это позволяет получателю выполнить точную оценку времени модификации объекта, в особенности, если объект был изменен буквально накануне формирования отклика.

Серверы HTTP/1.1 должны посылать поле Last-Modified всякий раз, когда это возможно.
------------------------------------------------------

  Ответить  
 
 автор: Лена   (02.03.2009 в 12:56)   письмо автору
12.1 Кб
 
   для: Trianon   (01.03.2009 в 17:45)
 

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

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

<?php
include_once("configs/dbopen.php");
//получаем нужную книгу по ссылке
if(isset($_GET['id_book'])){
        
//выбираем название книги, путь и расширение файла из базы
        
$sql "SELECT * FROM books WHERE id =" $_GET['id_book'];
        
$res mysql_query($sql);
                    if(!
$res) exit ("Error in $sql:" mysql_error());
                        while(
$row mysql_fetch_assoc($res)){
                        
//print_r($row);
                                //определяем расширение
                                
$extension $row['type'];
                                
$ext = array('zip''rar''gz''tgz''jpg','jpe','jpeg''gif''png''txt''pdf', );
                                
$stream = array('application/x-zip''application/x-rar-compressed''application/x-gzip','application/x-gzip','image/jpeg','image/jpeg','image/jpeg','image/gif''image/png''text/plain''application/pdf');
                                    
$arr array_combine($ext,$stream);
                                        foreach(
$arr as $k=>$v){
                                            if(
$extension == $k$mime $v;
                                            else 
$mime 'application/octet-stream';
                                        }
                                        
$filename $row['path'] . "/"$row['title'] . "." $row['type'];

//проверка на существование и воспроизведение нужного файла
if (!file_exists($filename) && !is_readable($filename)) exit('File does not exist or don`t readable');

//сервер сообщает клиенту, что непротив обслуживать байт-диапазонные запросы
header('Accept-Ranges: bytes');

    
$f fopen($filename'rb');
       
//проверка, открылся ли файл
        
if(!$f){
        
header ("HTTP/1.1 505 Internal server error");
        exit();
        }
    
flock($fLOCK_EX);
                                        
    
fseek($f0SEEK_END); //указатель - в конец файла, получаем позицию указателя
    
$size ftell($f);
                                        
    
//метка объекта, хеш всего файла, md5_file($filename)
    //выбираем из базы, вычисляем при загрузке данных про файл в базу
    
$etag_server $row['hash'];
                                        
    
//время последней модификации файла
    
$last_mod filemtime($filename);
                                        
    
//клиент запросил докачку
    
if (isset($_SERVER['HTTP_RANGE'])) {
        
//проверяем, что лежит в $_SERVER['HTTP_RANGE']: bytes=20-50/длина файла
            
if (!preg_match('/^bytes=(?:\d+-\d+|-\d+|\d+-//\d+)$/'$_SERVER['HTTP_RANGE'])){
                
//если range некорректный, посылаем весь текст
                
header("HTTP/1.1 416 Requested Range Not Satisfiable");
                
header("HTTP/1.1 200 OK");
                
header('Content-Type: '$mime);
                
header('Content-Disposition: attachment; filename=' basename($filename));
                
header('Content-Length:' $size);
                
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                }
                                            
                
// условные запросы 
                
if(isset($_SERVER['HTTP_IF_RANGE'])){
                
//определяем, чем являются первые символы - меткой или датой
                
$ifr trim($_SERVER['HTTP_IF_RANGE']); 
                
$iftag $ifdate '';
                    if(
ctype_digit($ifr)){
                    
$ifdate strtoftime($ifr);
                    
//если это дата, проверяем время модификации
                    
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
                    
$ifmod strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
                        if(
$ifdate == $ifmod && $ifmod == $last_mod){
                            
header("HTTP/1.1 304 Not Modified"); //файл не поменялся
                            
header("ETag: \""$etag_server ."\"");
                            
// последнее изменение
                            
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                            
//страница не будет меняться 10 минут
                            
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                            exit();
                        }else{
                        if(
$ifmod $last_mod){
                        
//файл изменился, присваиваем метку
                        
header("ETag: \""$etag_server ."\"");
                        
// последнее изменение
                        
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                        
//страница не будет меняться 10 минут
                        
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                        }
                         }
                        }
                        }else{
                        
$iftag $ifr;
                        
//если это метка, проверяем, совпадает ли метка объекта с текущей
                            
if(isset($_SERVER['HTTP_IF_MATCH']) && $_SERVER['HTTP_IF_MATCH'] != "*"){
                                
$etag_client trim($_SERVER['HTTP_IF_MATCH']);
                                
//метка не совпала
                                    
if($iftag == $etag_client && $etag_client !== $etag_server){
                                        
//присылаем полный объект
                                        
header("HTTP/1.1 200 OK");
                                        
header('Content-Length:' $size);
                                        
//надо присвоить новую метку
                                        
header("ETag: \""$etag_server ."\"");
                                        
// зафиксировать последнее изменение
                                        
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                                        
//страница не будет меняться 10 минут
                                        
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                                    }else{
                                        
//метка совпала, значит, файл не менялся    
                                        
header("HTTP/1.1 304 Not Modified");
                                        
header("ETag: \""$etag_server ."\"");
                                        
// последнее изменение
                                        
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                                        
//страница не будет меняться 10 минут
                                        
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                                    }
                                }
                            }
//if(ctype_digit
                        
}else{
                        
//условных запросов нет, заголовок - предварит. усл. не выполнены
                        
header("HTTP/1.1 412 Precondition Failed");
                        
header("ETag: \""$etag_server ."\"");
                        
// последнее изменение
                        
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                        
//страница не будет меняться 10 минут
                        
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                        exit();    
                        }
                                                
                        
//определяем диапазон
                        
$range=substr($_SERVER['HTTP_RANGE'], strpos($_SERVER['HTTP_RANGE'], '=')+1);
                        
$range substr($rangestrpos($range,0,"/"));
                           
$range explode("-"$range);
                        
$from $range[0];
                        
$to $range[1];
                        
//если первая позиция не задана, берем ее с нуля
                        
if($from 0$from 0;
                        if(
$to$to = ++ $to;
                        
//если не задана позиция конца
                        
else $to $size;
                                                                    
                        
//от позиции и до конца, диапазон типа 521000-
                            
if($range[1]==''){
                                
$from $range[0];
                                
$to $size;
                            }
                        
//последние байты, диапазон типа -300000
                            
if($range[0]==''){
                                
$from $range[1];
                                
$to $size;
                            }
                                                            
                        
//узнаем, запросил клиент полный файл либо кусок
                            
if ($from || $to $size) {
                            
//первый и последний байт куска
                            
$start $from;
                            
$end $to 1;
                            
header('HTTP/1.1 206 Partial Content');
                                                                
                            
//длина диапазона, который считывается
                            
$part $to $from;
                            
header('Content-Length:' $part);
                            
header('Content-Range: bytes ' $start "'-'" $end "'/'" $size);
                            
header("ETag: \""$etag_server ."\"");
                            
// последнее изменение
                            
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                            
//страница не будет меняться 10 минут
                            
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                            }else{
                           
// если клиент не запросил докачку или byte-range-spec некорректна
                            
header("HTTP/1.1 200 OK");
                            
header('Content-Length:' $size);
                            
header("ETag: \""$etag_server ."\"");
                            
// последнее изменение
                            
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                            
//страница не будет меняться 10 минут
                            
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                            }
                                }else{
                                    
//диапазон неопределен, значит, клиент запросил весь файл
                                    
header("HTTP/1.1 200 OK");
                                       
header('Content-Length:' $size);
                                    
header("ETag: \""$etag_server ."\"");
                                    
// последнее изменение
                                    
header ("Last-Modified: " gmdate("D, d M Y H:i:s \G\M\T"$last_mod));
                                    
//страница не будет меняться 10 минут
                                    
header("Expires: " gmdate("D, d M Y H:i:s \G\M\T"time()+60*10));
                                    
$range = array(0,$size);
                                    
$from $range[0];
                                    
$to $range[1];
                                    
$start $from;
                                    }
                                        
                                        
//отправляем заголовки и для целого файла, и для частичной загрузки
                                        
header('Content-Type: '$mime);
                                        
header('Content-Disposition: attachment; filename=' basename($filename));
                                        
                                        
//ставим указатель на начальную позицию, которую задал клиент
                                                   
fseek($f$startSEEK_SET);
                                                    
//выдача файла
                                                    
while(!feof($f)) {
                                                       if(
$to == $size){
                                                           
//выводим остаток до конца файла
                                                           
fpassthru($f);
                                                       }else{
                                                               
//выводим начало и середину
                                                            
$read fread($f256000);
                                                            echo 
$read;
                                                            
flush();
                                                        }
                                                    }
                                                
flock($fLOCK_UN); 
                                                
fclose($f);
                        }
//while($row =
        //счетчик скачиваний
        
$sql_update "UPDATE books SET count_download = count_download + 1 WHERE id =" $_GET['id_book'];
        
mysql_query($sql_update);
}
//if(isset($_GET
?>

  Ответить  
Rambler's Top100
вверх

Rambler's Top100 Яндекс.Метрика Яндекс цитирования