Pobieranie pliku przez skrypt PHP z możliwością wznowienia przerwanego połączenia
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 w stanie 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ł.
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'.
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'.
{
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('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 File Downloader by Mateusz Piechnat
[http://piechnat.prv.pl]
------------------------------------------------------------------------- */
define('ALLOWED_REFERRER', '');
define('ROOT_DIRECTORY', '.');
define('SEND_BUF_LEN', 1024 * 8);
/* ---------------------------------------------------------------------- */
@set_time_limit(0);
if (function_exists('apache_setenv')) @apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
while (ob_get_level()) ob_end_clean();
ob_implicit_flush(1);
/* ---------------------------------------------------------------------- */
$http_ref = strtoupper(@$_SERVER['HTTP_REFERER']);
if (($http_ref !== '') && (ALLOWED_REFERRER !== ''))
{
if (strpos($http_ref, strtoupper(ALLOWED_REFERRER)) === false)
{
header('HTTP/1.1 500 Internal Server Error');
die('Internal server error.');
}
}
/* ---------------------------------------------------------------------- */
$file = @$_GET['file'];
if (@get_magic_quotes_gpc()) $file = stripslashes($file);
$root_dir = realpath(ROOT_DIRECTORY);
$file = realpath($root_dir . '/' . $file);
if ((strpos($file, $root_dir) !== 0) || (! is_file($file)))
{
header('HTTP/1.1 404 File Not Found');
die('File not found.');
}
/* ---------------------------------------------------------------------- */
$fname = basename($file);
$fsize = filesize($file);
$ftime = filemtime($file);
$fmime = '';
$range = @$_SERVER['HTTP_RANGE'];
$r_start = 0;
$c_length = $fsize;
/* ---------------------------------------------------------------------- */
if (preg_match('/bytes=([0-9]*)-([0-9]*)/', $range, $tmp))
{
$r_start = (int) $tmp[1];
$r_stop = (int) $tmp[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');
}
/* ---------------------------------------------------------------------- */
if (function_exists('mime_content_type'))
{
$fmime = mime_content_type($file);
}
else if (function_exists('finfo_file'))
{
$finfo = finfo_open(FILEINFO_MIME);
$fmime = finfo_file($finfo, $file);
finfo_close($finfo);
}
if ($fmime == '')
{
$fmime = 'application/force-download';
}
/* ---------------------------------------------------------------------- */
header('Accept-Ranges: bytes');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $ftime) . ' GMT');
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false);
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename="' . $fname . '"');
header('Content-Type: ' . $fmime);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $c_length);
flush();
/* ---------------------------------------------------------------------- */
if ($fp = @fopen($file, 'rb'))
{
@flock($fp, 1);
@fseek($fp, $r_start);
while ((! feof($fp)) && ($c_length > SEND_BUF_LEN))
{
print(fread($fp, SEND_BUF_LEN));
$c_length = $c_length - SEND_BUF_LEN;
flush();
if (connection_status() != 0) break;
}
if ((! feof($fp)) && (connection_status() == 0))
{
print(fread($fp, $c_length));
flush();
}
@flock($fp, 3);
@fclose($fp);
}
/* ---------------------------------------------------- END OF SCRIPT --- */
To by było na tyle...



echo(fread($fp, filesize('plik.txt')));
flock($fp, 3);
fclose($fp);
dla dużych plików, jest to sposób raczej wątpliwy
Lepiej wysyłać metodą:
{
echo fread($f, 512 * 1024);
}
o wiele lepszy, bo jak mam hosting PHP z ograniczeniem na fopen() .....