Организация докачки в php HTTP_RANGE

Рейтинг: 14Ответов: 2Опубликовано: 06.07.2011

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

Ситуация: Скрипт генерит файлы на сервере во временную папку, потом используется скрипт отдачи (http_range, 206 partial content и т.д.). Как мне узнать когда можно его удалять.

Предложения с кроном не предлагать!

Ответы

▲ 4Принят

Определить в PHP, что файл был гарантированно скачен, к сожалению, не удастся. Однако, если в качестве фронтенда используется nginx, то можно, по крайне мере, гарантировано убедится, что файл (в данном случае часть файла) был отдан.

В nginx имеется директива post_action, которая делает повторный внутренний запрос уже после того, как ответ уже был отправлен клиенту. Соответственно, благодаря ней можно узнать сколько реально данных было отправлено клиенту. Выглядеть конфиг будет примерно так:

# скрипт, который отдает файл по частям
location = /download.php {
    fastcgi_pass localhost:9000;
    fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/$fastcgi_script_name;
    include fastcgi_params;
    post_action /complete;
}

# а этот скрипт уже будет подсчитывать байты
location = /complete {
    internal;
    fastcgi_pass localhost:9000;
    fastcgi_param SCRIPT_FILENAME /var/www/localhost/htdocs/complete.php;

    # количество отправленных клиенту байт
    fastcgi_param BYTE-SEND $body_bytes_sent;

    # заголовок Range
    fastcgi_param RANGE $http_range;

    include fastcgi_params;
}

После того, как download.php отправит очередной кусок файла, nginx сделает внутренний запрос к complete.php, где в $_SERVER['BYTE-SEND'] будет количество отправленных байт, а в $_SERVER['BYTE-SEND'] заголовок Range от клиента.

Ну а дальше уже дело техники. Считаем где-нибудь (например в сессии) байты или для большей надежности лучше промежутки Range. И кода количество отданных байт будет равно размеру файла - значит файл был отдан целиком.

PS К сожалению директива post_action не задокументирована.

UPD. Если PHP работает через fastcgi или за ним стоит любой reverse-proxy (опять же nginx например), то на самом PHP определять это бесполезно. После того как PHP отдаст веб-серверу кусок данных, дальнейшая судьба о нем ему уже не известна.

Если же PHP работает на apache через mod_php без каких-либо reverse-proxy, тогда, вероятно, еще с натяжкой можно считать, что после flush() данные ушли клиенту.

Еще один способ, это отдавать файл напрямую через socket'ы. Т.е. написать скрипт, который бы сам выступал в качестве веб-сервера. Однако это крайне не производительное решение.

Все, других вариантов на PHP я не вижу.

▲ 2

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

<?php
set_time_limit(0);
function file_download($filename, $mimetype='audio/mpeg') {
    $download_speed =   51200; // 51200
    $time_discret   =   1;
    if (file_exists($filename)) {
        $f              =   fopen($filename, 'r');
        if (isset($_SERVER['HTTP_RANGE'])) {
            $load_from      =   preg_replace('#[^0-9]#', '', $_SERVER['HTTP_RANGE']);
            fseek($f, $load_from);
            $filesize       =   filesize($filename);
            header('HTTP/1.1 206 Partial Content');
            header('Content-Type: ' . $mimetype);
            header('Content-Range: bytes '.$load_from.'-'.$filesize.'/'.$filesize);
            header('Last-Modified: ' . gmdate('r', filemtime($filename)));
            header('ETag: ' . sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)));
            header('Accept-Ranges: bytes');
            header('Content-Length: ' . (filesize($filename)));
            header('Connection: close');
            header('Content-Disposition: attachment; filename="' . basename($filename) . '";');
        } else {
            header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
            header('Content-Type: ' . $mimetype);
            header('Last-Modified: ' . gmdate('r', filemtime($filename)));
            header('ETag: ' . sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)));
            header('Content-Length: ' . (filesize($filename)));
            header('Connection: close');
            header('Content-Disposition: attachment; filename="' . basename($filename) . '";');
        }
            if((int) $download_speed > 0) {
                while(!feof($f)) {
                    $time_start = microtime(true);
                    echo fread($f, ceil($download_speed*$time_discret));
                    flush();
                    $time_end = microtime(true);
                    $time = $time_end - $time_start;
                    if($time_discret-$time > 0) usleep(($time_discret-$time)*1000000);
                }
            } else {
                while(!feof($f)) {
                    echo fread($f, 1024);
                    flush();
                }
            }
        fclose($f);

    } else {
        header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
        header('Status: 404 Not Found');
    }
    exit;
}
#
file_download('./20_dt8_project.mp3', $mimetype='audio/mpeg');
?>

Скрипт отправляет файл на скачку клиенту, а если нужно докачать файл - докачивает). Части брал с сайтов, часть сам дописал, докачка имеется, один из заголовков говорит, что она есть. Но сразу предупреждаю, такой скрипт очень сильно грузит сервер.