ดาวน์โหลดต่อเมื่อใช้ PHP เพื่อส่งไฟล์?


105

เราใช้สคริปต์ PHP สำหรับการดาวน์โหลดไฟล์ทันเนลเนื่องจากเราไม่ต้องการเปิดเผยเส้นทางที่แน่นอนของไฟล์ที่ดาวน์โหลดได้:

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

น่าเสียดายที่เราสังเกตเห็นว่าการดาวน์โหลดที่ส่งผ่านสคริปต์นี้ไม่สามารถดำเนินการต่อโดยผู้ใช้ปลายทางได้

มีวิธีใดบ้างที่จะรองรับการดาวน์โหลดต่อด้วยโซลูชันที่ใช้ PHP ดังกล่าว

คำตอบ:


103

สิ่งแรกที่คุณต้องทำคือส่งAccept-Ranges: bytesส่วนหัวในการตอบกลับทั้งหมดเพื่อบอกลูกค้าว่าคุณสนับสนุนเนื้อหาบางส่วน แล้วถ้าขอมีRange: bytes=x-y ส่วนหัวที่ได้รับ (ที่มีxและyเป็นตัวเลข) คุณแยกช่วงลูกค้าจะขอให้เปิดไฟล์ตามปกติแสวงหาxไบต์ข้างหน้าและส่งต่อไปy- xไบต์ HTTP/1.0 206 Partial Contentนอกจากนี้ยังมีการตั้งค่าการตอบสนองต่อ

โดยไม่ต้องทดสอบอะไรเลยก็สามารถใช้ได้ไม่มากก็น้อย:

$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);

ฉันอาจพลาดบางอย่างที่ชัดเจนและฉันได้เพิกเฉยต่อแหล่งที่มาของข้อผิดพลาดที่อาจเกิดขึ้น แต่ควรเป็นการเริ่มต้น

มีเป็นรายละเอียดของเนื้อหาบางส่วนที่นี่และฉันพบข้อมูลบางอย่างเกี่ยวกับเนื้อหาบางส่วนในหน้าเอกสารสำหรับfread


3
ข้อผิดพลาดเล็กน้อยนิพจน์ทั่วไปของคุณควรเป็น: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ ตรงกัน)
deepwell

1
คุณพูดถูกและฉันเปลี่ยนมันแล้ว อย่างไรก็ตามฉันก็เรียบง่ายเกินไปตามข้อกำหนดที่คุณสามารถทำได้ "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab" ฯลฯ ดังนั้นข้อบกพร่องใน เวอร์ชันก่อนหน้าคือเครื่องหมายทับที่ขาดหายไปไม่ใช่เครื่องหมายคำถาม
ธีโอ

8
มีประโยชน์มาก แต่ฉันต้องทำการปรับแต่งเล็กน้อยสองครั้งเพื่อให้มันใช้งานได้: 1. ถ้าไคลเอนต์ไม่ส่งปลายทางในช่วง (เนื่องจากเป็นนัย) $lengthจะเป็นค่าลบ $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;แก้ไขสิ่งนั้น 2. Content-Rangeถือว่าไบต์แรกเป็นไบต์เพื่อไบต์สุดท้ายคือ0 ดังนั้นจึงจะต้องมีการ$filesize - 1 ($offset + $length - 1)
Dennis

1
ด้านบนใช้ไม่ได้กับการดาวน์โหลดขนาดใหญ่คุณจะได้รับ "PHP Fatal error: ขนาดหน่วยความจำที่อนุญาต XXXX ไบต์หมด (พยายามจัดสรร XXX ไบต์) ใน" ในกรณีของฉัน 100MB ใหญ่เกินไป โดยพื้นฐานแล้วคุณจะบันทึกไฟล์ทั้งหมดในตัวแปรและคายออก
sarah.ferguson

1
คุณสามารถแก้ปัญหาไฟล์ขนาดใหญ่ได้โดยการอ่านเป็นกลุ่มแทนที่จะอ่านทั้งหมดในครั้งเดียว
dynamichael

72

