Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Response: support range requests for files #5153

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Filesystem/F.php
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,39 @@ public static function size(string|array $file): int
}
}

/**
* Continously outputs the file between the provided range
*/
public static function stream(
string $file,
int $start = 0,
int $end = null
): void {
$size = static::size($file);
$end ??= $size;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be checks for start < 0, start >= end and end > size here as well, throwing an exception to provide extra safety and for those who use this method directly.

if ($end === $size) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this also check for start being 0? Otherwise the full file is returned even when the beginning wasn't requested.

echo static::read($file);
return;
}

$handle = fopen($file, 'rb');
fseek($handle, $start);

if (!feof($handle)) {
throw new Exception('Invalid file handle');
}

while ($start < $end) {
$chunk = fread($handle, min(8 * 1024, $end - $start));
$start += strlen($chunk);
echo $chunk;
flush();
}

fclose($handle);
}

/**
* Categorize the file
*
Expand Down
83 changes: 74 additions & 9 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Exception;
use Kirby\Cms\App;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\F;
use Throwable;
Expand All @@ -30,7 +31,7 @@ class Response
/**
* The response body
*/
protected string $body;
protected string|Closure $body;

/**
* The HTTP response code
Expand All @@ -51,7 +52,7 @@ class Response
* Creates a new response object
*/
public function __construct(
string|array $body = '',
string|Closure|array $body = '',
string|null $type = null,
int|null $code = null,
array|null $headers = null,
Expand Down Expand Up @@ -107,6 +108,10 @@ public function __toString(): string
*/
public function body(): string
{
if (is_callable($this->body) === true) {
return call_user_func($this->body);
}

return $this->body;
}

Expand Down Expand Up @@ -169,14 +174,27 @@ public static function download(
*
* @param array $props Custom overrides for response props (e.g. headers)
*/
public static function file(string $file, array $props = []): static
{
$props = array_merge([
'body' => F::read($file),
'type' => F::extensionToMime(F::extension($file))
], $props);
public static function file(
string $file,
array $props = [],
string|false|null $range = null
): static {
// if no range is specified, lazily check request for HTTP_RANGE header
$range ??= App::instance(null, true)?->request()->header('range');

return new static($props);
if (is_string($range) === true) {
if ($response = static::range($file, $range, $props)) {
return $response;
}
}

return new static(array_merge([
'body' => F::read($file),
'type' => F::extensionToMime(F::extension($file)),
'headers' => [
'Content-Length' => F::size($file),
]
], $props));
}


Expand Down Expand Up @@ -250,6 +268,53 @@ public static function json(
]);
}

/**
* Creates a bytes range response for a file
* based on the passed range string
*/
protected static function range(
string $file,
string $range,
array $props = []
): static|null {
preg_match("/^bytes=(\d*)-(\d*)/", $range, $matches);

if ($matches === false) {
return null;
}

$size = F::size($file);
$start = $matches[1];
$end = $matches[2];

if ($start === '') {
$start = $size - (int)$end;
$end = $size;
}

if ($end === '') {
$end = $size;
}

$start = (int)$start;
$end = (int)$end;

// range out of bounds: provide specific response
if ($start < 0 || $start >= $end || $end > $size) {
return new static(['code' => 416]);
}

return new static(array_merge([
'code' => 206,
'body' => fn () => F::stream($file, $start, $end),
'type' => F::extensionToMime(F::extension($file)),
'headers' => [
'Content-Length' => $size,
'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $size,
]
], $props));
}

/**
* Creates a redirect response,
* which will send the visitor to the
Expand Down
Loading