Pliki do pobrania, które można wznowić, gdy wysyłasz plik za pomocą PHP?

104

Używamy skryptów PHP do tunelowania pobierania plików, ponieważ nie chcemy ujawniać bezwzględnej ścieżki do pliku do pobrania:

header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);

Niestety zauważyliśmy, że użytkownik końcowy nie może wznowić pobierania przechodzącego przez ten skrypt.

Czy za pomocą takiego rozwiązania opartego na PHP jest jakiś sposób na obsługę wznawiania pobierania?

Mark Amery
źródło

Odpowiedzi:

102

Pierwszą rzeczą, którą musisz zrobić, jest wysłanie Accept-Ranges: bytesnagłówka we wszystkich odpowiedziach, aby poinformować klienta, że ​​obsługujesz częściową zawartość. Następnie, jeśli prośba o Range: bytes=x-y odebraniu nagłówka (z xi ybędących numerami) analizowania zakresu klient żąda, otwórz plik jako zwykły zasięgnij xbajtów naprzód i wysłać kolejne y- xbajtów. Ustaw także odpowiedź na HTTP/1.0 206 Partial Content.

Bez przetestowania czegokolwiek mogłoby to zadziałać, mniej więcej:

$filesize = filesize($file);

$offset = 0;
$length = $filesize;

if ( isset($_SERVER['HTTP_RANGE']) ) {
    // if the HTTP_RANGE header is set we're dealing with partial content

    $partialContent = true;

    // find the requested range
    // this might be too simplistic, apparently the client can request
    // multiple ranges, which can become pretty complex, so ignore it for now
    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);

    $offset = intval($matches[1]);
    $length = intval($matches[2]) - $offset;
} else {
    $partialContent = false;
}

$file = fopen($file, 'r');

// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);

$data = fread($file, $length);

fclose($file);

if ( $partialContent ) {
    // output the right headers for partial content

    header('HTTP/1.1 206 Partial Content');

    header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $filesize);
}

// output the regular HTTP headers
header('Content-Type: ' . $ctype);
header('Content-Length: ' . $filesize);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');

// don't forget to send the data too
print($data);

Mogłem przeoczyć coś oczywistego i zdecydowanie zignorowałem niektóre potencjalne źródła błędów, ale to powinien być początek.

Jest tutaj opis częściowej zawartości i znalazłem informacje na temat częściowej zawartości na stronie dokumentacji dla fread .

Theo
źródło
3
Mały błąd, twoje wyrażenie regularne powinno wyglądać następująco: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ pasuje)
deepwell
1
Masz rację i zmieniłem to. Jednak i tak jest to zbyt uproszczone, zgodnie ze specyfikacjami, które możesz zrobić "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab" itd., Więc błąd w poprzednia wersja była brakującym ukośnikiem na końcu, a nie brakiem znaku zapytania.
Theo,
7
Bardzo pomocne, ale musiałem dokonać dwóch drobnych poprawek, aby to zadziałało: 1. Jeśli klient nie wyśle ​​punktu końcowego w zakresie (ponieważ jest niejawny), $lengthbędzie ujemny. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;naprawia to. 2. Content-Rangetraktuje pierwszy bajt jako bajt 0, więc ostatni bajt jest $filesize - 1. Dlatego tak musi być ($offset + $length - 1).
Dennis
1
Powyższe nie działa dla dużych pobrań, pojawia się „Błąd krytyczny PHP: Wyczerpano dozwolony rozmiar pamięci wynoszący XXXX bajtów (próbowano przydzielić XXX bajtów) w”. W moim przypadku 100MB było za duże. Zasadniczo zapisujesz cały plik w zmiennej i wypluwasz go.
sarah.ferguson
1
Możesz rozwiązać problem z dużym plikiem, odczytując go fragmentami zamiast wszystkich naraz.
dynamichael
71

EDYCJA 2017/01 - Napisałem bibliotekę, aby to zrobić w PHP> = 7.0 https://github.com/DaveRandom/Resume

