Added new FILE page mode that allows sending files to the browser with these improvements:

Reads the file and outputs it in chunks rather than all at once, reducing the amount of memory needed to very little, even for very very large files.
Supports http request ranges so that only parts of the file will be returned if requested. This allows in-browser video players to seek to arbitrary points in the video without needing to download the whole file.
Makes use of flush during send to allow the browser to being receiving file data immediately, allowing streamable video formats to begin playing before the server has finished sending the data. This could also be used in the future to add a transmission rate limiter.
Has early-disconnect detection, to terminate sending file data if the client browser has disconnected or aborted (for instance, a user starts a video, then seeks to near the middle, the first request of data will be terminated rather than continuing to process the file).
This commit is contained in:
Matthew Barbour 2019-06-25 13:50:52 -05:00 committed by Shish
parent ff28f34088
commit de6d6a0515
2 changed files with 103 additions and 20 deletions

View File

@ -31,6 +31,7 @@ abstract class PageMode
const REDIRECT = 'redirect';
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
}
/**
@ -75,9 +76,14 @@ class Page
/** @var string; public only for unit test */
public $data = "";
/** @var string; */
public $file = null;
/** @var string; public only for unit test */
public $filename = null;
private $disposition = null;
/**
* Set the raw data to be sent.
*/
@ -86,12 +92,18 @@ class Page
$this->data = $data;
}
public function set_file(string $file): void
{
$this->file = $file;
}
/**
* Set the recommended download filename.
*/
public function set_filename(string $filename): void
public function set_filename(string $filename, string $disposition = "attachment"): void
{
$this->filename = $filename;
$this->disposition = $disposition;
}
@ -294,10 +306,79 @@ class Page
case PageMode::DATA:
header("Content-Length: " . strlen($this->data));
if (!is_null($this->filename)) {
header('Content-Disposition: attachment; filename='.$this->filename);
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
print $this->data;
break;
case PageMode::FILE:
if (!is_null($this->filename)) {
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
//https://gist.github.com/codler/3906826
$size = filesize($this->file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
header("Content-Length: " . strlen($size));
header('Accept-Ranges: bytes');
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
break;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
} else {
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
break;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: " . $length);
$fp = fopen($this->file, 'r');
try {
fseek($fp, $start);
$buffer = 1024 * 64;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
// After flush, we can tell if the client browser has disconnected.
// This means we can start sending a large file, and if we detect they disappeared
// then we can just stop and not waste any more resources or bandwidth.
if (connection_status() != 0)
break;
}
} finally {
fclose($fp);
}
break;
case PageMode::REDIRECT:
header('Location: ' . $this->redirect);
print 'You should be redirected to <a href="' . $this->redirect . '">' . $this->redirect . '</a>';

View File

@ -255,7 +255,6 @@ class ImageIO extends Extension
global $page;
if (!is_null($image)) {
$page->set_mode(PageMode::DATA);
if ($type == "thumb") {
$ext = $config->get_string("thumb_type");
if (array_key_exists($ext, MIME_TYPE_MAP)) {
@ -278,14 +277,17 @@ class ImageIO extends Extension
$gmdate_mod = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT';
if ($if_modified_since == $gmdate_mod) {
$page->set_mode(PageMode::DATA);
$page->set_code(304);
$page->set_data("");
} else {
$page->set_mode(PageMode::FILE);
$page->add_http_header("Last-Modified: $gmdate_mod");
if ($type != "thumb") {
$page->add_http_header("Content-Disposition: inline; filename=".$image->get_nice_image_name());
$page->set_filename($image->get_nice_image_name(), 'inline');
}
$page->set_data(file_get_contents($file));
$page->set_file($file);
if ($config->get_int("image_expires")) {
$expires = date(DATE_RFC1123, time() + $config->get_int("image_expires"));