mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 05:28:20 +01:00
Merge pull request #590 from getformwork/feature/proper-file-responses
Proper `FileResponse`
This commit is contained in:
commit
dff5387074
@ -138,9 +138,11 @@ final class App
|
||||
|
||||
DynamicFieldValue::$vars = $this->container->call(require $this->config()->get('system.fields.dynamic.vars.file'));
|
||||
|
||||
$request = $this->request();
|
||||
|
||||
$response = $this->router()->dispatch();
|
||||
|
||||
$response->send();
|
||||
$response->prepare($request)->send();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
@ -34,11 +34,11 @@ class ErrorHandlers
|
||||
Response::cleanOutputBuffers();
|
||||
|
||||
if ($this->request->isXmlHttpRequest()) {
|
||||
JsonResponse::error('Error', $responseStatus)->send();
|
||||
JsonResponse::error('Error', $responseStatus)->prepare($this->request)->send();
|
||||
} else {
|
||||
$view = $this->viewFactory->make('errors.error', ['status' => $responseStatus->code(), 'message' => $responseStatus->message(), 'throwable' => $throwable]);
|
||||
$response = new Response($view->render(), $responseStatus);
|
||||
$response->send();
|
||||
$response->prepare($this->request)->send();
|
||||
// Don't exit, otherwise the error will not be logged
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,32 @@ namespace Formwork\Http;
|
||||
|
||||
use Formwork\Http\Utils\Header;
|
||||
use Formwork\Utils\FileSystem;
|
||||
use RuntimeException;
|
||||
|
||||
class FileResponse extends Response
|
||||
{
|
||||
protected const CHUNK_SIZE = 512 * 1024;
|
||||
|
||||
protected int $fileSize;
|
||||
|
||||
protected int $offset = 0;
|
||||
|
||||
protected int $length;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct(string $path, ResponseStatus $responseStatus = ResponseStatus::OK, array $headers = [], bool $download = false)
|
||||
public function __construct(protected string $path, ResponseStatus $responseStatus = ResponseStatus::OK, array $headers = [], bool $download = false)
|
||||
{
|
||||
$this->fileSize = FileSystem::fileSize($path);
|
||||
|
||||
$headers += [
|
||||
'Content-Type' => FileSystem::mimeType($path),
|
||||
'Content-Disposition' => $download ? Header::make(['attachment', 'filename' => basename($path)]) : 'inline',
|
||||
'Content-Length' => (string) FileSystem::fileSize($path),
|
||||
'Content-Length' => (string) $this->fileSize,
|
||||
];
|
||||
parent::__construct(FileSystem::read($path), $responseStatus, $headers);
|
||||
|
||||
parent::__construct('', $responseStatus, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,6 +38,80 @@ class FileResponse extends Response
|
||||
public function send(): void
|
||||
{
|
||||
parent::cleanOutputBuffers();
|
||||
parent::send();
|
||||
|
||||
$this->sendHeaders();
|
||||
|
||||
$file = fopen($this->path, 'r');
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
if ($output === false) {
|
||||
throw new RuntimeException('Unable to open output stream');
|
||||
}
|
||||
|
||||
if ($file === false) {
|
||||
throw new RuntimeException('Unable to open file: ' . $this->path);
|
||||
}
|
||||
|
||||
ignore_user_abort(true);
|
||||
|
||||
if ($this->offset > 0) {
|
||||
fseek($file, $this->offset);
|
||||
}
|
||||
|
||||
$length = $this->length ?? $this->fileSize;
|
||||
|
||||
while ($length > 0 && !feof($file)) {
|
||||
$read = fread($file, self::CHUNK_SIZE);
|
||||
|
||||
if ($read === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$written = fwrite($output, $read);
|
||||
|
||||
if (connection_aborted() || $written === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$length -= $written;
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
public function prepare(Request $request): static
|
||||
{
|
||||
parent::prepare($request);
|
||||
|
||||
if (!isset($this->headers['Accept-Ranges']) && $request->method() === RequestMethod::GET) {
|
||||
$this->headers['Accept-Ranges'] = 'bytes';
|
||||
}
|
||||
|
||||
if ($request->method() === RequestMethod::GET && preg_match('/^bytes=(\d+)?-(\d+)?$/', $request->headers()->get('Range', ''), $matches, PREG_UNMATCHED_AS_NULL)) {
|
||||
[, $start, $end] = $matches;
|
||||
|
||||
if ($start === null) {
|
||||
$start = max(0, $this->fileSize - (int) $end);
|
||||
$end = $this->fileSize - 1;
|
||||
} elseif ($end === null || $end > $this->fileSize - 1) {
|
||||
$end = $this->fileSize - 1;
|
||||
}
|
||||
|
||||
$this->offset = (int) $start;
|
||||
|
||||
if ($start > $end) {
|
||||
$this->length = 0;
|
||||
$this->responseStatus = ResponseStatus::RangeNotSatisfiable;
|
||||
$this->headers['Content-Range'] = sprintf('bytes */%s', $this->fileSize);
|
||||
} else {
|
||||
$this->length = (int) ($end - $start + 1);
|
||||
$this->responseStatus = ResponseStatus::PartialContent;
|
||||
$this->headers['Content-Range'] = sprintf('bytes %s-%s/%s', $start, $end, $this->fileSize);
|
||||
$this->headers['Content-Length'] = sprintf('%s', $this->length);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,14 @@ class Response implements ResponseInterface
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare response according to the given HTTP request
|
||||
*/
|
||||
public function prepare(Request $request): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HTTP status
|
||||
*/
|
||||
|
@ -35,6 +35,11 @@ interface ResponseInterface extends ArraySerializable
|
||||
*/
|
||||
public function headers(): array;
|
||||
|
||||
/**
|
||||
* Prepare response according to the given HTTP request
|
||||
*/
|
||||
public function prepare(Request $request): static;
|
||||
|
||||
/**
|
||||
* Send HTTP status
|
||||
*/
|
||||
|
@ -224,6 +224,7 @@ abstract class AbstractController extends BaseAbstractController
|
||||
if (!$this->user()->permissions()->has($permission)) {
|
||||
$this->container->build(ErrorsController::class)
|
||||
->forbidden()
|
||||
->prepare($this->request)
|
||||
->send();
|
||||
exit;
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class PanelServiceLoader implements ResolutionAwareServiceLoaderInterface
|
||||
if ($service->isLoggedIn() && $this->config->get('system.errors.setHandlers')) {
|
||||
$errorsController = $this->container->build(ErrorsController::class);
|
||||
set_exception_handler(function (Throwable $throwable) use ($errorsController): never {
|
||||
$errorsController->internalServerError($throwable)->send();
|
||||
$errorsController->internalServerError($throwable)->prepare($this->request)->send();
|
||||
throw $throwable;
|
||||
});
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<?php endif ?>
|
||||
<?php if ($file->type() === 'video') : ?>
|
||||
<video class="file-thumbnail">
|
||||
<source src="<?= $file->uri() ?>" type="<?= $file->mimeType() ?>" />
|
||||
<source src="<?= $file->uri() ?>" type="<?= $file->mimeType() ?>" preload="metadata" />
|
||||
</video>
|
||||
<?php endif ?>
|
||||
<div class="file-icon"><?= $this->icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?></div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user