EDYCJA 2016/02 - Kod całkowicie przepisany na zestaw narzędzi modułowych jako przykład użycia zamiast funkcji monolitycznej. Wprowadzono poprawki wymienione w komentarzach poniżej.


Przetestowane, działające rozwiązanie (w dużej mierze oparte na powyższej odpowiedzi Theo), które zajmuje się wznowieniem pobierania, w zestawie kilku samodzielnych narzędzi. Ten kod wymaga PHP w wersji 5.4 lub nowszej.

To rozwiązanie nadal radzi sobie tylko z jednym zakresem na żądanie, ale w każdych okolicznościach ze standardową przeglądarką, o której mogę pomyśleć, nie powinno to powodować problemu.

<?php

/**
 * Get the value of a header in the current request context
 *
 * @param string $name Name of the header
 * @return string|null Returns null when the header was not sent or cannot be retrieved
 */
function get_request_header($name)
{
    $name = strtoupper($name);

    // IIS/Some Apache versions and configurations
    if (isset($_SERVER['HTTP_' . $name])) {
        return trim($_SERVER['HTTP_' . $name]);
    }

    // Various other SAPIs
    foreach (apache_request_headers() as $header_name => $value) {
        if (strtoupper($header_name) === $name) {
            return trim($value);
        }
    }

    return null;
}

class NonExistentFileException extends \RuntimeException {}
class UnreadableFileException extends \RuntimeException {}
class UnsatisfiableRangeException extends \RuntimeException {}
class InvalidRangeHeaderException extends \RuntimeException {}

class RangeHeader
{
    /**
     * The first byte in the file to send (0-indexed), a null value indicates the last
     * $end bytes
     *
     * @var int|null
     */
    private $firstByte;

    /**
     * The last byte in the file to send (0-indexed), a null value indicates $start to
     * EOF
     *
     * @var int|null
     */
    private $lastByte;

    /**
     * Create a new instance from a Range header string
     *
     * @param string $header
     * @return RangeHeader
     */
    public static function createFromHeaderString($header)
    {
        if ($header === null) {
            return null;
        }

        if (!preg_match('/^\s*(\S+)\s*(\d*)\s*-\s*(\d*)\s*(?:,|$)/', $header, $info)) {
            throw new InvalidRangeHeaderException('Invalid header format');
        } else if (strtolower($info[1]) !== 'bytes') {
            throw new InvalidRangeHeaderException('Unknown range unit: ' . $info[1]);
        }

        return new self(
            $info[2] === '' ? null : $info[2],
            $info[3] === '' ? null : $info[3]
        );
    }

    /**
     * @param int|null $firstByte
     * @param int|null $lastByte
     * @throws InvalidRangeHeaderException
     */
    public function __construct($firstByte, $lastByte)
    {
        $this->firstByte = $firstByte === null ? $firstByte : (int)$firstByte;
        $this->lastByte = $lastByte === null ? $lastByte : (int)$lastByte;

        if ($this->firstByte === null && $this->lastByte === null) {
            throw new InvalidRangeHeaderException(
                'Both start and end position specifiers empty'
            );
        } else if ($this->firstByte < 0 || $this->lastByte < 0) {
            throw new InvalidRangeHeaderException(
                'Position specifiers cannot be negative'
            );
        } else if ($this->lastByte !== null && $this->lastByte < $this->firstByte) {
            throw new InvalidRangeHeaderException(
                'Last byte cannot be less than first byte'
            );
        }
    }

