From d13e4d2eefeed427c0377be04761a639e9fdb8fc Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Sat, 9 Nov 2024 14:01:24 +0000
Subject: [PATCH] ZIP imports: Started actual import logic

---
 app/Entities/Tools/Cloner.php                 |  17 +--
 .../ZipExports/Models/ZipExportAttachment.php |   6 +-
 .../ZipExports/Models/ZipExportTag.php        |   6 +-
 app/Exports/ZipExports/ZipExportReader.php    |   8 ++
 app/Exports/ZipExports/ZipImportRunner.php    | 107 ++++++++++++++++++
 dev/docs/portable-zip-file-format.md          |   4 +-
 6 files changed, 124 insertions(+), 24 deletions(-)

diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php
index 2030b050c..2be6083e3 100644
--- a/app/Entities/Tools/Cloner.php
+++ b/app/Entities/Tools/Cloner.php
@@ -18,17 +18,12 @@ use Illuminate\Http\UploadedFile;
 
 class Cloner
 {
-    protected PageRepo $pageRepo;
-    protected ChapterRepo $chapterRepo;
-    protected BookRepo $bookRepo;
-    protected ImageService $imageService;
-
-    public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
-    {
-        $this->pageRepo = $pageRepo;
-        $this->chapterRepo = $chapterRepo;
-        $this->bookRepo = $bookRepo;
-        $this->imageService = $imageService;
+    public function __construct(
+        protected PageRepo $pageRepo,
+        protected ChapterRepo $chapterRepo,
+        protected BookRepo $bookRepo,
+        protected ImageService $imageService,
+    ) {
     }
 
     /**
diff --git a/app/Exports/ZipExports/Models/ZipExportAttachment.php b/app/Exports/ZipExports/Models/ZipExportAttachment.php
index 1dbdc7333..c6615e1dc 100644
--- a/app/Exports/ZipExports/Models/ZipExportAttachment.php
+++ b/app/Exports/ZipExports/Models/ZipExportAttachment.php
@@ -10,13 +10,12 @@ class ZipExportAttachment extends ZipExportModel
 {
     public ?int $id = null;
     public string $name;
-    public ?int $order = null;
     public ?string $link = null;
     public ?string $file = null;
 
     public function metadataOnly(): void
     {
-        $this->order = $this->link = $this->file = null;
+        $this->link = $this->file = null;
     }
 
     public static function fromModel(Attachment $model, ZipExportFiles $files): self
@@ -24,7 +23,6 @@ class ZipExportAttachment extends ZipExportModel
         $instance = new self();
         $instance->id = $model->id;
         $instance->name = $model->name;
-        $instance->order = $model->order;
 
         if ($model->external) {
             $instance->link = $model->path;
@@ -47,7 +45,6 @@ class ZipExportAttachment extends ZipExportModel
         $rules = [
             'id'    => ['nullable', 'int'],
             'name'  => ['required', 'string', 'min:1'],
-            'order' => ['nullable', 'integer'],
             'link'  => ['required_without:file', 'nullable', 'string'],
             'file'  => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
         ];
@@ -61,7 +58,6 @@ class ZipExportAttachment extends ZipExportModel
 
         $model->id = $data['id'] ?? null;
         $model->name = $data['name'];
-        $model->order = isset($data['order']) ? intval($data['order']) : null;
         $model->link = $data['link'] ?? null;
         $model->file = $data['file'] ?? null;
 
diff --git a/app/Exports/ZipExports/Models/ZipExportTag.php b/app/Exports/ZipExports/Models/ZipExportTag.php
index b6c9e338a..6b4720fca 100644
--- a/app/Exports/ZipExports/Models/ZipExportTag.php
+++ b/app/Exports/ZipExports/Models/ZipExportTag.php
@@ -9,11 +9,10 @@ class ZipExportTag extends ZipExportModel
 {
     public string $name;
     public ?string $value = null;
-    public ?int $order = null;
 
     public function metadataOnly(): void
     {
-        $this->value = $this->order = null;
+        $this->value =  null;
     }
 
     public static function fromModel(Tag $model): self
@@ -21,7 +20,6 @@ class ZipExportTag extends ZipExportModel
         $instance = new self();
         $instance->name = $model->name;
         $instance->value = $model->value;
-        $instance->order = $model->order;
 
         return $instance;
     }
@@ -36,7 +34,6 @@ class ZipExportTag extends ZipExportModel
         $rules = [
             'name'  => ['required', 'string', 'min:1'],
             'value' => ['nullable', 'string'],
-            'order' => ['nullable', 'integer'],
         ];
 
         return $context->validateData($data, $rules);
@@ -48,7 +45,6 @@ class ZipExportTag extends ZipExportModel
 
         $model->name = $data['name'];
         $model->value = $data['value'] ?? null;
-        $model->order = isset($data['order']) ? intval($data['order']) : null;
 
         return $model;
     }
diff --git a/app/Exports/ZipExports/ZipExportReader.php b/app/Exports/ZipExports/ZipExportReader.php
index c3e47da04..ebc2fbbc9 100644
--- a/app/Exports/ZipExports/ZipExportReader.php
+++ b/app/Exports/ZipExports/ZipExportReader.php
@@ -73,6 +73,14 @@ class ZipExportReader
         return $this->zip->statName("files/{$fileName}") !== false;
     }
 
+    /**
+     * @return false|resource
+     */
+    public function streamFile(string $fileName)
+    {
+        return $this->zip->getStream("files/{$fileName}");
+    }
+
     /**
      * @throws ZipExportException
      */
diff --git a/app/Exports/ZipExports/ZipImportRunner.php b/app/Exports/ZipExports/ZipImportRunner.php
index 2f784ebea..2b897ff91 100644
--- a/app/Exports/ZipExports/ZipImportRunner.php
+++ b/app/Exports/ZipExports/ZipImportRunner.php
@@ -5,18 +5,33 @@ namespace BookStack\Exports\ZipExports;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\ZipExportException;
 use BookStack\Exceptions\ZipImportException;
 use BookStack\Exports\Import;
 use BookStack\Exports\ZipExports\Models\ZipExportBook;
 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
 use BookStack\Exports\ZipExports\Models\ZipExportPage;
+use BookStack\Exports\ZipExports\Models\ZipExportTag;
 use BookStack\Uploads\FileStorage;
+use BookStack\Uploads\ImageService;
+use Illuminate\Http\UploadedFile;
 
 class ZipImportRunner
 {
+    protected array $tempFilesToCleanup = []; // TODO
+    protected array $createdImages = []; // TODO
+    protected array $createdAttachments = []; // TODO
+
     public function __construct(
         protected FileStorage $storage,
+        protected PageRepo $pageRepo,
+        protected ChapterRepo $chapterRepo,
+        protected BookRepo $bookRepo,
+        protected ImageService $imageService,
     ) {
     }
 
@@ -51,6 +66,98 @@ class ZipImportRunner
         $this->ensurePermissionsPermitImport($exportModel);
 
         // TODO - Run import
+          // TODO - In transaction?
+            // TODO - Revert uploaded files if goes wrong
+    }
+
+    protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
+    {
+        $book = $this->bookRepo->create([
+            'name' => $exportBook->name,
+            'description_html' => $exportBook->description_html ?? '',
+            'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
+            'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
+        ]);
+
+        // TODO - Parse/format description_html references
+
+        if ($book->cover) {
+            $this->createdImages[] = $book->cover;
+        }
+
+        // TODO - Pages
+        foreach ($exportBook->chapters as $exportChapter) {
+            $this->importChapter($exportChapter, $book);
+        }
+        // TODO - Sort chapters/pages by order
+
+        return $book;
+    }
+
+    protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter
+    {
+        $chapter = $this->chapterRepo->create([
+            'name' => $exportChapter->name,
+            'description_html' => $exportChapter->description_html ?? '',
+            'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
+        ], $parent);
+
+        // TODO - Parse/format description_html references
+
+        $exportPages = $exportChapter->pages;
+        usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
+            return ($a->priority ?? 0) - ($b->priority ?? 0);
+        });
+
+        foreach ($exportPages as $exportPage) {
+            //
+        }
+        // TODO - Pages
+
+        return $chapter;
+    }
+
+    protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page
+    {
+        $page = $this->pageRepo->getNewDraftPage($parent);
+
+        // TODO - Import attachments
+        // TODO - Import images
+        // TODO - Parse/format HTML
+
+        $this->pageRepo->publishDraft($page, [
+            'name' => $exportPage->name,
+            'markdown' => $exportPage->markdown,
+            'html' => $exportPage->html,
+            'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
+        ]);
+
+        return $page;
+    }
+
+    protected function exportTagsToInputArray(array $exportTags): array
+    {
+        $tags = [];
+
+        /** @var ZipExportTag $tag */
+        foreach ($exportTags as $tag) {
+            $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];
+        }
+
+        return $tags;
+    }
+
+    protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
+    {
+        $tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');
+        $fileStream = $reader->streamFile($fileName);
+        $tempStream = fopen($tempPath, 'wb');
+        stream_copy_to_stream($fileStream, $tempStream);
+        fclose($tempStream);
+
+        $this->tempFilesToCleanup[] = $tempPath;
+
+        return new UploadedFile($tempPath, $fileName);
     }
 
     /**
diff --git a/dev/docs/portable-zip-file-format.md b/dev/docs/portable-zip-file-format.md
index 6cee7356d..7e5df3f01 100644
--- a/dev/docs/portable-zip-file-format.md
+++ b/dev/docs/portable-zip-file-format.md
@@ -135,12 +135,10 @@ embedded within it.
 - `name` - String, required, name of attachment.
 - `link` - String, semi-optional, URL of attachment.
 - `file` - String reference, semi-optional, reference to attachment file.
-- `order` - Number, optional, integer order of the attachments (shown low to high).
 
 Either `link` or `file` must be present, as that will determine the type of attachment. 
 
 #### Tag
 
 - `name` - String, required, name of the tag.
-- `value` - String, optional, value of the tag (can be empty).
-- `order` - Number, optional, integer order of the tags (shown low to high).
\ No newline at end of file
+- `value` - String, optional, value of the tag (can be empty).
\ No newline at end of file