JustPaste.it

Pobieranie pliku przez skrypt PHP z możliwością wznowienia przerwanego połączeni

Jak wiadomo serwer HTTP zajmuje się udostępnianiem plików. Gdy klient (zazwyczaj przeglądarka internetowa) poprosi o jakiś dokument, serwer odsyła jego zawartość poprzedzoną nagłówkami HTTP informującymi o typie MIME czy też wielkości tego dokumentu. W momencie, gdy przeglądarka nie jest wstanie obsłużyć danego typu, wyświetla użytkownikowi komunikat pytający czy plik ma zostać zapisany na dysku.

Aby za pomocą naszego skryptu PHP można było ściągnąć przykładowy 'plik.txt' z serwera HTTP należy oszukać przeglądarkę. Dać jej do zrozumienia, że skrypt, który właśnie pobiera jest załącznikiem. Robi się to poprzez nagłówek 'Content-Disposition' z wartością 'attachment'. Po nim należy podać nazwę załącznika, bo w przeciwnym wypadku domyślną nazwą naszego pliku będzie nazwa skryptu, który go wysłał.

header('Content-Disposition: attachment; filename=plik.txt');

W następnej kolejności trzeba zmusić przeglądarkę do pokazania już wspomnianego pytania o zapis na dysk żeby 'plik.txt' nie został przez nią zwyczajnie wyświetlony. W nagłówku 'Content-Type' podajemy wartość 'application/x-unknown'.
header('Content-Type: application/x-unknown');

Teraz, mimo że jest to zwyczajny 'plik.txt' przeglądarka nie wie o tym i jedyne co jej pozostaje to pobrać go i zapisać. Wystarczy już wysłać do niej zawartość 'plik.txt'.
if ($fp = fopen('plik.txt', 'rb'))
    {
        flock($fp, 1);
        echo(fread($fp, filesize('plik.txt')));
        flock($fp, 3);
        fclose($fp);
    }

Co jednak, jeśli przez nasz skrypt PHP pobierany jest o wiele większy plik przy pomocy jakiegoś Download Acclerator'a i w trakcie tej operacji zostanie zerwane połączenie? Protokół HTTP 1.1 udostępnia mechanizmy umożliwiające ściągnięcie dowolnego fragmentu pliku. Niestety w naszym skrypcie one nie będą działały, dlatego trzeba je zaimplementować samemu, aby pozwolić klientowi na wznowienie pobierania.

Po pierwsze trzeba dołączyć nagłówek 'Accept-Ranges'. Jak na razie jedyna dostępna dla niego wartość to bytes. Dzięki temu klient wie ze może prosić o wybrane fragmenty pliku. Wykonuje to wysyłając nagłówek 'Range' z wartością 'bytes=a-b', gdzie 'a' to pierwszy bajt, od którego chce rozpocząć pobieranie, natomiast 'b' to ostatni bajt, który jak wynika z moich obserwacji jest opcjonalny. Kiedy go nie ma serwer powinien zwrócić wszystko do końca pliku. Wartości te liczone są od zera, czyli pierwszy bajt pliku o wielkości 1024 to '0' a ostatni to '1023'. Nagłówek 'Range' dostępny jest w zmiennej '$_SERVER["HTTP_RANGE"]'.

Cała sprawa polega już tylko na tym, aby wyciągnąć wartości, wykonać kilka obliczeń a następnie przesunąć wskaźnik do pobieranego pliku na wybrane przez klienta miejsce i wysłać mu tyle danych ile potrzebuje. Oczywiście należy go poinformować, co dokładnie jest mu wysyłane, mimo iż powinien pamiętać, o co przed chwilą prosił ;) W tym wypadku nie podaje się tak jak zwykle nagłówka 'HTTP/1.1 200 OK' tylko 'HTTP/1.1 206 Partial Content'. Parametry przesyłanych danych podaje się w 'Content-Range: bytes a-b/c', gdzie wartości 'a' i 'b' mają takie znaczenie jak poprzednio, natomiast 'c' to wielkość całego pliku. Należy zwrócić uwagę na to, że w takiej sytuacji wartość nagłówka 'Content-Length' nie powinna stanowić wielkości całego pliku a jedynie tej części która jest właśnie wysyłana. Przy żądaniu przez klienta ostatnich 400 bajtów z pliku o wielkości 1000 bajtów powinno to wyglądać tak:
header('HTTP/1.1 206 Partial Content');
    header('Accept-Ranges: bytes');
    header('Content-Range: bytes 600-999/1000');
    header('Content-Length: 400');

Poniżej znajduje się przykładowy skrypt, który wykonuje opisane przeze mnie czynności. Ścieżkę do pliku, który ma być ściągnięty należy podać w zmiennej 'file' w URLu.

<?php

    // Download file in PHP by piechnat
    // http://piechnat.prv.pl

    $file = stripslashes($_GET['file']);

    if (! is_file($file))
    {
        header('HTTP/1.1 404 File Not Found');
        die();
    }

    $fname = basename($file);
    $fsize = filesize($file);
    $ftime = filemtime($file);
    $range = $_SERVER['HTTP_RANGE'];

    $r_start = 0;
    $c_length = $fsize;

    if ($range)
    {
        $reg = "/bytes=([0-9]+)-([0-9]*)/";
        preg_match($reg, $range, $matches);

        $r_start = (int) $matches[1];
        $r_stop = (int) $matches[2];
        if ($r_stop < $r_start) $r_stop = $fsize - 1;
        $c_length = $r_stop - $r_start + 1;

        header('HTTP/1.1 206 Partial Content');
        header('Content-Range: bytes ' .
            $r_start . '-' . $r_stop . '/' . $fsize);
    }
    else
    {
        header('HTTP/1.1 200 OK');
    }

    header('Last-Modified: ' .
        gmdate('D, d M Y H:i:s', $ftime) . ' GMT');
    header('Content-Disposition: ' .
        'attachment; filename="' . $fname . '"');
    header('Accept-Ranges: bytes');
    header('Content-Type: application/x-unknown');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: ' . $c_length);

    if ($fp = fopen($file, 'rb'))
    {
        flock($fp, 1);
        fseek($fp, $r_start);
        echo(fread($fp, $c_length));
        flock($fp, 3);
        fclose($fp);
    } 

?>


To by było na tyle...

 


Źródło: 4programmers.net. Treść udostępniona na zasadach licencji Creative Commons Attribution

 

Autor: 4programmers.net

Licencja: Creative Commons - bez utworów zależnych