    /**
     * Get the start position when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getStartPosition($fileSize)
    {
        $size = (int)$fileSize;

        if ($this->firstByte === null) {
            return ($size - 1) - $this->lastByte;
        }

        if ($size <= $this->firstByte) {
            throw new UnsatisfiableRangeException(
                'Start position is after the end of the file'
            );
        }

        return $this->firstByte;
    }

    /**
     * Get the end position when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getEndPosition($fileSize)
    {
        $size = (int)$fileSize;

        if ($this->lastByte === null) {
            return $size - 1;
        }

        if ($size <= $this->lastByte) {
            throw new UnsatisfiableRangeException(
                'End position is after the end of the file'
            );
        }

        return $this->lastByte;
    }

    /**
     * Get the length when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getLength($fileSize)
    {
        $size = (int)$fileSize;

        return $this->getEndPosition($size) - $this->getStartPosition($size) + 1;
    }

    /**
     * Get a Content-Range header corresponding to this Range and the specified file
     * size
     *
     * @param int $fileSize
     * @return string
     */
    public function getContentRangeHeader($fileSize)
    {
        return 'bytes ' . $this->getStartPosition($fileSize) . '-'
             . $this->getEndPosition($fileSize) . '/' . $fileSize;
    }
}

class PartialFileServlet
{
    /**
     * The range header on which the data transmission will be based
     *
     * @var RangeHeader|null
     */
    private $range;

    /**
     * @param RangeHeader $range Range header on which the transmission will be based
     */
    public function __construct(RangeHeader $range = null)
    {
        $this->range = $range;
    }

    /**
     * Send part of the data in a seekable stream resource to the output buffer
     *
     * @param resource $fp Stream resource to read data from
     * @param int $start Position in the stream to start reading
     * @param int $length Number of bytes to read
     * @param int $chunkSize Maximum bytes to read from the file in a single operation
     */
    private function sendDataRange($fp, $start, $length, $chunkSize = 8192)
    {
        if ($start > 0) {
            fseek($fp, $start, SEEK_SET);
        }

        while ($length) {
            $read = ($length > $chunkSize) ? $chunkSize : $length;
            $length -= $read;
            echo fread($fp, $read);
        }
    }

    /**
     * Send the headers that are included regardless of whether a range was requested
     *
     * @param string $fileName
     * @param int $contentLength
     * @param string $contentType
     */
    private function sendDownloadHeaders($fileName, $contentLength, $contentType)
    {
        header('Content-Type: ' . $contentType);
        header('Content-Length: ' . $contentLength);
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
        header('Accept-Ranges: bytes');
    }

    /**
     * Send data from a file based on the current Range header
     *
     * @param string $path Local file system path to serve
     * @param string $contentType MIME type of the data stream
     */
    public function sendFile($path, $contentType = 'application/octet-stream')
    {
        // Make sure the file exists and is a file, otherwise we are wasting our time
        $localPath = realpath($path);
        if ($localPath === false || !is_file($localPath)) {
            throw new NonExistentFileException(
                $path . ' does not exist or is not a file'
            );
        }

        // Make sure we can open the file for reading
        if (!$fp = fopen($localPath, 'r')) {
            throw new UnreadableFileException(
                'Failed to open ' . $localPath . ' for reading'
            );
        }

        $fileSize = filesize($localPath);

        if ($this->range == null) {
            // No range requested, just send the whole file
            header('HTTP/1.1 200 OK');
            $this->sendDownloadHeaders(basename($localPath), $fileSize, $contentType);

            fpassthru($fp);
        } else {
            // Send the request range
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: ' . $this->range->getContentRangeHeader($fileSize));
            $this->sendDownloadHeaders(
                basename($localPath),
                $this->range->getLength($fileSize),
                $contentType
            );

            $this->sendDataRange(
                $fp,
                $this->range->getStartPosition($fileSize),
                $this->range->getLength($fileSize)
            );
        }

        fclose($fp);
    }
}

Przykładowe użycie:

<?php

$path = '/local/path/to/file.ext';
$contentType = 'application/octet-stream';

// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
ini_set('display_errors', '0');

