mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-02-21 11:15:40 +08:00
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
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:
parent
259aa829d4
commit
74fce9640e
@ -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
41
app/Exports/Import.php
Normal 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;
|
||||
}
|
||||
}
|
102
app/Exports/ZipExports/ZipExportReader.php
Normal file
102
app/Exports/ZipExports/ZipExportReader.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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']);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
32
database/factories/Exports/ImportFactory.php
Normal file
32
database/factories/Exports/ImportFactory.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user