แก้ไข 2017/01 - ฉันเขียนไลบรารีเพื่อทำสิ่งนี้ใน PHP> = 7.0 https://github.com/DaveRandom/Resume

แก้ไข 2016/02 - โค้ดถูกเขียนใหม่ทั้งหมดไปยังชุดเครื่องมือโมดูลาร์ซึ่งเป็นการใช้งานตัวอย่างแทนที่จะเป็นฟังก์ชันเสาหิน การแก้ไขที่กล่าวถึงในความคิดเห็นด้านล่างได้รวมเอาไว้


โซลูชันการทำงานที่ผ่านการทดสอบแล้ว (อ้างอิงจากคำตอบของธีโอด้านบน) ซึ่งเกี่ยวข้องกับการดาวน์โหลดต่อได้ในชุดเครื่องมือแบบสแตนด์อโลนสองสามชุด รหัสนี้ต้องใช้ PHP 5.4 หรือใหม่กว่า

โซลูชันนี้ยังคงสามารถรับมือกับหนึ่งช่วงต่อคำขอเท่านั้น แต่ไม่ว่าในกรณีใด ๆ กับเบราว์เซอร์มาตรฐานที่ฉันคิดได้สิ่งนี้ไม่ควรทำให้เกิดปัญหา

<?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);
    }
}

ตัวอย่างการใช้งาน:

<?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;

รหัสสวย ๆ ที่นี่ ฉันพบข้อผิดพลาดในบรรทัดที่กำหนด $ length ควรเป็น: $ length = $ end - $ start + 1;
bobwienholt

ฉันจะหยุดการดาวน์โหลดชั่วคราวได้อย่างไร
Prasanth Bendra

4
ควรกำหนดความยาวเนื้อหาเป็นขนาดไฟล์จริงหรือเพียงจำนวนไบต์บางส่วนที่ส่ง หน้านี้ทำให้ดูเหมือนว่าควรเป็นไบต์บางส่วน แต่นั่นไม่ใช่สิ่งที่ทำในโค้ดตัวอย่างด้านบน w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus

3
พิมพ์ผิดอีกเล็กน้อย: $start = $end - intval($range[0]);ควรจะเป็นrange[1]
BurninLeo

1
@ sarah.ferguson Code ถูกเขียนใหม่และอัปเดตทั้งหมดดูด้านบน
DaveRandom

15

ใช่. สนับสนุน byteranges ดูRFC 2616 ส่วน 14.35

โดยทั่วไปหมายความว่าคุณควรอ่านRangeส่วนหัวและเริ่มให้บริการไฟล์จากออฟเซ็ตที่ระบุ

ซึ่งหมายความว่าคุณไม่สามารถใช้ readfile () ได้เนื่องจากให้บริการทั้งไฟล์ ให้ใช้fopen ()ก่อนจากนั้นจึงfseek ()ไปยังตำแหน่งที่ถูกต้องจากนั้นใช้fpassthru ()เพื่อให้บริการไฟล์


4
fpassthru ไม่ใช่ความคิดที่ดีหากไฟล์มีขนาดหลายเมกะไบต์หน่วยความจำอาจหมด เพียงแค่ fread () และพิมพ์ () เป็นชิ้น ๆ
Willem

3
fpassthru ใช้งานได้ดีกับพื้นที่หลายร้อยเมกะไบต์ echo file_get_contents(...)ไม่ทำงาน (OOM) ผมจึงไม่คิดว่าเป็นประเด็น PHP 5.3
Janus Troelsen

1
@JanusTroelsen ไม่มันไม่ใช่ ทุกอย่างขึ้นอยู่กับการกำหนดค่าเซิร์ฟเวอร์ของคุณ หากคุณมีเซิร์ฟเวอร์ที่แข็งแกร่งและมีหน่วยความจำจำนวนมากที่จัดสรรให้กับ PHP นั่นอาจจะใช้ได้ดีสำหรับคุณ ในการกำหนดค่า "อ่อนแอ" (ตัวอักษร: โฮสต์ที่ใช้ร่วมกัน) โดยใช้fpassthruจะล้มเหลวในไฟล์ 50 MB คุณไม่ควรใช้มันอย่างแน่นอนหากคุณให้บริการไฟล์ขนาดใหญ่ในการกำหนดค่าเซิร์ฟเวอร์ที่อ่อนแอ ดังที่ @Wimmer ชี้ให้เห็นอย่างถูกต้องfread+ printคือทั้งหมดที่คุณต้องการในกรณีนี้
trejder

