ZIP Import: Added model+migration, and reader class
Some checks are pending
analyse-php / build (push) Waiting to run
lint-php / build (push) Waiting to run
test-migrations / build (8.1) (push) Waiting to run
test-migrations / build (8.2) (push) Waiting to run
test-migrations / build (8.3) (push) Waiting to run
test-php / build (8.1) (push) Waiting to run
test-php / build (8.2) (push) Waiting to run
test-php / build (8.3) (push) Waiting to run

This commit is contained in:
Dan Brown 2024-11-02 17:17:34 +00:00
parent 259aa829d4
commit 74fce9640e
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
8 changed files with 234 additions and 35 deletions

View 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
}
}

41
app/Exports/Import.php Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

View 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']);

View 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();
}
}

View 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);

View File

@ -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(),
];
}
}

View File

@ -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');
}
};