X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/141eecb858cce126452baeb16905e25b6ceb13c6..refs/pull/5676/head:/app/Http/DownloadResponseFactory.php diff --git a/app/Http/DownloadResponseFactory.php b/app/Http/DownloadResponseFactory.php index 20032f525..8384484ad 100644 --- a/app/Http/DownloadResponseFactory.php +++ b/app/Http/DownloadResponseFactory.php @@ -2,18 +2,15 @@ namespace BookStack\Http; -use BookStack\Util\WebSafeMimeSniffer; use Illuminate\Http\Request; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\StreamedResponse; class DownloadResponseFactory { - protected Request $request; - - public function __construct(Request $request) - { - $this->request = $request; + public function __construct( + protected Request $request, + ) { } /** @@ -21,55 +18,95 @@ class DownloadResponseFactory */ public function directly(string $content, string $fileName): Response { - return response()->make($content, 200, $this->getHeaders($fileName)); + return response()->make($content, 200, $this->getHeaders($fileName, strlen($content))); } /** * Create a response that forces a download, from a given stream of content. */ - public function streamedDirectly($stream, string $fileName): StreamedResponse + public function streamedDirectly($stream, string $fileName, int $fileSize): StreamedResponse { - return response()->stream(function () use ($stream) { - - // End & flush the output buffer, if we're in one, otherwise we still use memory. - // Output buffer may or may not exist depending on PHP `output_buffering` setting. - // Ignore in testing since output buffers are used to gather a response. - if (!empty(ob_get_status()) && !app()->runningUnitTests()) { - ob_end_clean(); - } - - fpassthru($stream); - fclose($stream); - }, 200, $this->getHeaders($fileName)); + $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request); + $headers = array_merge($this->getHeaders($fileName, $fileSize), $rangeStream->getResponseHeaders()); + return response()->stream( + fn() => $rangeStream->outputAndClose(), + $rangeStream->getResponseStatus(), + $headers, + ); } + /** + * Create a response that downloads the given file via a stream. + * Has the option to delete the provided file once the stream is closed. + */ + public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse + { + $fileSize = filesize($filePath); + $stream = fopen($filePath, 'r'); + + if ($deleteAfter) { + // Delete the given file if it still exists after the app terminates + $callback = function () use ($filePath) { + if (file_exists($filePath)) { + unlink($filePath); + } + }; + + // We watch both app terminate and php shutdown to cover both normal app termination + // as well as other potential scenarios (connection termination). + app()->terminating($callback); + register_shutdown_function($callback); + } + + return $this->streamedDirectly($stream, $fileName, $fileSize); + } + + /** * Create a file download response that provides the file with a content-type * correct for the file, in a way so the browser can show the content in browser, * for a given content stream. */ - public function streamedInline($stream, string $fileName): StreamedResponse + public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse { - $sniffContent = fread($stream, 2000); - $mime = (new WebSafeMimeSniffer())->sniff($sniffContent); - - return response()->stream(function () use ($sniffContent, $stream) { - echo $sniffContent; - fpassthru($stream); - fclose($stream); - }, 200, $this->getHeaders($fileName, $mime)); + $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request); + $mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION)); + $headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders()); + + return response()->stream( + fn() => $rangeStream->outputAndClose(), + $rangeStream->getResponseStatus(), + $headers, + ); + } + + /** + * Create a response that provides the given file via a stream with detected content-type. + * Has the option to delete the provided file once the stream is closed. + */ + public function streamedFileInline(string $filePath, ?string $fileName = null): StreamedResponse + { + $fileSize = filesize($filePath); + $stream = fopen($filePath, 'r'); + + if ($fileName === null) { + $fileName = basename($filePath); + } + + return $this->streamedInline($stream, $fileName, $fileSize); } /** * Get the common headers to provide for a download response. */ - protected function getHeaders(string $fileName, string $mime = 'application/octet-stream'): array + protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array { $disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline'; $downloadName = str_replace('"', '', $fileName); return [ 'Content-Type' => $mime, + 'Content-Length' => $fileSize, 'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"", 'X-Content-Type-Options' => 'nosniff', ];