1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-30 04:23:11 +03:00

ZIP Exports: Added working image handling/inclusion

This commit is contained in:
Dan Brown
2024-10-21 13:59:15 +01:00
parent 06ffd8ee72
commit 4fb4fe0931
7 changed files with 148 additions and 18 deletions

View File

@ -35,7 +35,7 @@ class ZipExportBuilder
*/
protected function build(): string
{
$this->references->buildReferences();
$this->references->buildReferences($this->files);
$this->data['exported_at'] = date(DATE_ATOM);
$this->data['instance'] = [

View File

@ -4,6 +4,8 @@ namespace BookStack\Exports;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Support\Str;
class ZipExportFiles
@ -14,8 +16,15 @@ class ZipExportFiles
*/
protected array $attachmentRefsById = [];
/**
* References for images by image ID.
* @var array<int, string>
*/
protected array $imageRefsById = [];
public function __construct(
protected AttachmentService $attachmentService,
protected ImageService $imageService,
) {
}
@ -30,15 +39,46 @@ class ZipExportFiles
return $this->attachmentRefsById[$attachment->id];
}
$existingFiles = $this->getAllFileNames();
do {
$fileName = Str::random(20) . '.' . $attachment->extension;
} while (in_array($fileName, $this->attachmentRefsById));
} while (in_array($fileName, $existingFiles));
$this->attachmentRefsById[$attachment->id] = $fileName;
return $fileName;
}
/**
* Gain a reference to the given image instance.
* This is expected to be an image that the user has visibility of,
* no permission/access checks are performed here.
*/
public function referenceForImage(Image $image): string
{
if (isset($this->imageRefsById[$image->id])) {
return $this->imageRefsById[$image->id];
}
$existingFiles = $this->getAllFileNames();
$extension = pathinfo($image->path, PATHINFO_EXTENSION);
do {
$fileName = Str::random(20) . '.' . $extension;
} while (in_array($fileName, $existingFiles));
$this->imageRefsById[$image->id] = $fileName;
return $fileName;
}
protected function getAllFileNames(): array
{
return array_merge(
array_values($this->attachmentRefsById),
array_values($this->imageRefsById),
);
}
/**
* Extract each of the ZIP export tracked files.
* Calls the given callback for each tracked file, passing a temporary
@ -54,5 +94,14 @@ class ZipExportFiles
stream_copy_to_stream($stream, $tmpFileStream);
$callback($tmpFile, $ref);
}
foreach ($this->imageRefsById as $imageId => $ref) {
$image = Image::query()->find($imageId);
$stream = $this->imageService->getImageStream($image);
$tmpFile = tempnam(sys_get_temp_dir(), 'bszipimage-');
$tmpFileStream = fopen($tmpFile, 'w');
stream_copy_to_stream($stream, $tmpFileStream);
$callback($tmpFile, $ref);
}
}
}

View File

@ -2,10 +2,24 @@
namespace BookStack\Exports\ZipExportModels;
use BookStack\Activity\Models\Tag;
use BookStack\Exports\ZipExportFiles;
use BookStack\Uploads\Image;
class ZipExportImage extends ZipExportModel
{
public ?int $id = null;
public string $name;
public string $file;
public string $type;
public static function fromModel(Image $model, ZipExportFiles $files): self
{
$instance = new self();
$instance->id = $model->id;
$instance->name = $model->name;
$instance->type = $model->type;
$instance->file = $files->referenceForImage($model);
return $instance;
}
}

View File

@ -3,8 +3,13 @@
namespace BookStack\Exports;
use BookStack\App\Model;
use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExportModels\ZipExportAttachment;
use BookStack\Exports\ZipExportModels\ZipExportImage;
use BookStack\Exports\ZipExportModels\ZipExportModel;
use BookStack\Exports\ZipExportModels\ZipExportPage;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\Image;
class ZipExportReferences
{
@ -16,6 +21,9 @@ class ZipExportReferences
/** @var ZipExportAttachment[] */
protected array $attachments = [];
/** @var ZipExportImage[] */
protected array $images = [];
public function __construct(
protected ZipReferenceParser $parser,
) {
@ -34,19 +42,12 @@ class ZipExportReferences
}
}
public function buildReferences(): void
public function buildReferences(ZipExportFiles $files): void
{
// TODO - References to images, attachments, other entities
// TODO - Parse page MD & HTML
foreach ($this->pages as $page) {
$page->html = $this->parser->parse($page->html ?? '', function (Model $model): ?string {
// TODO - Handle found link to $model
// - Validate we can see/access $model, or/and that it's
// part of the export in progress.
// TODO - Add images after the above to files
return '[CAT]';
$page->html = $this->parser->parse($page->html ?? '', function (Model $model) use ($files, $page) {
return $this->handleModelReference($model, $page, $files);
});
// TODO - markdown
}
@ -55,4 +56,45 @@ class ZipExportReferences
// TODO - Parse chapter desc html
// TODO - Parse book desc html
}
protected function handleModelReference(Model $model, ZipExportModel $exportModel, ZipExportFiles $files): ?string
{
// TODO - References to other entities
// Handle attachment references
// No permission check needed here since they would only already exist in this
// reference context if already allowed via their entity access.
if ($model instanceof Attachment) {
if (isset($this->attachments[$model->id])) {
return "[[bsexport:attachment:{$model->id}]]";
}
return null;
}
// Handle image references
if ($model instanceof Image) {
// Only handle gallery and drawio images
if ($model->type !== 'gallery' && $model->type !== 'drawio') {
return null;
}
// We don't expect images to be part of book/chapter content
if (!($exportModel instanceof ZipExportPage)) {
return null;
}
$page = $model->getPage();
if ($page && userCan('view', $page)) {
if (!isset($this->images[$model->id])) {
$exportImage = ZipExportImage::fromModel($model, $files);
$this->images[$model->id] = $exportImage;
$exportModel->images[] = $exportImage;
}
return "[[bsexport:image:{$model->id}]]";
}
return null;
}
return null;
}
}

View File

@ -133,6 +133,19 @@ class ImageService
return $disk->get($image->path);
}
/**
* Get the raw data content from an image.
*
* @throws Exception
* @returns ?resource
*/
public function getImageStream(Image $image): mixed
{
$disk = $this->storage->getDisk();
return $disk->stream($image->path);
}
/**
* Destroy an image along with its revisions, thumbnails and remaining folders.
*

View File

@ -55,6 +55,15 @@ class ImageStorageDisk
return $this->filesystem->get($this->adjustPathForDisk($path));
}
/**
* Get a stream to the file at the given path.
* @returns ?resource
*/
public function stream(string $path): mixed
{
return $this->filesystem->readStream($this->adjustPathForDisk($path));
}
/**
* Save the given image data at the given path. Can choose to set
* the image as public which will update its visibility after saving.