try {
    $rangeHeader = RangeHeader::createFromHeaderString(get_request_header('Range'));
    (new PartialFileServlet($rangeHeader))->sendFile($path, $contentType);
} catch (InvalidRangeHeaderException $e) {
    header("HTTP/1.1 400 Bad Request");
} catch (UnsatisfiableRangeException $e) {
    header("HTTP/1.1 416 Range Not Satisfiable");
} catch (NonExistentFileException $e) {
    header("HTTP/1.1 404 Not Found");
} catch (UnreadableFileException $e) {
    header("HTTP/1.1 500 Internal Server Error");
}

// It's usually a good idea to explicitly exit after sending a file to avoid sending any
// extra data on the end that might corrupt the file
exit;
DaveRandom
źródło
Całkiem niezły kod. Znalazłem błąd w wierszu, w którym ustawiono $ length. Powinno być: $ długość = $ koniec - $ początek + 1;
bobwienholt
Jak wstrzymam pobieranie
Prasanth Bendra,
3
Czy Content-Length należy ustawić na rzeczywisty rozmiar pliku, czy tylko na liczbę wysyłanych częściowych bajtów? Wygląda na to, że ta strona powinna zawierać częściowe bajty, ale tak nie jest w powyższym przykładowym kodzie. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus
3
Kolejna mała literówka: $start = $end - intval($range[0]);powinna brzmiećrange[1]
BurninLeo
1
@ sarah.ferguson Kod całkowicie przepisany i zaktualizowany, patrz wyżej.
DaveRandom
16

To działa w 100% super sprawdź, używam i nie ma już problemów.

        /* Function: download with resume/speed/stream options */


         /* List of File Types */
        function fileTypes($extension){
            $fileTypes['swf'] = 'application/x-shockwave-flash';
            $fileTypes['pdf'] = 'application/pdf';
            $fileTypes['exe'] = 'application/octet-stream';
            $fileTypes['zip'] = 'application/zip';
            $fileTypes['doc'] = 'application/msword';
            $fileTypes['xls'] = 'application/vnd.ms-excel';
            $fileTypes['ppt'] = 'application/vnd.ms-powerpoint';
            $fileTypes['gif'] = 'image/gif';
            $fileTypes['png'] = 'image/png';
            $fileTypes['jpeg'] = 'image/jpg';
            $fileTypes['jpg'] = 'image/jpg';
            $fileTypes['rar'] = 'application/rar';

            $fileTypes['ra'] = 'audio/x-pn-realaudio';
            $fileTypes['ram'] = 'audio/x-pn-realaudio';
            $fileTypes['ogg'] = 'audio/x-pn-realaudio';

            $fileTypes['wav'] = 'video/x-msvideo';
            $fileTypes['wmv'] = 'video/x-msvideo';
            $fileTypes['avi'] = 'video/x-msvideo';
            $fileTypes['asf'] = 'video/x-msvideo';
            $fileTypes['divx'] = 'video/x-msvideo';

            $fileTypes['mp3'] = 'audio/mpeg';
            $fileTypes['mp4'] = 'audio/mpeg';
            $fileTypes['mpeg'] = 'video/mpeg';
            $fileTypes['mpg'] = 'video/mpeg';
            $fileTypes['mpe'] = 'video/mpeg';
            $fileTypes['mov'] = 'video/quicktime';
            $fileTypes['swf'] = 'video/quicktime';
            $fileTypes['3gp'] = 'video/quicktime';
            $fileTypes['m4a'] = 'video/quicktime';
            $fileTypes['aac'] = 'video/quicktime';
            $fileTypes['m3u'] = 'video/quicktime';
            return $fileTypes[$extention];
        };

        /*
          Parameters: downloadFile(File Location, File Name,
          max speed, is streaming
          If streaming - videos will show as videos, images as images
          instead of download prompt
         */

        function downloadFile($fileLocation, $fileName, $maxSpeed = 100, $doStream = false) {
            if (connection_status() != 0)
                return(false);
        //    in some old versions this can be pereferable to get extention
        //    $extension = strtolower(end(explode('.', $fileName)));
            $extension = pathinfo($fileName, PATHINFO_EXTENSION);

            $contentType = fileTypes($extension);
            header("Cache-Control: public");
            header("Content-Transfer-Encoding: binary\n");
            header('Content-Type: $contentType');

            $contentDisposition = 'attachment';

            if ($doStream == true) {
                /* extensions to stream */
                $array_listen = array('mp3', 'm3u', 'm4a', 'mid', 'ogg', 'ra', 'ram', 'wm',
                    'wav', 'wma', 'aac', '3gp', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'wmv', 'divx', 'asf');
                if (in_array($extension, $array_listen)) {
                    $contentDisposition = 'inline';
                }
            }

            if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) {
                $fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
                header("Content-Disposition: $contentDisposition;
                    filename=\"$fileName\"");
            } else {
                header("Content-Disposition: $contentDisposition;
                    filename=\"$fileName\"");
            }

            header("Accept-Ranges: bytes");
            $range = 0;
            $size = filesize($fileLocation);

            if (isset($_SERVER['HTTP_RANGE'])) {
                list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
                str_replace($range, "-", $range);
                $size2 = $size - 1;
                $new_length = $size - $range;
                header("HTTP/1.1 206 Partial Content");
                header("Content-Length: $new_length");
                header("Content-Range: bytes $range$size2/$size");
            } else {
                $size2 = $size - 1;
                header("Content-Range: bytes 0-$size2/$size");
                header("Content-Length: " . $size);
            }

            if ($size == 0) {
                die('Zero byte file! Aborting download');
            }
            set_magic_quotes_runtime(0);
            $fp = fopen("$fileLocation", "rb");

            fseek($fp, $range);

            while (!feof($fp) and ( connection_status() == 0)) {
                set_time_limit(0);
                print(fread($fp, 1024 * $maxSpeed));
                flush();
                ob_flush();
                sleep(1);
            }
            fclose($fp);

            return((connection_status() == 0) and ! connection_aborted());
        }

        /* Implementation */
        // downloadFile('path_to_file/1.mp3', '1.mp3', 1024, false);
