Skip to content

Instantly share code, notes, and snippets.

@kosinix
Created September 22, 2014 08:46
  • Star 21 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save kosinix/4cf0d432638817888149 to your computer and use it in GitHub Desktop.
PHP - resumable download
<?php
class ResumeDownload {
private $file;
private $name;
private $boundary;
private $delay = 0;
private $size = 0;
function __construct($file, $delay = 0) {
if (! is_file($file)) {
header("HTTP/1.1 400 Invalid Request");
die("<h3>File Not Found</h3>");
}
$this->size = filesize($file);
$this->file = fopen($file, "r");
$this->boundary = md5($file);
$this->delay = $delay;
$this->name = basename($file);
}
public function process() {
$ranges = NULL;
$t = 0;
if ($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_SERVER['HTTP_RANGE']) && $range = stristr(trim($_SERVER['HTTP_RANGE']), 'bytes=')) {
$range = substr($range, 6);
$ranges = explode(',', $range);
$t = count($ranges);
}
header("Accept-Ranges: bytes");
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");
header(sprintf('Content-Disposition: attachment; filename="%s"', $this->name));
if ($t > 0) {
header("HTTP/1.1 206 Partial content");
$t === 1 ? $this->pushSingle($range) : $this->pushMulti($ranges);
} else {
header("Content-Length: " . $this->size);
$this->readFile();
}
flush();
}
private function pushSingle($range) {
$start = $end = 0;
$this->getRange($range, $start, $end);
header("Content-Length: " . ($end - $start + 1));
header(sprintf("Content-Range: bytes %d-%d/%d", $start, $end, $this->size));
fseek($this->file, $start);
$this->readBuffer($end - $start + 1);
$this->readFile();
}
private function pushMulti($ranges) {
$length = $start = $end = 0;
$output = "";
$tl = "Content-type: application/octet-stream\r\n";
$formatRange = "Content-range: bytes %d-%d/%d\r\n\r\n";
foreach ( $ranges as $range ) {
$this->getRange($range, $start, $end);
$length += strlen("\r\n--$this->boundary\r\n");
$length += strlen($tl);
$length += strlen(sprintf($formatRange, $start, $end, $this->size));
$length += $end - $start + 1;
}
$length += strlen("\r\n--$this->boundary--\r\n");
header("Content-Length: $length");
header("Content-Type: multipart/x-byteranges; boundary=$this->boundary");
foreach ( $ranges as $range ) {
$this->getRange($range, $start, $end);
echo "\r\n--$this->boundary\r\n";
echo $tl;
echo sprintf($formatRange, $start, $end, $this->size);
fseek($this->file, $start);
$this->readBuffer($end - $start + 1);
}
echo "\r\n--$this->boundary--\r\n";
}
private function getRange($range, &$start, &$end) {
list($start, $end) = explode('-', $range);
$fileSize = $this->size;
if ($start == '') {
$tmp = $end;
$end = $fileSize - 1;
$start = $fileSize - $tmp;
if ($start < 0)
$start = 0;
} else {
if ($end == '' || $end > $fileSize - 1)
$end = $fileSize - 1;
}
if ($start > $end) {
header("Status: 416 Requested range not satisfiable");
header("Content-Range: */" . $fileSize);
exit();
}
return array(
$start,
$end
);
}
private function readFile() {
while ( ! feof($this->file) ) {
echo fgets($this->file);
flush();
usleep($this->delay);
}
}
private function readBuffer($bytes, $size = 1024) {
$bytesLeft = $bytes;
while ( $bytesLeft > 0 && ! feof($this->file) ) {
$bytesLeft > $size ? $bytesRead = $size : $bytesRead = $bytesLeft;
$bytesLeft -= $bytesRead;
echo fread($this->file, $bytesRead);
flush();
usleep($this->delay);
}
}
}
$file = '/home/file.zip';
set_time_limit(0);
$download = new ResumeDownload($file, 50000); //delay about in microsecs
$download->process();
@shaunjstokes
Copy link

Great script, works perfectly and has saved me lots of time. Thanks!

@MyPC8MyBrain
Copy link

Thank you for sharing Nico,
finally a working resume script,
one question though... why is it so slow?
(normal download is 2-3mb sec; while this script remains connected and resumes it is painfully slow as 45kb sec compared to normal)

Chris

@hamedghari
Copy link

thank you very much, it is so great code. I use it ,works very well ^__^

@sameedalam12
Copy link

@kosinix thanks its working fine on download manager with resume and good speed but on browser download its speed very low 45kbps how to solve it

@kosinix
Copy link
Author

kosinix commented Nov 9, 2017

For everyone asking why download is slow, turn off the delay option:

$download = new ResumeDownload($file, 50000); //delay about in microsecs

to

$download = new ResumeDownload($file); // no delay

That delay was for demo purposes only when dealing with download managers.

@GaalexxC
Copy link

GaalexxC commented Nov 28, 2017

Throws 500 error with PHP 7.1, looking into the issue.

Opps its your alteration:
$download = new ResumeDownload($file,);
Remove the comma after $file
$download = new ResumeDownload($file);

Thanks for the Gist

@totop275
Copy link

totop275 commented Jan 4, 2018

Thanks for this, it works great. i modified some code and use it on my project. thanks

@sameedalam12
Copy link

i face one problem on it. when multi download at a time like 300+ download . then your server site and file processing slow like its take 30 second to 3 minutes.
i have good sever
16gb ram
8 core processor
ssd drive

I think this script every file open through ram then its problem occur . because I see our ram use as a cache full
please give a solution for it

@alecos71
Copy link

@sameedalam12: the script reads byte per byte but in fastest way. If you have a lot of files and your files are very big, it's normal a delay in response to download action. This is the fastest script that I seen. The only way to have no delay is serving normal links in your files to download instead a script that have to read each single byte per byte, the files are not supplied directly but virtualized, the script first read byte per byte and then send the output to the browser.

If you use the script like suggested by @kosinix the problem is very mitigated...

wrong:

$download = new ResumeDownload($file, 50000); //delay about in microsecs

correct:

$download = new ResumeDownload($file); // no delay

Have a nice day! Greetings.

@xcoders-hub
Copy link

Thanks, your code works perfectly if file is in our local server. But if i try to download from url it shows file not found. Anyway to solve this issue.

@alecos71
Copy link

@xcoders-hub, you have to provide full path because local server and remote server have different structure ($_SERVER["DOCUMENT_ROOT"]). For me works great both on local server and remote server.

@xcoders-hub
Copy link

It works on same server but if the url is from another server it says file not found.

@alecos71
Copy link

@xcoders-hub, the script in on server 1, the file is on server 2, you want provide the file on server 2 using the script that is on the server 1, the script is server side, cannot read remote file size, can only read local file size...

Therefore is dangerous to perform a download using remote connection because someone in the url could put something of evil, the script must determine the file size and cannot doing this on remote files. The range for resume download is determined by file size without cannot work as expected.

@xcoders-hub
Copy link

Thanks mate , I'm looking solution for Google drive resumable download. For large files drive download cancelled most of the times on slow internet connections.

@mreduar
Copy link

mreduar commented Mar 10, 2021

@xcoders-hub Did you find a solution?

@itank
Copy link

itank commented Feb 5, 2024

resumable download for safari 17 does not work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment