]> BookStack Code Mirror - bookstack/commitdiff
ZIP Import: Added model+migration, and reader class
authorDan Brown <redacted>
Sat, 2 Nov 2024 17:17:34 +0000 (17:17 +0000)
committerDan Brown <redacted>
Sat, 2 Nov 2024 17:17:34 +0000 (17:17 +0000)
app/Exports/Controllers/ImportController.php
app/Exports/Import.php [new file with mode: 0644]
app/Exports/ZipExports/ZipExportReader.php [new file with mode: 0644]
app/Exports/ZipExports/ZipExportValidator.php
app/Exports/ZipExports/ZipFileReferenceRule.php
app/Exports/ZipExports/ZipValidationHelper.php
database/factories/Exports/ImportFactory.php [new file with mode: 0644]
database/migrations/2024_11_02_160700_create_imports_table.php [new file with mode: 0644]

index 323ecef268f1700e2dd37f5d46949a03af5540d5..bbf0ff57d8ca0c52983683551b7780ade16b70cc 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace BookStack\Exports\Controllers;
 
+use BookStack\Exports\Import;
+use BookStack\Exports\ZipExports\ZipExportReader;
 use BookStack\Exports\ZipExports\ZipExportValidator;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
@@ -37,17 +39,23 @@ class ImportController extends Controller
             return redirect('/import');
         }
 
+        $zipEntityInfo = (new ZipExportReader($zipPath))->getEntityInfo();
+        $import = new Import();
+        $import->name = $zipEntityInfo['name'];
+        $import->book_count = $zipEntityInfo['book_count'];
+        $import->chapter_count = $zipEntityInfo['chapter_count'];
+        $import->page_count = $zipEntityInfo['page_count'];
+        $import->created_by = user()->id;
+        $import->size = filesize($zipPath);
+        // TODO - Set path
+        // TODO - Save
+
+        // TODO - Split out attachment service to separate out core filesystem/disk stuff
+        //        To reuse for import handling
+
         dd('passed');
         // TODO - Upload to storage
         // TODO - Store info/results for display:
-          // - zip_path
-          // - name (From name of thing being imported)
-          // - size
-          // - book_count
-          // - chapter_count
-          // - page_count
-          // - created_by
-          // - created_at/updated_at
         // TODO - Send user to next import stage
     }
 }
diff --git a/app/Exports/Import.php b/app/Exports/Import.php
new file mode 100644 (file)
index 0000000..c3ac3d5
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace BookStack\Exports;
+
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @property string $path
+ * @property string $name
+ * @property int $size - ZIP size in bytes
+ * @property int $book_count
+ * @property int $chapter_count
+ * @property int $page_count
+ * @property int $created_by
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ */
+class Import extends Model
+{
+    use HasFactory;
+
+    public const TYPE_BOOK = 'book';
+    public const TYPE_CHAPTER = 'chapter';
+    public const TYPE_PAGE = 'page';
+
+    /**
+     * Get the type (model) that this import is intended to be.
+     */
+    public function getType(): string
+    {
+        if ($this->book_count === 1) {
+            return self::TYPE_BOOK;
+        } elseif ($this->chapter_count === 1) {
+            return self::TYPE_CHAPTER;
+        }
+
+        return self::TYPE_PAGE;
+    }
+}
diff --git a/app/Exports/ZipExports/ZipExportReader.php b/app/Exports/ZipExports/ZipExportReader.php
new file mode 100644 (file)
index 0000000..7187a18
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+namespace BookStack\Exports\ZipExports;
+
+use BookStack\Exceptions\ZipExportException;
+use ZipArchive;
+
+class ZipExportReader
+{
+    protected ZipArchive $zip;
+    protected bool $open = false;
+
+    public function __construct(
+        protected string $zipPath,
+    ) {
+        $this->zip = new ZipArchive();
+    }
+
+    /**
+     * @throws ZipExportException
+     */
+    protected function open(): void
+    {
+        if ($this->open) {
+            return;
+        }
+
+        // Validate file exists
+        if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
+            throw new ZipExportException(trans('errors.import_zip_cant_read'));
+        }
+
+        // Validate file is valid zip
+        $opened = $this->zip->open($this->zipPath, ZipArchive::RDONLY);
+        if ($opened !== true) {
+            throw new ZipExportException(trans('errors.import_zip_cant_read'));
+        }
+
+        $this->open = true;
+    }
+
+    public function close(): void
+    {
+        if ($this->open) {
+            $this->zip->close();
+            $this->open = false;
+        }
+    }
+
+    /**
+     * @throws ZipExportException
+     */
+    public function readData(): array
+    {
+        $this->open();
+
+        // Validate json data exists, including metadata
+        $jsonData = $this->zip->getFromName('data.json') ?: '';
+        $importData = json_decode($jsonData, true);
+        if (!$importData) {
+            throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));
+        }
+
+        return $importData;
+    }
+
+    public function fileExists(string $fileName): bool
+    {
+        return $this->zip->statName("files/{$fileName}") !== false;
+    }
+
+    /**
+     * @throws ZipExportException
+     * @returns array{name: string, book_count: int, chapter_count: int, page_count: int}
+     */
+    public function getEntityInfo(): array
+    {
+        $data = $this->readData();
+        $info = ['name' => '', 'book_count' => 0, 'chapter_count' => 0, 'page_count' => 0];
+
+        if (isset($data['book'])) {
+            $info['name'] = $data['book']['name'] ?? '';
+            $info['book_count']++;
+            $chapters = $data['book']['chapters'] ?? [];
+            $pages = $data['book']['pages'] ?? [];
+            $info['chapter_count'] += count($chapters);
+            $info['page_count'] += count($pages);
+            foreach ($chapters as $chapter) {
+                $info['page_count'] += count($chapter['pages'] ?? []);
+            }
+        } elseif (isset($data['chapter'])) {
+            $info['name'] = $data['chapter']['name'] ?? '';
+            $info['chapter_count']++;
+            $info['page_count'] += count($data['chapter']['pages'] ?? []);
+        } elseif (isset($data['page'])) {
+            $info['name'] = $data['page']['name'] ?? '';
+            $info['page_count']++;
+        }
+
+        return $info;
+    }
+}
index dd56f3e70a8e7d4b75583cfc35d5454dd3011edc..e476998c216dd0ffa54081ac8ab58ccabc902a32 100644 (file)
@@ -2,10 +2,10 @@
 
 namespace BookStack\Exports\ZipExports;
 
