Resumable downloads when using PHP to send the file?

Asked
Active3 hr before
Viewed126 times

8 Answers

using
90%

Meta Stack Overflow ,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.,Stack Overflow en español,Stack Overflow em Português

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);
load more v
88%

We are using a PHP scripting for tunnelling file downloads, since we don't want to expose the absolute path of downloadable file:,Than for an interrupted download you need to check the Range request header by:,It basically means that you should read the Range header, and start serving the file from the specified offset.,Yes, you can use the Range header for that. You need to give 3 more headers to the client for a full download:

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);
load more v
72%

Defines the range unit the server supports. ,An integer in the given unit indicating the end of the requested range.,The Content-Range response HTTP header indicates where in a full body message a partial message belongs.,In web application, we often need to provide file downloads. For small files, there is no problems since it needs a short time to download. For large files, it’s useful to allow downloads to be resumed. Doing so is more involved, but it’s really worth doing, especially if you serve large files or video/audio.

Syntax

Accept - Ranges: bytes
Accept - Ranges: none
load more v
65%

First of all, I notice the use of headers like Content-Description and Content-Transfer-Encoding. There is no such thing in HTTP. Don’t believe me? Have a look at RFC2616, they specifically state “HTTP, unlike MIME, does not use Content-Transfer-Encoding, and does use Transfer-Encoding and Content-Encoding“. You may add those headers if you want, but they do absolutely nothing. Sadly, this wrong example is present even in the PHP manual.,Now, the use of Cache-Control is wrong in this case, especially to both values set to zero, according to Microsoft, but it works in IE6 and IE7 and later ignores it so no harm done.,You can output the file using the method described above, skipping until the start of the range and delivering the length of the range.,(yet again, many scripts get this wrong by sending 400 errors or other codes). Do not try to guess or fix the range(s) as it may result in corrupted downloads, which are more dangerous than failed ones.

It’s very tempting to write something like

readfile($_GET['file']);
load more v
75%

Reach out to all the awesome people in our web development community by starting your own topic. We equally welcome both specific questions as well as open-ended discussions. , We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts learning and sharing knowledge. ,Hello there am working now on download script for some projects I have many option to do from limiting the speed or make the download resumable. everything works fine except for the resume download, I've tried many script and codes, none of them works :( here is my last code I used,everything works fine except for the resume download

and Implementation

download_file('uploads/somefile.zip', 'application/octet-stream', TRUE);
load more v
40%

post_max_size – The maximum allowed POST data size. ,Click here to download all the example code in a zip file – I have released it under the MIT License, so feel free to build on top of it if you want to. ,upload_max_filesize – The maximum allowed upload file size. ,First, here is the download link to the example code as promised.

upload_max_filesize = 150 M
post_max_size = 150 M
max_input_time = 300
max_execution_time = 300
php_value upload_max_filesize 150 M
php_value post_max_size 150 M
php_value max_input_time 300
php_value max_execution_time 300
<!-- (A) HTML UPLOAD FORM -->
<form method="post" enctype="multipart/form-data">
  <input type="file" name="up" required/>
  <input type="submit" value="Go"/>
</form>
 
<?php
// (B) HANDLE FILE UPLOAD
if (isset($_FILES["up"])) {
  $source = $_FILES["fileup"]["tmp_name"];
  $destination = $_FILES["fileup"]["name"];
  move_uploaded_file($source, $destination);
  echo "OK";
}
?>
<!-- (A) UPLOAD BUTTON & FILE LIST -->
<input type="button" id="pickfiles" value="Upload"/>
<div id="filelist"></div>
 
