mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-13 00:41:59 +03:00
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:
@ -10,13 +10,14 @@ use BookStack\Uploads\AttachmentService;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\MessageBag;
|
use Illuminate\Support\MessageBag;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class AttachmentController extends Controller
|
class AttachmentController extends Controller
|
||||||
{
|
{
|
||||||
protected $attachmentService;
|
protected AttachmentService $attachmentService;
|
||||||
protected $pageRepo;
|
protected PageRepo $pageRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AttachmentController constructor.
|
* AttachmentController constructor.
|
||||||
@ -230,13 +231,13 @@ class AttachmentController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fileName = $attachment->getFileName();
|
$fileName = $attachment->getFileName();
|
||||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||||
|
|
||||||
if ($request->get('open') === 'true') {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,6 +12,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
abstract class Controller extends BaseController
|
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
|
* 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.
|
* 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.
|
* Show a positive, successful notification to the user on next view load.
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|||||||
|
|
||||||
class AttachmentService
|
class AttachmentService
|
||||||
{
|
{
|
||||||
protected $fileSystem;
|
protected FilesystemManager $fileSystem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AttachmentService constructor.
|
* AttachmentService constructor.
|
||||||
@ -73,6 +73,18 @@ class AttachmentService
|
|||||||
return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
|
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.
|
* Store a new attachment upon user upload.
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user