+use BookStack\Exceptions\ZipExportException;
 use BookStack\Exports\ZipExports\Models\ZipExportBook;
 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
 use BookStack\Exports\ZipExports\Models\ZipExportPage;
-use ZipArchive;
 
 class ZipExportValidator
 {
@@ -16,26 +16,14 @@ class ZipExportValidator
 
     public function validate(): array
     {
-        // Validate file exists
-        if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
-            return ['format' => trans('errors.import_zip_cant_read')];
+        $reader = new ZipExportReader($this->zipPath);
+        try {
+            $importData = $reader->readData();
+        } catch (ZipExportException $exception) {
+            return ['format' => $exception->getMessage()];
         }
 
-        // Validate file is valid zip
-        $zip = new \ZipArchive();
-        $opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
-        if ($opened !== true) {
-            return ['format' => trans('errors.import_zip_cant_read')];
-        }
-
-        // Validate json data exists, including metadata
-        $jsonData = $zip->getFromName('data.json') ?: '';
-        $importData = json_decode($jsonData, true);
-        if (!$importData) {
-            return ['format' => trans('errors.import_zip_cant_decode_data')];
-        }
-
-        $helper = new ZipValidationHelper($zip);
+        $helper = new ZipValidationHelper($reader);
 
         if (isset($importData['book'])) {
             $modelErrors = ZipExportBook::validate($helper, $importData['book']);
index 4f942e0e78967c4443a545a5c0658029f4ec6afa..bcd3c39acf054d28a6e24edad5c8e830e6887507 100644 (file)
@@ -19,7 +19,7 @@ class ZipFileReferenceRule implements ValidationRule
      */
     public function validate(string $attribute, mixed $value, Closure $fail): void
     {
-        if (!$this->context->zipFileExists($value)) {
+        if (!$this->context->zipReader->fileExists($value)) {
             $fail('validation.zip_file')->translate();
         }
     }
index 8c285deaf5dd802fd42a8d16f87924d905ded3cf..55c86b03b5ba1ee982e34f37bb7410922ecff40a 100644 (file)
@@ -4,14 +4,13 @@ namespace BookStack\Exports\ZipExports;
 
 use BookStack\Exports\ZipExports\Models\ZipExportModel;
 use Illuminate\Validation\Factory;
-use ZipArchive;
 
 class ZipValidationHelper
 {
     protected Factory $validationFactory;
 
     public function __construct(
-        protected ZipArchive $zip,
+        public ZipExportReader $zipReader,
     ) {
         $this->validationFactory = app(Factory::class);
     }
@@ -27,11 +26,6 @@ class ZipValidationHelper
         return $messages;
     }
 
-    public function zipFileExists(string $name): bool
-    {
-        return $this->zip->statName("files/{$name}") !== false;
-    }
-
     public function fileReferenceRule(): ZipFileReferenceRule
     {
         return new ZipFileReferenceRule($this);
diff --git a/database/factories/Exports/ImportFactory.php b/database/factories/Exports/ImportFactory.php
new file mode 100644 (file)
index 0000000..55378d5
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace Database\Factories\Exports;
+
+use BookStack\Users\Models\User;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+class ImportFactory extends Factory
+{
+    /**
+     * The name of the factory's corresponding model.
+     *
+     * @var string
+     */
+    protected $model = \BookStack\Exports\Import::class;
+
+    /**
+     * Define the model's default state.
+     */
+    public function definition(): array
+    {
+        return [
+            'path' => 'uploads/imports/' . Str::random(10) . '.zip',
+            'name' => $this->faker->words(3, true),
+            'book_count' => 1,
+            'chapter_count' => 5,
+            'page_count' => 15,
+            'created_at' => User::factory(),
+        ];
+    }
+}
diff --git a/database/migrations/2024_11_02_160700_create_imports_table.php b/database/migrations/2024_11_02_160700_create_imports_table.php
new file mode 100644 (file)
index 0000000..ed18822
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('imports', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('name');
+            $table->string('path');
+            $table->integer('size');
+            $table->integer('book_count');
+            $table->integer('chapter_count');
+            $table->integer('page_count');
+            $table->integer('created_by');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('imports');
+    }
+};