Download 40M zip file using PHP - php

I have a 40M zip file that is at a web address of say http://info/data/bigfile.zip that I would like to download to my local server. What is the best way currently to download a zip file of that size using PHP or header requests such that it won't time out at 8M or give me a 500 error? Right now, I keep getting timed out.

In order to download large file via php try something like this (source http://teddy.fr/2007/11/28/how-serve-big-files-through-php/):
<?php
define('CHUNK_SIZE', 1024*1024); // Size (in bytes) of tiles chunk
// Read a file and display its content chunk by chunk
function readfile_chunked($filename, $retbytes = TRUE) {
$buffer = '';
$cnt = 0;
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, CHUNK_SIZE);
echo $buffer;
ob_flush();
flush();
if ($retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if ($retbytes && $status) {
return $cnt; // return num. bytes delivered like readfile() does.
}
return $status;
}
// Here goes your code for checking that the user is logged in
// ...
// ...
$filename = 'path/to/your/file'; // url of your file
$mimetype = 'mime/type';
header('Content-Type: '.$mimetype );
readfile_chunked($filename);
?>
**Second solution **
Copy the file one small chunk at a time
/**
* Copy remote file over HTTP one small chunk at a time.
*
* #param $infile The full URL to the remote file
* #param $outfile The path where to save the file
*/
function copyfile_chunked($infile, $outfile) {
$chunksize = 10 * (1024 * 1024); // 10 Megs
/**
* parse_url breaks a part a URL into it's parts, i.e. host, path,
* query string, etc.
*/
$parts = parse_url($infile);
$i_handle = fsockopen($parts['host'], 80, $errstr, $errcode, 5);
$o_handle = fopen($outfile, 'wb');
if ($i_handle == false || $o_handle == false) {
return false;
}
if (!empty($parts['query'])) {
$parts['path'] .= '?' . $parts['query'];
}
/**
* Send the request to the server for the file
*/
$request = "GET {$parts['path']} HTTP/1.1\r\n";
$request .= "Host: {$parts['host']}\r\n";
$request .= "User-Agent: Mozilla/5.0\r\n";
$request .= "Keep-Alive: 115\r\n";
$request .= "Connection: keep-alive\r\n\r\n";
fwrite($i_handle, $request);
/**
* Now read the headers from the remote server. We'll need
* to get the content length.
*/
$headers = array();
while(!feof($i_handle)) {
$line = fgets($i_handle);
if ($line == "\r\n") break;
$headers[] = $line;
}
/**
* Look for the Content-Length header, and get the size
* of the remote file.
*/
$length = 0;
foreach($headers as $header) {
if (stripos($header, 'Content-Length:') === 0) {
$length = (int)str_replace('Content-Length: ', '', $header);
break;
}
}
/**
* Start reading in the remote file, and writing it to the
* local file one chunk at a time.
*/
$cnt = 0;
while(!feof($i_handle)) {
$buf = '';
$buf = fread($i_handle, $chunksize);
$bytes = fwrite($o_handle, $buf);
if ($bytes == false) {
return false;
}
$cnt += $bytes;
/**
* We're done reading when we've reached the conent length
*/
if ($cnt >= $length) break;
}
fclose($i_handle);
fclose($o_handle);
return $cnt;
}
Adjust the $chunksize variable to your needs. This has only been mildly tested. It could easily break for a number of reasons.
Usage:
copyfile_chunked('http://somesite.com/somefile.jpg', '/local/path/somefile.jpg');

Not a lot of detail given, but it sounds like php.ini defaults are restricting the servers ability to transfer large files via php web interface in a timely manner.
Namely these settings post_max_size, upload_max_filesize, or max_execution_time
Might jump in your php.ini file, up the sizes, restart Apache, and retry the file transfer.
HTH

Related

Cron Job timeout: PHP script to download long videos

I call the script mywebsite.com/download.php in crontab.
download.php
$video_url = "www.example.com/sample.mp4"; //hour long video
$link = '/home/mywebsite.com/videos/test.mp4';
$chunkSize = 1;
$chunksize = $chunkSize*(1024*1024); // How many bytes per chunk
$data = '';
$bytesCount = 0;
$handle = fopen($video_url, 'rb');
$fp = fopen($link, 'w');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$data = fread($handle, $chunksize);
fwrite($fp, $data, strlen($data));
}
$status = fclose($handle);
fclose($fp);
The problem is the script timeouts with long videos. I don't have the option of increasing server timeout limits.
What is an alternate method to guarantee the video get's downloaded?
ignore_user_abort(); ?
set_time_limit(0); ? Will this be ignored by server?
I have a queue system so only 1 video downloads at a time.
crontab call
*/15 * * * * /usr/bin/php /home/example.com/index.php media get_video