<!-- (B) LOAD PLUPLOAD FROM CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/plupload/3.1.3/plupload.full.min.js"></script>
<script>
// (C) INITIALIZE UPLOADER
window.addEventListener("load", () => {
  // (C1) GET HTML FILE LIST
  var filelist = document.getElementById("filelist");
 
  // (C2) INIT PLUPLOAD
  var uploader = new plupload.Uploader({
    runtimes: "html5",
    browse_button: "pickfiles",
    url: "2b-chunk.php",
    chunk_size: "10mb",
    filters: {
      max_file_size: "150mb",
      mime_types: [{title: "Image files", extensions: "jpg,gif,png"}]
    },
    init: {
      PostInit: () => { filelist.innerHTML = "<div>Ready</div>"; },
      FilesAdded: (up, files) => {
        plupload.each(files, (file) => {
          let row = document.createElement("div");
          row.id = file.id;
          row.innerHTML = `${file.name} (${plupload.formatSize(file.size)}) <strong></strong>`;
          filelist.appendChild(row);
        });
        uploader.start();
      },
      UploadProgress: (up, file) => {
        document.querySelector(`#${file.id} strong`).innerHTML = `${file.percent}%`;
      },
      Error: (up, err) => { console.error(err); }
    }
  });
  uploader.init();
});
</script>
< ? php
// (A) HELPER FUNCTION - SERVER RESPONSE
function verbose($ok = 1, $info = "") {
   if ($ok == 0) {
      http_response_code(400);
   }
   exit(json_encode(["ok" => $ok, "info" => $info]));
}

// (B) INVALID UPLOAD
if (empty($_FILES) || $_FILES["file"]["error"]) {
   verbose(0, "Failed to move uploaded file.");
}

// (C) UPLOAD DESTINATION - CHANGE FOLDER IF REQUIRED!
$filePath = __DIR__.DIRECTORY_SEPARATOR.
"uploads";
if (!file_exists($filePath)) {
   if (!mkdir($filePath, 0777, true)) {
      verbose(0, "Failed to create $filePath");
   }
}
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
$filePath = $filePath.DIRECTORY_SEPARATOR.$fileName;

// (D) DEAL WITH CHUNKS
$chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
$chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
$out = @fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
if ($out) {
   $in = @fopen($_FILES["file"]["tmp_name"], "rb");
   if ($in) {
      while ($buff = fread($in, 4096)) {
         fwrite($out, $buff);
      }
   } else {
      verbose(0, "Failed to open input stream");
   }
   @fclose($in);
   @fclose($out);
   @unlink($_FILES["file"]["tmp_name"]);
} else {
   verbose(0, "Failed to open output stream");
}

// (E) CHECK IF FILE HAS BEEN UPLOADED
if (!$chunks || $chunk == $chunks - 1) {
   rename("{$filePath}.part", $filePath);
}
verbose(1, "Upload OK");
<!-- (A) UPLOAD BUTTON & LIST -->
<input type="button" id="upbrowse" value="Browse"/>
<input type="button" id="upToggle" value="Pause OR Continue"/>
<div id="uplist"></div>
 