user1524615
źródło
1
Głosowałem za, ponieważ ograniczenie prędkości jest naprawdę przydatne, jednak sprawdzenie MD5 wznowionego pliku (Firefox) wykazało niezgodność. Element str_replace dla $ range jest nieprawidłowy, powinien być kolejnym rozbiciem, wynik powinien być liczbowy, a myślnik dodany do nagłówka Content-Range.
WhoIsRich
Jak dostosować go do obsługi zdalnego pobierania plików?
Siyamak Shahpasand
1
miałeś na myśli podwójny cudzysłów „Content-Type: $ contentType”;
Matt,
set_time_limit (0); moim zdaniem nie jest właściwe. Może bardziej rozsądny limit 24 godzin?
twicejr
Dziękuję za sprawdzenie moich literówek :)!
user1524615
15

Tak. Wsparcie byteranges. Zobacz RFC 2616, sekcja 14.35 .

Zasadniczo oznacza to, że powinieneś przeczytać Rangenagłówek i rozpocząć obsługę pliku od określonego przesunięcia.

Oznacza to, że nie możesz użyć readfile (), ponieważ obsługuje on cały plik. Zamiast tego najpierw użyj fopen () , następnie fseek () do właściwej pozycji, a następnie użyj fpassthru () do wyświetlenia pliku.

