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:
parent
ff28f34088
commit
de6d6a0515
2 changed files with 103 additions and 20 deletions
109
core/page.php
109
core/page.php
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -171,7 +183,7 @@ class Page
|
|||
/**
|
||||
* Add a line to the HTML head section.
|
||||
*/
|
||||
public function add_html_header(string $line, int $position=50): void
|
||||
public function add_html_header(string $line, int $position = 50): void
|
||||
{
|
||||
while (isset($this->html_headers[$position])) {
|
||||
$position++;
|
||||
|
@ -182,7 +194,7 @@ class Page
|
|||
/**
|
||||
* Add a http header to be sent to the client.
|
||||
*/
|
||||
public function add_http_header(string $line, int $position=50): void
|
||||
public function add_http_header(string $line, int $position = 50): void
|
||||
{
|
||||
while (isset($this->http_headers[$position])) {
|
||||
$position++;
|
||||
|
@ -197,13 +209,13 @@ class Page
|
|||
*/
|
||||
public function add_cookie(string $name, string $value, int $time, string $path): void
|
||||
{
|
||||
$full_name = COOKIE_PREFIX."_".$name;
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
$this->cookies[] = [$full_name, $value, $time, $path];
|
||||
}
|
||||
|
||||
public function get_cookie(string $name): ?string
|
||||
{
|
||||
$full_name = COOKIE_PREFIX."_".$name;
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
if (isset($_COOKIE[$full_name])) {
|
||||
return $_COOKIE[$full_name];
|
||||
} else {
|
||||
|
@ -252,8 +264,8 @@ class Page
|
|||
global $page, $user;
|
||||
|
||||
header("HTTP/1.0 {$this->code} Shimmie");
|
||||
header("Content-type: ".$this->type);
|
||||
header("X-Powered-By: SCore-".SCORE_VERSION);
|
||||
header("Content-type: " . $this->type);
|
||||
header("X-Powered-By: SCore-" . SCORE_VERSION);
|
||||
|
||||
if (!headers_sent()) {
|
||||
foreach ($this->http_headers as $head) {
|
||||
|
@ -292,15 +304,84 @@ class Page
|
|||
$layout->display_page($page);
|
||||
break;
|
||||
case PageMode::DATA:
|
||||
header("Content-Length: ".strlen($this->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>';
|
||||
header('Location: ' . $this->redirect);
|
||||
print 'You should be redirected to <a href="' . $this->redirect . '">' . $this->redirect . '</a>';
|
||||
break;
|
||||
default:
|
||||
print "Invalid page mode";
|
||||
|
@ -341,7 +422,7 @@ class Page
|
|||
/*** Generate CSS cache files ***/
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(
|
||||
zglob("ext/{".ENABLED_EXTS."}/style.css"),
|
||||
zglob("ext/{" . ENABLED_EXTS . "}/style.css"),
|
||||
zglob("themes/$theme_name/style.css")
|
||||
);
|
||||
foreach ($css_files as $css) {
|
||||
|
@ -354,7 +435,7 @@ class Page
|
|||
foreach ($css_files as $file) {
|
||||
$file_data = file_get_contents($file);
|
||||
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||
$replace = 'url("../../../'.dirname($file).'/$1")';
|
||||
$replace = 'url("../../../' . dirname($file) . '/$1")';
|
||||
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||
$css_data .= $file_data . "\n";
|
||||
}
|
||||
|
@ -372,7 +453,7 @@ class Page
|
|||
"vendor/bower-asset/js-cookie/src/js.cookie.js",
|
||||
"ext/handle_static/modernizr-3.3.1.custom.js",
|
||||
],
|
||||
zglob("ext/{".ENABLED_EXTS."}/script.js"),
|
||||
zglob("ext/{" . ENABLED_EXTS . "}/script.js"),
|
||||
zglob("themes/$theme_name/script.js")
|
||||
);
|
||||
foreach ($js_files as $js) {
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -263,7 +262,7 @@ class ImageIO extends Extension
|
|||
} else {
|
||||
$page->set_type("image/jpeg");
|
||||
}
|
||||
|
||||
|
||||
$file = $image->get_thumb_filename();
|
||||
} else {
|
||||
$page->set_type($image->get_mime_type());
|
||||
|
@ -278,26 +277,29 @@ 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"));
|
||||
} else {
|
||||
$expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning
|
||||
}
|
||||
$page->add_http_header('Expires: '.$expires);
|
||||
$page->add_http_header('Expires: ' . $expires);
|
||||
}
|
||||
} else {
|
||||
$page->set_title("Not Found");
|
||||
$page->set_heading("Not Found");
|
||||
$page->add_block(new Block("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0));
|
||||
$page->add_block(new Block("Navigation", "<a href='" . make_link() . "'>Index</a>", "left", 0));
|
||||
$page->add_block(new Block(
|
||||
"Image not in database",
|
||||
"The requested image was not found in the database"
|
||||
|
|
Reference in a new issue