*/
protected function build(): string
{
- $this->references->buildReferences();
+ $this->references->buildReferences($this->files);
$this->data['exported_at'] = date(DATE_ATOM);
$this->data['instance'] = [
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
+use BookStack\Uploads\Image;
+use BookStack\Uploads\ImageService;
use Illuminate\Support\Str;
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,
) {
}
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
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);
+ }
}
}
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;
+ }
}
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
{
/** @var ZipExportAttachment[] */
protected array $attachments = [];
+ /** @var ZipExportImage[] */
+ protected array $images = [];
+
public function __construct(
protected ZipReferenceParser $parser,
) {
}
}
- 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
}
// 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;
+ }
}
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.
*
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.
[[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]]
#### 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