Sietse
źródło
4
fpassthru nie jest dobrym pomysłem, jeśli plik ma wiele megabajtów, może zabraknąć pamięci. Po prostu fread () i print () w kawałkach.
Willem
3
fpassthru działa tutaj świetnie z setkami megabajtów. echo file_get_contents(...)nie działa (OOM). Więc nie sądzę, żeby to był problem. PHP 5.3.0
Janus Troelsen
1
@JanusTroelsen Nie, to nie. Wszystko zależy od konfiguracji twojego serwera. Jeśli masz mocny serwer z dużą ilością pamięci przeznaczonej dla PHP, może to działa dobrze. Na „słabych” konfiguracjach (dosłownie: współdzielone hosty) użycie fpassthrunie powiedzie się nawet na plikach 50 MB. Zdecydowanie nie powinieneś go używać, jeśli obsługujesz duże pliki na słabej konfiguracji serwera. Jak słusznie zauważa @Wimmer, fread+ printto wszystko, czego potrzebujesz w tym przypadku.
trejder
2
@trejder: Zobacz uwagę dotyczącą readfile () : readfile () samodzielnie nie spowoduje problemów z pamięcią, nawet podczas wysyłania dużych plików. Jeśli napotkasz błąd braku pamięci, upewnij się, że buforowanie wyjścia jest wyłączone za pomocą ob_get_level ().
Janus Troelsen,
1
@trejder problem polega na tym, że nie skonfigurowałeś poprawnie buforowania wyjścia. Robi to automatycznie, jeśli powiesz mu: php.net/manual/en/ ... np. Output_buffering = 4096 (a jeśli twój framework na to nie pozwala, twój framework jest do bani)
ZJR
11

Naprawdę fajnym sposobem rozwiązania tego problemu bez konieczności „rozwijania własnego” kodu PHP jest użycie modułu mod_xsendfile Apache. Następnie w PHP wystarczy ustawić odpowiednie nagłówki. Apache robi swoje.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Jonathan Hawkes
źródło
2
A jeśli chcesz odłączyć plik po wysłaniu?
Janus Troelsen
1
Jeśli chcesz odłączyć plik po wysłaniu, potrzebujesz specjalnej flagi, aby to wskazać, zobacz XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta ).
Jens A. Koch
9

Jeśli chcesz zainstalować nowy moduł PECL, najłatwiejszym sposobem obsługi pobierania wznawialnego za pomocą PHP jest wykonanie w http_send_file()ten sposób

<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>

źródło: http://www.php.net/manual/en/function.http-send-file.php

Używamy go do obsługi treści przechowywanych w bazie danych i działa jak marzenie!

Justin T.
źródło
3
Działa jak marzenie. Uważaj jednak, aby nie włączyć buforowania wyjścia (ob_start itp.). Szczególnie przy wysyłaniu dużych plików spowoduje to zbuforowanie całego żądanego zakresu.
Pieter van Ginkel
Kiedy to zostało dodane do PHP? Zawsze tam byłeś?
thomthom
1
To Pecl, a nie PHP. Nie mam tej funkcji.
Geo
4

Najlepsza odpowiedź zawiera różne błędy.

  1. Główny błąd: nie obsługuje poprawnie nagłówka Range. bytes a-bpowinno oznaczać [a, b]zamiast [a, b)ibytes a- nie jest obsługiwane.
  2. Drobny błąd: nie używa bufora do obsługi wyjścia. Może to zajmować zbyt dużo pamięci i powodować niską prędkość w przypadku dużych plików.

Oto mój zmodyfikowany kod:

// TODO: configurations here
$fileName = "File Name";
$file = "File Path";
$bufferSize = 2097152;

$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if (isset($_SERVER['HTTP_RANGE'])) {
    // if the HTTP_RANGE header is set we're dealing with partial content
    // find the requested range
    // this might be too simplistic, apparently the client can request
    // multiple ranges, which can become pretty complex, so ignore it for now
    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
    $offset = intval($matches[1]);
    $end = $matches[2] || $matches[2] === '0' ? intval($matches[2]) : $filesize - 1;
    $length = $end + 1 - $offset;
    // output the right headers for partial content
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes $offset-$end/$filesize");
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($file));
header("Content-Length: $filesize");
header("Content-Disposition: attachment; filename=\"$fileName\"");
header('Accept-Ranges: bytes');

