Updated attachment download responses to stream from filesystem

This allows download of attachments that are larger than current memory
limits, since we're not loading the entire file into memory any more.

For inline file responses, we take a 1kb portion of the file to sniff
before to check mime before we proceed.
This commit is contained in:
Dan Brown 2022-04-02 18:07:43 +01:00
parent affae2e3c4
commit 82e8b1577e
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 56 additions and 6 deletions

View File

@ -10,13 +10,14 @@ use BookStack\Uploads\AttachmentService;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $pageRepo;
protected AttachmentService $attachmentService;
protected PageRepo $pageRepo;
/**
* AttachmentController constructor.
@ -230,13 +231,13 @@ class AttachmentController extends Controller
}
$fileName = $attachment->getFileName();
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
return $this->streamedInlineDownloadResponse($attachmentStream, $fileName);
}
return $this->downloadResponse($attachmentContents, $fileName);
return $this->streamedDownloadResponse($attachmentStream, $fileName);
}
/**

View File

@ -12,6 +12,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
use Symfony\Component\HttpFoundation\StreamedResponse;
abstract class Controller extends BaseController
{
@ -120,6 +121,21 @@ abstract class Controller extends BaseController
]);
}
/**
* Create a response that forces a download, from a given stream of content.
*/
protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse
{
return response()->stream(function() use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
/**
* 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.
@ -135,6 +151,27 @@ abstract class Controller extends BaseController
]);
}
/**
* 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.
*/
protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse
{
$sniffContent = fread($stream, 1000);
$mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
return response()->stream(function() use ($sniffContent, $stream) {
echo $sniffContent;
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
/**
* Show a positive, successful notification to the user on next view load.
*/

View File

@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
{
protected $fileSystem;
protected FilesystemManager $fileSystem;
/**
* AttachmentService constructor.
@ -73,6 +73,18 @@ class AttachmentService
return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
}
/**
* Stream an attachment from storage.
*
* @return resource|null
* @throws FileNotFoundException
*/
public function streamAttachmentFromStorage(Attachment $attachment)
{
return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path));
}
/**
* Store a new attachment upon user upload.
*