<!-- (B) LOAD FLOWJS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/flow.js/2.14.1/flow.min.js"></script>
<script>
// (C) INIT FLOWJS
window.addEventListener("load", () => {
  // (C1) NEW FLOW OBJECT
  var flow = new Flow({
    target: "3c-resumable.php",
    chunkSize: 1024*1024, // 1MB
    singleFile: true
  });
 
  if (flow.support) {
    // (C2) ASSIGN BROWSE BUTTON
    flow.assignBrowse(document.getElementById("upbrowse"));
    // OR DEFINE DROP ZONE
    // flow.assignDrop(document.getElementById("updrop"));
 
    // (C3) ON FILE ADDED
    flow.on("fileAdded", (file, evt) => {
      let fileslot = document.createElement("div");
      fileslot.id = file.uniqueIdentifier;
      fileslot.innerHTML = `${file.name} (${file.size}) - <strong>0%</strong>`;
      document.getElementById("uplist").appendChild(fileslot);
    });

    // (C4) ON FILE SUBMITTED (ADDED TO UPLOAD QUEUE)
    flow.on("filesSubmitted", (arr, evt) => { flow.upload(); });
 
    // (C5) ON UPLOAD PROGRESS
    flow.on("fileProgress", (file, chunk) => {
      let progress = (chunk.offset + 1) / file.chunks.length * 100;
      progress = progress.toFixed(2) + "%";
      let fileslot = document.getElementById(file.uniqueIdentifier);
      fileslot = fileslot.getElementsByTagName("strong")[0];
      fileslot.innerHTML = progress;
    });
 
    // (C6) ON UPLOAD SUCCESS
    flow.on("fileSuccess", (file, message, chunk) => {
      let fileslot = document.getElementById(file.uniqueIdentifier);
      fileslot = fileslot.getElementsByTagName("strong")[0];
      fileslot.innerHTML = "DONE";
    });
 
    // (C7) ON UPLOAD ERROR
    flow.on("fileError", (file, message) => {
      let fileslot = document.getElementById(file.uniqueIdentifier);
      fileslot = fileslot.getElementsByTagName("strong")[0];
      fileslot.innerHTML = "ERROR";
    });
 
    // (C8) PAUSE/CONTINUE UPLOAD
    document.getElementById("upToggle").onclick = () => {
      if (flow.isUploading()) { flow.pause(); }
      else { flow.resume(); }
    };
  }
});
</script>
< ? php
// (A) INIT PHP FLOW
require __DIR__.DIRECTORY_SEPARATOR.
"vendor".DIRECTORY_SEPARATOR.
"autoload.php";
$config = new\ Flow\ Config();
$config - > setTempDir(__DIR__.DIRECTORY_SEPARATOR.
   "temp");
$request = new\ Flow\ Request();

// (B) HANDLE UPLOAD
$uploadFolder = __DIR__.DIRECTORY_SEPARATOR.
"uploads".DIRECTORY_SEPARATOR;
$uploadFileName = uniqid().
"_".$request - > getFileName();
$uploadPath = $uploadFolder.$uploadFileName;
if (\Flow\ Basic::save($uploadPath, $config, $request)) {
   // File saved successfully
} else {
   // Not final chunk or invalid request. Continue to upload.
}

All the above examples now accept all kinds of files and extensions. If you want to restrict the file types, I will highly recommend doing a simple server-side check instead.

< ? php
$ext = strtoupper(pathinfo($FILE, PATHINFO_EXTENSION));

// LET'S SAY, WE ALLOW ONLY XLS, XLSX
if ($ext != "XLS" || $ext != "XLSX") {
   exit("INVALID FILE TYPE");
}
load more v
22%

1.2.$filesize = filesize($file);
3.4.$offset = 0;
5. $length = $filesize;
6.7.if(isset($_SERVER['HTTP_RANGE'])) {
      8. // if the HTTP_RANGE header is set we're dealing with partial content9.10.    $partialContent = true;11.12.    // find the requested range13.    // this might be too simplistic, apparently the client can request14.    // multiple ranges, which can become pretty complex, so ignore it for now15.    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);16.17.    $offset = intval($matches[1]);18.    $length = intval($matches[2]) - $offset;19.} else {20.    $partialContent = false;21.}22.23.$file = fopen($file, 'r');24.25.// seek to the requested offset, this is 0 if it's not a partial content request26.fseek($file, $offset);27.28.$data = fread($file, $length);29.30.fclose($file);31.32.if ( $partialContent ) {33.    // output the right headers for partial content34.35.    header('HTTP/1.1 206 Partial Content');36.37.    header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $filesize);38.}39.40.// output the regular HTTP headers41.header('Content-Type: ' . $ctype);42.header('Content-Length: ' . $filesize);43.header('Content-Disposition: attachment; filename="' . $fileName . '"');44.header('Accept-Ranges: bytes');45.46.// don't forget to send the data too47.print($data);48.
60%

When using fpassthru() on a binary file on Windows systems, you should make sure to open the file in binary mode by appending a b to the mode used in the call to fopen(). ,Example #1 Using fpassthru() with binary files,fpassthru — Output all remaining data on a file pointer, You are encouraged to use the b flag when dealing with binary files, even if your system does not require it, so that your scripts will be more portable.

load more v

Other "using-undefined" queries related to "Resumable downloads when using PHP to send the file?"