Laravel: Downloading external files using response()->stream() randomly leads to corrupted downloads

I have files stored on a CDN, which are available for download. The files can be up to 100 MB, so I'm using simple chunking method (below).
The download works, but some of the files (mostly PDF-s) end up being corrupted and cannot be opened.
Can anyone point out a flaw or a gotcha in the code?
One thing I can think of is that the headers are not sent in time somehow. Another one is that some of the files are not saved in UTF-8 and this might cause problems reading the file fully. I'm stuck.
Download logic (using stream()):
$url = 'http://example.com/files/example.pdf';
$headers = array(
'Pragma' => 'public',
'Expires' => '0',
'Content-Transfer-Encoding' => 'binary',
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="example.pdf"'
);
return response()->stream(function() use ($url) {
$this->readfileChunked($url);
},200, $headers);
The chunking:
/**
* Serves the given file in chunks.
* Source: http://cn2.php.net/manual/en/function.readfile.php#52598
*
* #param $filename
* #param bool $retbytes
*
* #return bool|int
*/
public function readfileChunked($filename, $retbytes = true) {
$chunkSize = 1024 * 1024; // Size (in bytes) of tiles chunk
$cnt = 0;
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunkSize);
echo $buffer;
ob_flush();
flush();
if ($retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if ($retbytes && $status) {
return $cnt; // return number of bytes delivered like readfile() does
}
return $status;
}
Have you tried using:
return response()->file($url, $headers);
And does this result in corrupted files like the stream method?

Download media File in PHP instead of open [duplicate]

