ZIP Exports: Added working image handling/inclusion
Some checks failed
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.1) (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-php / build (8.1) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled

This commit is contained in:
Dan Brown 2024-10-21 13:59:15 +01:00
parent 06ffd8ee72
commit 4fb4fe0931
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
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.

View File

@ -46,13 +46,12 @@ This can be done using the following format:
[[bsexport:<object>:<reference>]]
```
Images and attachments are referenced via their file name within the `files/` directory.
Otherwise, other content types are referenced by `id`.
References are to the `id` for data objects.
Here's an example of each type of such reference that could be used:
```
[[bsexport:image:an-image-path.png]]
[[bsexport:attachment:an-image-path.png]]
[[bsexport:image:22]]
[[bsexport:attachment:55]]
[[bsexport:page:40]]
[[bsexport:chapter:2]]
[[bsexport:book:8]]
@ -121,10 +120,14 @@ The page editor type, and edit content will be determined by what content is pro
#### Image
- `id` - Number, optional, original ID for the page from exported system.
- `name` - String, required, name of image.
- `file` - String reference, required, reference to image file.
- `type` - String, required, must be 'gallery' or 'drawio'
File must be an image type accepted by BookStack (png, jpg, gif, webp)
File must be an image type accepted by BookStack (png, jpg, gif, webp).
Images of type 'drawio' are expected to be png with draw.io drawing data
embedded within it.
#### Attachment