diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php index 323ecef26..bbf0ff57d 100644 --- a/app/Exports/Controllers/ImportController.php +++ b/app/Exports/Controllers/ImportController.php @@ -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 index 000000000..c3ac3d529 --- /dev/null +++ b/app/Exports/Import.php @@ -0,0 +1,41 @@ +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 index 000000000..7187a1889 --- /dev/null +++ b/app/Exports/ZipExports/ZipExportReader.php @@ -0,0 +1,102 @@ +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; + } +} diff --git a/app/Exports/ZipExports/ZipExportValidator.php b/app/Exports/ZipExports/ZipExportValidator.php index dd56f3e70..e476998c2 100644 --- a/app/Exports/ZipExports/ZipExportValidator.php +++ b/app/Exports/ZipExports/ZipExportValidator.php @@ -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']); diff --git a/app/Exports/ZipExports/ZipFileReferenceRule.php b/app/Exports/ZipExports/ZipFileReferenceRule.php index 4f942e0e7..bcd3c39ac 100644 --- a/app/Exports/ZipExports/ZipFileReferenceRule.php +++ b/app/Exports/ZipExports/ZipFileReferenceRule.php @@ -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(); } } diff --git a/app/Exports/ZipExports/ZipValidationHelper.php b/app/Exports/ZipExports/ZipValidationHelper.php index 8c285deaf..55c86b03b 100644 --- a/app/Exports/ZipExports/ZipValidationHelper.php +++ b/app/Exports/ZipExports/ZipValidationHelper.php @@ -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 index 000000000..55378d583 --- /dev/null +++ b/database/factories/Exports/ImportFactory.php @@ -0,0 +1,32 @@ + '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 index 000000000..ed1882269 --- /dev/null +++ b/database/migrations/2024_11_02_160700_create_imports_table.php @@ -0,0 +1,34 @@ +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'); + } +};