]> BookStack Code Mirror - bookstack/blob - app/Http/DownloadResponseFactory.php
ZIP Exports: Improved temp file tracking & clean-up
[bookstack] / app / Http / DownloadResponseFactory.php
1 <?php
2
3 namespace BookStack\Http;
4
5 use Illuminate\Http\Request;
6 use Illuminate\Http\Response;
7 use Symfony\Component\HttpFoundation\StreamedResponse;
8
9 class DownloadResponseFactory
10 {
11     public function __construct(
12         protected Request $request,
13     ) {
14     }
15
16     /**
17      * Create a response that directly forces a download in the browser.
18      */
19     public function directly(string $content, string $fileName): Response
20     {
21         return response()->make($content, 200, $this->getHeaders($fileName, strlen($content)));
22     }
23
24     /**
25      * Create a response that forces a download, from a given stream of content.
26      */
27     public function streamedDirectly($stream, string $fileName, int $fileSize): StreamedResponse
28     {
29         $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
30         $headers = array_merge($this->getHeaders($fileName, $fileSize), $rangeStream->getResponseHeaders());
31         return response()->stream(
32             fn() => $rangeStream->outputAndClose(),
33             $rangeStream->getResponseStatus(),
34             $headers,
35         );
36     }
37
38     /**
39      * Create a response that downloads the given file via a stream.
40      * Has the option to delete the provided file once the stream is closed.
41      */
42     public function streamedFileDirectly(string $filePath, string $fileName, int $fileSize, bool $deleteAfter = false): StreamedResponse
43     {
44         $stream = fopen($filePath, 'r');
45
46         if ($deleteAfter) {
47             // Delete the given file if it still exists after the app terminates
48             $callback = function () use ($filePath) {
49                 if (file_exists($filePath)) {
50                     unlink($filePath);
51                 }
52             };
53
54             // We watch both app terminate and php shutdown to cover both normal app termination
55             // as well as other potential scenarios (connection termination).
56             app()->terminating($callback);
57             register_shutdown_function($callback);
58         }
59
60         return $this->streamedDirectly($stream, $fileName, $fileSize);
61     }
62
63
64     /**
65      * Create a file download response that provides the file with a content-type
66      * correct for the file, in a way so the browser can show the content in browser,
67      * for a given content stream.
68      */
69     public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
70     {
71         $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
72         $mime = $rangeStream->sniffMime();
73         $headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
74
75         return response()->stream(
76             fn() => $rangeStream->outputAndClose(),
77             $rangeStream->getResponseStatus(),
78             $headers,
79         );
80     }
81
82     /**
83      * Get the common headers to provide for a download response.
84      */
85     protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array
86     {
87         $disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
88         $downloadName = str_replace('"', '', $fileName);
89
90         return [
91             'Content-Type'           => $mime,
92             'Content-Length'         => $fileSize,
93             'Content-Disposition'    => "{$disposition}; filename=\"{$downloadName}\"",
94             'X-Content-Type-Options' => 'nosniff',
95         ];
96     }
97 }