2
@trejder: ดูหมายเหตุเกี่ยวกับ readfile () : readfile () จะไม่แสดงปัญหาเกี่ยวกับหน่วยความจำแม้ว่าจะส่งไฟล์ขนาดใหญ่ด้วยตัวเองก็ตาม หากคุณพบข้อผิดพลาดหน่วยความจำไม่เพียงพอตรวจสอบให้แน่ใจว่าการบัฟเฟอร์เอาต์พุตปิดอยู่ด้วย ob_get_level ()
Janus Troelsen

1
@trejder ปัญหาคือคุณไม่ได้กำหนดค่าบัฟเฟอร์เอาต์พุตของคุณถูกต้อง มันทำการแยกชิ้นส่วนโดยอัตโนมัติหากคุณบอกให้php.net/manual/en/…เช่น output_buffering = 4096 (และถ้ากรอบงานของคุณไม่อนุญาตสิ่งนี้กรอบงานของคุณจะแย่)
ZJR

15

วิธีนี้ใช้งานได้ 100% ตรวจสอบขั้นสูงว่าฉันกำลังใช้งานอยู่และไม่มีปัญหาอีกต่อไป

        /* 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);

1
ฉันเพิ่มคะแนนเนื่องจากขีด จำกัด ความเร็วมีประโยชน์มากอย่างไรก็ตามการตรวจสอบ MD5 ในไฟล์ที่กลับมาทำงาน (Firefox) พบว่าไม่ตรงกัน str_replace สำหรับ $ range ไม่ถูกต้องควรเป็นระเบิดอื่นผลลัพธ์ที่เป็นตัวเลขและขีดเพิ่มในส่วนหัว Content-Range
WhoIsRich

จะปรับแต่งอย่างไรเพื่อรองรับการดาวน์โหลดไฟล์ระยะไกล
Siyamak Shahpasand

2
คุณหมายถึงการอ้างสองครั้ง 'Content-Type: $ contentType';
แมตต์

set_time_limit (0); ไม่เหมาะสมจริงๆในความคิดของฉัน อาจจะมีการ จำกัด 24 ชั่วโมงที่สมเหตุสมผลกว่านี้?
twicejr

ขอบคุณสำหรับการตรวจสอบการพิมพ์ผิดของฉัน :)!
user1524615

11

วิธีที่ดีจริงๆในการแก้ปัญหานี้โดยไม่ต้อง "ม้วนโค้ด" ของคุณเองคือการใช้โมดูล mod_xsendfile Apache จากนั้นใน PHP คุณเพียงแค่ตั้งค่าส่วนหัวที่เหมาะสม Apache จะทำสิ่งนั้น

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");

2
จะเกิดอะไรขึ้นหากคุณต้องการยกเลิกการลิงก์ไฟล์หลังจากส่ง
Janus Troelsen

1
หากคุณต้องการยกเลิกการลิงก์ไฟล์หลังจากส่งคุณต้องมีแฟล็กพิเศษเพื่อระบุว่าโปรดดูที่XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta )
Jens

9

หากคุณยินดีที่จะติดตั้งโมดูล PECL ใหม่วิธีที่ง่ายที่สุดในการรองรับการดาวน์โหลดต่อด้วย PHPคือวิธีhttp_send_file()นี้

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

แหล่งที่มา: http://www.php.net/manual/en/function.http-send-file.php

เราใช้เพื่อให้บริการเนื้อหาที่จัดเก็บฐานข้อมูลและทำงานได้อย่างมีเสน่ห์!


3
ใช้งานได้เหมือนมีเสน่ห์ อย่างไรก็ตามโปรดระวังว่าคุณไม่ได้เปิดการบัฟเฟอร์เอาต์พุต (ob_start ฯลฯ ) ไว้ โดยเฉพาะอย่างยิ่งเมื่อส่งไฟล์ขนาดใหญ่สิ่งนี้จะบัฟเฟอร์ช่วงที่ร้องขอทั้งหมด
Pieter van Ginkel

สิ่งนี้ถูกเพิ่มใน PHP เมื่อใด อยู่ที่นั่นเสมอ?
ธ ม

1
นั่นคือ Pecl ไม่ใช่ PHP ฉันไม่มีฟังก์ชั่นนี้
Geo

4

คำตอบด้านบนมีจุดบกพร่องต่างๆ

  1. จุดบกพร่องที่สำคัญ: ไม่สามารถจัดการส่วนหัวของช่วงได้อย่างถูกต้อง bytes a-bควรหมายถึง[a, b]แทนที่จะเป็น[a, b)และbytes a-เป็นไม่ได้รับการจัดการ
  2. ข้อผิดพลาดเล็กน้อย: ไม่ใช้บัฟเฟอร์ในการจัดการเอาต์พุต ซึ่งอาจใช้หน่วยความจำมากเกินไปและทำให้ไฟล์ขนาดใหญ่มีความเร็วต่ำ

นี่คือรหัสที่แก้ไขของฉัน:

// 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);

ทำไมถึงต้องการini_set('memory_limit', '-1');?
Mikko Rantalainen

1
@MikkoRantalainen ฉันลืมไปแล้ว คุณสามารถลองลบออกและดูว่าเกิดอะไรขึ้น
Mygod

1
ขออภัยคุณจะเกิดข้อผิดพลาดในการกำหนด $ end ในกรณีที่ไม่ได้ตั้งค่า $ match [2] (เช่นด้วยคำขอ "Range = 0-") ฉันใช้สิ่งนี้แทน:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet

3

ใช่คุณสามารถใช้ส่วนหัวของช่วงสำหรับสิ่งนั้นได้ คุณต้องให้ส่วนหัวเพิ่มเติมอีก 3 รายการแก่ลูกค้าเพื่อดาวน์โหลดแบบเต็ม:

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

กว่าการดาวน์โหลดที่ถูกขัดจังหวะคุณต้องตรวจสอบส่วนหัวของคำขอช่วงโดย:

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

และในกรณีนี้อย่าลืมแสดงเนื้อหาด้วยรหัสสถานะ 206:

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

คุณจะได้รับตัวแปร $ start และ $ to จากส่วนหัวของคำขอและใช้ fseek () เพื่อค้นหาตำแหน่งที่ถูกต้องในไฟล์


2
@ceejayoz: getallheaders () เป็นฟังก์ชัน php ที่คุณจะได้รับหากคุณใช้ apache uk2.php.net/getallheaders
Tom Haigh



1

การดาวน์โหลดต่อใน HTTP ทำได้ผ่านRangeส่วนหัว หากคำขอประกอบด้วยRangeส่วนหัวและหากตัวชี้วัดอื่น ๆ (เช่นIf-Match,If-Unmodified-Since ) ระบุว่าเนื้อหาไม่ได้เปลี่ยนแปลงตั้งแต่การดาวน์โหลดเริ่มต้นคุณจะให้รหัส 206 การตอบสนอง (มากกว่า 200) ระบุช่วงของไบต์คุณกลับ ในContent-Rangeส่วนหัวจากนั้นระบุช่วงนั้นในเนื้อหาการตอบสนอง

ฉันไม่รู้ว่าจะทำอย่างไรใน PHP แม้ว่า


1

ขอบคุณธีโอ! วิธีการของคุณใช้ไม่ได้กับการสตรีม divx โดยตรงเพราะฉันพบว่าเครื่องเล่น divx กำลังส่งช่วงเช่น bytes = 9932800-

แต่มันแสดงให้ฉันเห็นว่าต้องทำอย่างไรขอบคุณ: D

if(isset($_SERVER['HTTP_RANGE']))
{
    file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);

0

คุณสามารถใช้รหัสด้านล่างสำหรับการสนับสนุนคำขอช่วงไบต์ในเบราว์เซอร์ใดก็ได้

    <?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);
?>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.