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;
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
}
}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
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
{
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']);
*/
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();
}
}
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);
}
return $messages;
}
- public function zipFileExists(string $name): bool
- {
- return $this->zip->statName("files/{$name}") !== false;
- }
-
public function fileReferenceRule(): ZipFileReferenceRule
{
return new ZipFileReferenceRule($this);
--- /dev/null
+<?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(),
+ ];
+ }
+}
--- /dev/null
+<?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');
+ }
+};