]> BookStack Code Mirror - bookstack/commitdiff
ZIP Exports: Added working image handling/inclusion
authorDan Brown <redacted>
Mon, 21 Oct 2024 12:59:15 +0000 (13:59 +0100)
committerDan Brown <redacted>
Mon, 21 Oct 2024 12:59:15 +0000 (13:59 +0100)
app/Exports/ZipExportBuilder.php
app/Exports/ZipExportFiles.php
app/Exports/ZipExportModels/ZipExportImage.php
app/Exports/ZipExportReferences.php
app/Uploads/ImageService.php
app/Uploads/ImageStorageDisk.php
dev/docs/portable-zip-file-format.md

index 720b4997d4a6b1b489fe2e93f216599b0234ead6..5c56e531b40a679720246ff70709d1bf0fe99be5 100644 (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'] = [
index d3ee70e93f2efa15d7e48933743079ecc518876e..27b6f937a385c3772ff51acaf30ac2da12531424 100644 (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);
+        }
     }
 }
index 540d3d4e55d542cbbf02db1890b01961156f34bd..39f1d10129821d61ededab6fd6beab394e29bf78 100644 (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;
+    }
 }
index 76a7fedbec4c31f1310f77354ab3940f3615585a..19672db0a0ebe1dff1058e68b9d1e45cadc6146a 100644 (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;
+    }
 }
index 8d8da61ec185b42691fde6bd1673abeb44a307ad..e501cc7b12ded686776c7ba4d80b3d25c9fc8389 100644 (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.
      *
index 798b72abdbf9d9e0384b66c2508dccf89cf39530..8df702e0d94183b23248a441935e6723d199c4d1 100644 (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.
index 7a99563d14b71bf9e68c6c470137fc743db8191c..1ba5872018c92175de757f764b9e29156cde7cc1 100644 (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
 
Morty Proxy This is a proxified and sanitized view of the page, visit original site.