We are using a PHP scripting for tunnelling file downloads, since we don't want to expose the absolute path of downloadable file:
header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);
Unfortunately we noticed that downloads passed through this script can't be resumed by the end user.
Is there any way to support resumable downloads with such a PHP-based solution?
The first thing you need to do is to send the Accept-Ranges: bytes header in all responses, to tell the client that you support partial content. Then, if request with a Range: bytes=x-y header is received (with x and y being numbers) you parse the range the client is requesting, open the file as usual, seek x bytes ahead and send the next y - x bytes. Also set the response to HTTP/1.0 206 Partial Content.
Without having tested anything, this could work, more or less:
$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);
I may have missed something obvious, and I have most definitely ignored some potential sources of errors, but it should be a start.
There's a description of partial content here and I found some info on partial content on the documentation page for fread.
EDIT 2017/01 - I wrote a library to do this in PHP >=7.0 https://github.com/DaveRandom/Resume
EDIT 2016/02 - Code completely rewritten to a set of modular tools an an example usage, rather than a monolithic function. Corrections mentioned in comments below have been incorporated.
A tested, working solution (based heavily on Theo's answer above) which deals with resumable downloads, in a set of a few standalone tools. This code requires PHP 5.4 or later.
This solution can still only cope with one range per request, but under any circumstance with a standard browser that I can think of, this should not cause a problem.
<?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);
}
}
Example usage:
<?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;
Yes. Support byteranges. See RFC 2616 section 14.35 .
It basically means that you should read the Range header, and start serving the file from the specified offset.
This means that you can't use readfile(), since that serves the whole file. Instead, use fopen() first, then fseek() to the correct position, and then use fpassthru() to serve the file.
This works 100% super check it
I am using it and no problems any more.
/* 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);
A really nice way to solve this without having to "roll your own" PHP code is to use the mod_xsendfile Apache module. Then in PHP, you just set the appropriate headers. Apache gets to do its thing.
header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
If you're willing to install a new PECL module, the easiest way to support resumeable downloads with PHP is through http_send_file(), like this
<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>
source : http://www.php.net/manual/en/function.http-send-file.php
We use it to serve database-stored content and it works like a charm !
Yes, you can use the Range header for that. You need to give 3 more headers to the client for a full download:
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");
Than for an interrupted download you need to check the Range request header by:
$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');
And in this case don't forget to serve the content with 206 status code:
header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");
You'll get the $start and $to variables from the request header, and use fseek() to seek to the correct position in the file.
The top answer has various bugs.
The major bug: It doesn't handle Range header correctly. bytes a-b should mean [a, b] instead of [a, b), and bytes a- is not handled.
The minor bug: It doesn't use buffer to handle output. This may consume too much memory and cause low speed for large files.
Here's my modified code:
// 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);
This worked very well for me: https://github.com/pomle/php-serveFilePartial
Small composer enabled class which works the same way as pecl http_send_file. This means support for resumable downloads and throttle. https://github.com/diversen/http-send-file
You could use the below code for byte range request support across any browser
<?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);
?>
Resuming downloads in HTTP is done through the Range header. If the request contains a Range header, and if other indicators (e.g. If-Match, If-Unmodified-Since) indicate that the content hasn't changed since the download was started, you give a 206 response code (rather than 200), indicate the range of bytes you're returning in the Content-Range header, then provide that range in the response body.
I don't know how to do that in PHP, though.
Thanks Theo! your method did not directly work for streaming divx because i found the divx player was sending ranges like bytes=9932800-
but it showed me how to do it so thanks :D
if(isset($_SERVER['HTTP_RANGE']))
{
file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
I've created a library for serving files with support for conditional (don't download file again unless it has changed) and ranged (pause and resume download) requests. It even works with virtual file systems, such as Flysystem.
Check it out here: FileWaiter
Example usage:
use Stadly\FileWaiter\Adapter\Local;
use Stadly\FileWaiter\File;
use Stadly\FileWaiter\Waiter;
$streamFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible stream factory.
$file = new File(new Local('filename.txt', $streamFactory)); // Or another file adapter. See below.
$responseFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible response factory.
$waiter = new Waiter($file, $responseFactory);
$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); // Any PSR-7 compatible server request.
$response = $waiter->handle($request); // The response is created by the response factory.
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter(); // Any way of emitting PSR-7 responses.
$emitter->emit($response);
die();

PHP Stop Reading Remote Files when after it is Fully Downloaded

I'm getting a 30 second timeout error because the code keeps checking if the file is over 5mb when it's under. The code is designed to reject files over 5mb but i need it to also stop executing when the file is under 5mb. Is there a way to check the file transfer chunk to see if it's empty? I'm currently using this example by DaveRandom:
PHP Stop Remote File Download if it Exceeds 5mb
Code by DaveRandom:
$url = 'http://www.spacetelescope.org/static/archives/images/large/heic0601a.jpg';
$file = '../temp/test.jpg';
$limit = 5 * 1024 * 1024; // 5MB
if (!$rfp = fopen($url, 'r')) {
// error, could not open remote file
}
if (!$lfp = fopen($file, 'w')) {
// error, could not open local file
}
// Check the content-length for exceeding the limit
foreach ($http_response_header as $header) {
if (preg_match('/^\s*content-length\s*:\s*(\d+)\s*$/', $header, $matches)) {
if ($matches[1] > $limit) {
// error, file too large
}
}
}
$downloaded = 0;
while ($downloaded < $limit) {
$chunk = fread($rfp, 8192);
fwrite($lfp, $chunk);
$downloaded += strlen($chunk);
}
if ($downloaded > $limit) {
// error, file too large
unlink($file); // delete local data
} else {
// success
}
You should check if you have reached the end of the file:
while (!feof($rfp) && $downloaded < $limit) {
$chunk = fread($rfp, 8192);
fwrite($lfp, $chunk);
$downloaded += strlen($chunk);
}

Resumable downloads when using PHP to send the file?

We are using a PHP scripting for tunnelling file downloads, since we don't want to expose the absolute path of downloadable file:
header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);
Unfortunately we noticed that downloads passed through this script can't be resumed by the end user.
Is there any way to support resumable downloads with such a PHP-based solution?
The first thing you need to do is to send the Accept-Ranges: bytes header in all responses, to tell the client that you support partial content. Then, if request with a Range: bytes=x-y header is received (with x and y being numbers) you parse the range the client is requesting, open the file as usual, seek x bytes ahead and send the next y - x bytes. Also set the response to HTTP/1.0 206 Partial Content.
Without having tested anything, this could work, more or less:
$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);
I may have missed something obvious, and I have most definitely ignored some potential sources of errors, but it should be a start.
There's a description of partial content here and I found some info on partial content on the documentation page for fread.
EDIT 2017/01 - I wrote a library to do this in PHP >=7.0 https://github.com/DaveRandom/Resume
EDIT 2016/02 - Code completely rewritten to a set of modular tools an an example usage, rather than a monolithic function. Corrections mentioned in comments below have been incorporated.
A tested, working solution (based heavily on Theo's answer above) which deals with resumable downloads, in a set of a few standalone tools. This code requires PHP 5.4 or later.
This solution can still only cope with one range per request, but under any circumstance with a standard browser that I can think of, this should not cause a problem.
<?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);
}
}
Example usage:
<?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;
Yes. Support byteranges. See RFC 2616 section 14.35 .
It basically means that you should read the Range header, and start serving the file from the specified offset.
This means that you can't use readfile(), since that serves the whole file. Instead, use fopen() first, then fseek() to the correct position, and then use fpassthru() to serve the file.
This works 100% super check it
I am using it and no problems any more.
/* 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);
A really nice way to solve this without having to "roll your own" PHP code is to use the mod_xsendfile Apache module. Then in PHP, you just set the appropriate headers. Apache gets to do its thing.
header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
If you're willing to install a new PECL module, the easiest way to support resumeable downloads with PHP is through http_send_file(), like this
<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>
source : http://www.php.net/manual/en/function.http-send-file.php
We use it to serve database-stored content and it works like a charm !
Yes, you can use the Range header for that. You need to give 3 more headers to the client for a full download:
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");
Than for an interrupted download you need to check the Range request header by:
$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');
And in this case don't forget to serve the content with 206 status code:
header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");
You'll get the $start and $to variables from the request header, and use fseek() to seek to the correct position in the file.
The top answer has various bugs.
The major bug: It doesn't handle Range header correctly. bytes a-b should mean [a, b] instead of [a, b), and bytes a- is not handled.
The minor bug: It doesn't use buffer to handle output. This may consume too much memory and cause low speed for large files.
Here's my modified code:
// 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);
This worked very well for me: https://github.com/pomle/php-serveFilePartial
Small composer enabled class which works the same way as pecl http_send_file. This means support for resumable downloads and throttle. https://github.com/diversen/http-send-file
You could use the below code for byte range request support across any browser
<?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);
?>
Resuming downloads in HTTP is done through the Range header. If the request contains a Range header, and if other indicators (e.g. If-Match, If-Unmodified-Since) indicate that the content hasn't changed since the download was started, you give a 206 response code (rather than 200), indicate the range of bytes you're returning in the Content-Range header, then provide that range in the response body.
I don't know how to do that in PHP, though.
Thanks Theo! your method did not directly work for streaming divx because i found the divx player was sending ranges like bytes=9932800-
but it showed me how to do it so thanks :D
if(isset($_SERVER['HTTP_RANGE']))
{
file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
I've created a library for serving files with support for conditional (don't download file again unless it has changed) and ranged (pause and resume download) requests. It even works with virtual file systems, such as Flysystem.
Check it out here: FileWaiter
Example usage:
use Stadly\FileWaiter\Adapter\Local;
use Stadly\FileWaiter\File;
use Stadly\FileWaiter\Waiter;
$streamFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible stream factory.
$file = new File(new Local('filename.txt', $streamFactory)); // Or another file adapter. See below.
$responseFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible response factory.
$waiter = new Waiter($file, $responseFactory);
$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); // Any PSR-7 compatible server request.
$response = $waiter->handle($request); // The response is created by the response factory.
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter(); // Any way of emitting PSR-7 responses.
$emitter->emit($response);
die();

Categories