X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/b4d9029dc301f73f0e87026b8eff78cf2cda6164..refs/pull/5721/head:/app/Http/DownloadResponseFactory.php diff --git a/app/Http/DownloadResponseFactory.php b/app/Http/DownloadResponseFactory.php index f8c10165c..8384484ad 100644 --- a/app/Http/DownloadResponseFactory.php +++ b/app/Http/DownloadResponseFactory.php @@ -2,7 +2,6 @@ namespace BookStack\Http; -use BookStack\Util\WebSafeMimeSniffer; use Illuminate\Http\Request; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -10,7 +9,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class DownloadResponseFactory { public function __construct( - protected Request $request + protected Request $request, ) { } @@ -19,7 +18,7 @@ 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))); } /** @@ -27,12 +26,42 @@ class DownloadResponseFactory */ public function streamedDirectly($stream, string $fileName, int $fileSize): StreamedResponse { - $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request->headers); - return response()->stream(function () use ($rangeStream) { - $rangeStream->outputAndClose(); - }, 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, @@ -40,24 +69,44 @@ class DownloadResponseFactory */ public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse { - $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request->headers); - $mime = $rangeStream->sniffMime(); + $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 response()->stream(function () use ($rangeStream) { - $rangeStream->outputAndClose(); - }, 200, $this->getHeaders($fileName, $mime)); + 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', ];