$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
// don't forget to send the data too
ini_set('memory_limit', '-1');
while ($length >= $bufferSize)
{
    print(fread($file, $bufferSize));
    $length -= $bufferSize;
}
if ($length) print(fread($file, $length));
fclose($file);
Mój Boże
źródło
Dlaczego jest to potrzebne ini_set('memory_limit', '-1');?
Mikko Rantalainen
1
@MikkoRantalainen Zapomniałem. Możesz spróbować go usunąć i zobaczyć, co się stanie.
Mygod
1
Niestety w przypisaniu $ end zostanie wyświetlony błąd w przypadku, gdy $ pasuje [2] nie jest ustawione (np. Z żądaniem „Range = 0-”). Zamiast tego użyłem tego:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet
3

Tak, możesz do tego użyć nagłówka Range. Aby uzyskać pełne pobranie, musisz przekazać klientowi 3 dodatkowe nagłówki:

header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");

Niż w przypadku przerwanego pobierania należy sprawdzić nagłówek żądania zakresu przez:

$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');

I w tym przypadku nie zapomnij podać treści z kodem stanu 206:

header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");

Otrzymasz zmienne $ start i $ to z nagłówka żądania i użyjesz fseek (), aby znaleźć właściwą pozycję w pliku.

Zsolt Szeberenyi
źródło
2
@ceejayoz: getallheaders () to funkcja php, którą otrzymujesz, jeśli używasz apache uk2.php.net/getallheaders
Tom Haigh
2

Klasa z włączoną funkcją Small Composer, która działa tak samo, jak http_send_file pecl. Oznacza to obsługę wznawiania pobierania i ograniczania przepustowości. https://github.com/diversen/http-send-file

dennis
źródło
1

Wznowienie pobierania w HTTP odbywa się za pośrednictwem Rangenagłówka. Jeżeli wniosek zawiera Rangenagłówek, a jeśli inne wskaźniki (np If-Match, If-Unmodified-Since) wskazują, że zawartość nie uległa zmianie, ponieważ pobieranie rozpoczęto, podać kod 206 odpowiedzi (zamiast 200), wskazać zakres bajtów wracasz wContent-Range nagłówku, a następnie podaj ten zakres w treści odpowiedzi.

Nie wiem jednak, jak to zrobić w PHP.

Mike Dimmick
źródło
1

Dzięki Theo! twoja metoda nie działała bezpośrednio w przypadku strumieniowego przesyłania danych divx, ponieważ odkryłem, że odtwarzacz divx wysyła zakresy takie jak bajty = 9932800-

ale pokazał mi, jak to zrobić, dzięki: D

if(isset($_SERVER['HTTP_RANGE']))
{
    file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
Barbatrux
źródło
0

Możesz użyć poniższego kodu do obsługi żądań zakresu bajtów w dowolnej przeglądarce

    <?php
$file = 'YouTube360p.mp4';
$fileLoc = $file;
$filesize = filesize($file);
$offset = 0;
$fileLength = $filesize;
$length = $filesize - 1;

if ( isset($_SERVER['HTTP_RANGE']) ) {
    // if the HTTP_RANGE header is set we're dealing with partial content

    $partialContent = true;
    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);

    $offset = intval($matches[1]);
    $tempLength = intval($matches[2]) - 0;
    if($tempLength != 0)
    {
        $length = $tempLength;
    }
    $fileLength = ($length - $offset) + 1;
} else {
    $partialContent = false;
    $offset = $length;
}

$file = fopen($file, 'r');

// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);

$data = fread($file, $length);

fclose($file);

if ( $partialContent ) {
    // output the right headers for partial content
    header('HTTP/1.1 206 Partial Content');
}

// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($fileLoc));
header('Content-Length: ' . $fileLength);
header('Content-Disposition: inline; filename="' . $file . '"');
header('Accept-Ranges: bytes');
header('Content-Range: bytes ' . $offset . '-' . $length . '/' . $filesize);

// don't forget to send the data too
print($data);
?>
smerfa
źródło