mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-08-07 23:03:00 +03:00
ZIP Import: Finished base import process & error handling
Added file creation reverting and DB rollback on error. Added error display on failed import. Extracted likely shown import form/error text to translation files.
This commit is contained in:
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace BookStack\Exports\Controllers;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Exceptions\ZipImportException;
|
||||
use BookStack\Exceptions\ZipValidationException;
|
||||
use BookStack\Exports\ImportRepo;
|
||||
use BookStack\Http\Controller;
|
||||
@@ -48,12 +48,9 @@ class ImportController extends Controller
|
||||
try {
|
||||
$import = $this->imports->storeFromUpload($file);
|
||||
} catch (ZipValidationException $exception) {
|
||||
session()->flash('validation_errors', $exception->errors);
|
||||
return redirect('/import');
|
||||
return redirect('/import')->with('validation_errors', $exception->errors);
|
||||
}
|
||||
|
||||
$this->logActivity(ActivityType::IMPORT_CREATE, $import);
|
||||
|
||||
return redirect($import->getUrl());
|
||||
}
|
||||
|
||||
@@ -80,20 +77,20 @@ class ImportController extends Controller
|
||||
$parent = null;
|
||||
|
||||
if ($import->type === 'page' || $import->type === 'chapter') {
|
||||
session()->setPreviousUrl($import->getUrl());
|
||||
$data = $this->validate($request, [
|
||||
'parent' => ['required', 'string']
|
||||
'parent' => ['required', 'string'],
|
||||
]);
|
||||
$parent = $data['parent'];
|
||||
}
|
||||
|
||||
$entity = $this->imports->runImport($import, $parent);
|
||||
if ($entity) {
|
||||
$this->logActivity(ActivityType::IMPORT_RUN, $import);
|
||||
return redirect($entity->getUrl());
|
||||
try {
|
||||
$entity = $this->imports->runImport($import, $parent);
|
||||
} catch (ZipImportException $exception) {
|
||||
return redirect($import->getUrl())->with('import_errors', $exception->errors);
|
||||
}
|
||||
// TODO - Redirect to result
|
||||
// TODO - Or redirect back with errors
|
||||
return 'failed';
|
||||
|
||||
return redirect($entity->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,8 +101,6 @@ class ImportController extends Controller
|
||||
$import = $this->imports->findVisible($id);
|
||||
$this->imports->deleteImport($import);
|
||||
|
||||
$this->logActivity(ActivityType::IMPORT_DELETE, $import);
|
||||
|
||||
return redirect('/import');
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Exports;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
@@ -14,8 +15,10 @@ use BookStack\Exports\ZipExports\Models\ZipExportPage;
|
||||
use BookStack\Exports\ZipExports\ZipExportReader;
|
||||
use BookStack\Exports\ZipExports\ZipExportValidator;
|
||||
use BookStack\Exports\ZipExports\ZipImportRunner;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Uploads\FileStorage;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImportRepo
|
||||
@@ -93,25 +96,42 @@ class ImportRepo
|
||||
$import->path = $path;
|
||||
$import->save();
|
||||
|
||||
Activity::add(ActivityType::IMPORT_CREATE, $import);
|
||||
|
||||
return $import;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZipValidationException|ZipImportException
|
||||
* @throws ZipImportException
|
||||
*/
|
||||
public function runImport(Import $import, ?string $parent = null): ?Entity
|
||||
public function runImport(Import $import, ?string $parent = null): Entity
|
||||
{
|
||||
$parentModel = null;
|
||||
if ($import->type === 'page' || $import->type === 'chapter') {
|
||||
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
|
||||
}
|
||||
|
||||
return $this->importer->run($import, $parentModel);
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$model = $this->importer->run($import, $parentModel);
|
||||
} catch (ZipImportException $e) {
|
||||
DB::rollBack();
|
||||
$this->importer->revertStoredFiles();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
$this->deleteImport($import);
|
||||
Activity::add(ActivityType::IMPORT_RUN, $import);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function deleteImport(Import $import): void
|
||||
{
|
||||
$this->storage->delete($import->path);
|
||||
$import->delete();
|
||||
|
||||
Activity::add(ActivityType::IMPORT_DELETE, $import);
|
||||
}
|
||||
}
|
||||
|
@@ -139,4 +139,21 @@ class ZipImportReferences
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Image[]
|
||||
*/
|
||||
public function images(): array
|
||||
{
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return $this->attachments;
|
||||
}
|
||||
}
|
||||
|
@@ -47,14 +47,17 @@ class ZipImportRunner
|
||||
* Returns the top-level entity item which was imported.
|
||||
* @throws ZipImportException
|
||||
*/
|
||||
public function run(Import $import, ?Entity $parent = null): ?Entity
|
||||
public function run(Import $import, ?Entity $parent = null): Entity
|
||||
{
|
||||
$zipPath = $this->getZipPath($import);
|
||||
$reader = new ZipExportReader($zipPath);
|
||||
|
||||
$errors = (new ZipExportValidator($reader))->validate();
|
||||
if ($errors) {
|
||||
throw new ZipImportException(["ZIP failed to validate"]);
|
||||
throw new ZipImportException([
|
||||
trans('errors.import_validation_failed'),
|
||||
...$errors,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -65,15 +68,14 @@ class ZipImportRunner
|
||||
|
||||
// Validate parent type
|
||||
if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
|
||||
throw new ZipImportException(["Must not have a parent set for a Book import"]);
|
||||
} else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
|
||||
throw new ZipImportException(["Parent book required for chapter import"]);
|
||||
throw new ZipImportException(["Must not have a parent set for a Book import."]);
|
||||
} else if ($exportModel instanceof ZipExportChapter && !($parent instanceof Book)) {
|
||||
throw new ZipImportException(["Parent book required for chapter import."]);
|
||||
} else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
|
||||
throw new ZipImportException(["Parent book or chapter required for page import"]);
|
||||
throw new ZipImportException(["Parent book or chapter required for page import."]);
|
||||
}
|
||||
|
||||
$this->ensurePermissionsPermitImport($exportModel);
|
||||
$entity = null;
|
||||
$this->ensurePermissionsPermitImport($exportModel, $parent);
|
||||
|
||||
if ($exportModel instanceof ZipExportBook) {
|
||||
$entity = $this->importBook($exportModel, $reader);
|
||||
@@ -81,32 +83,46 @@ class ZipImportRunner
|
||||
$entity = $this->importChapter($exportModel, $parent, $reader);
|
||||
} else if ($exportModel instanceof ZipExportPage) {
|
||||
$entity = $this->importPage($exportModel, $parent, $reader);
|
||||
} else {
|
||||
throw new ZipImportException(['No importable data found in import data.']);
|
||||
}
|
||||
|
||||
// TODO - In transaction?
|
||||
// TODO - Revert uploaded files if goes wrong
|
||||
// TODO - Attachments
|
||||
// TODO - Images
|
||||
// (Both listed/stored in references)
|
||||
|
||||
$this->references->replaceReferences();
|
||||
|
||||
$reader->close();
|
||||
$this->cleanup();
|
||||
|
||||
dd('stop');
|
||||
|
||||
// TODO - Delete import/zip after import?
|
||||
// Do this in parent repo?
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
protected function cleanup()
|
||||
/**
|
||||
* Revert any files which have been stored during this import process.
|
||||
* Considers files only, and avoids the database under the
|
||||
* assumption that the database may already have been
|
||||
* reverted as part of a transaction rollback.
|
||||
*/
|
||||
public function revertStoredFiles(): void
|
||||
{
|
||||
foreach ($this->references->images() as $image) {
|
||||
$this->imageService->destroyFileAtPath($image->type, $image->path);
|
||||
}
|
||||
|
||||
foreach ($this->references->attachments() as $attachment) {
|
||||
if (!$attachment->external) {
|
||||
$this->attachmentService->deleteFileInStorage($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
protected function cleanup(): void
|
||||
{
|
||||
foreach ($this->tempFilesToCleanup as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
$this->tempFilesToCleanup = [];
|
||||
}
|
||||
|
||||
protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
|
||||
@@ -256,9 +272,6 @@ class ZipImportRunner
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// TODO - Extract messages to language files
|
||||
// TODO - Ensure these are shown to users on failure
|
||||
|
||||
$chapters = [];
|
||||
$pages = [];
|
||||
$images = [];
|
||||
@@ -266,7 +279,7 @@ class ZipImportRunner
|
||||
|
||||
if ($exportModel instanceof ZipExportBook) {
|
||||
if (!userCan('book-create-all')) {
|
||||
$errors[] = 'You are lacking the required permission to create books.';
|
||||
$errors[] = trans('errors.import_perms_books');
|
||||
}
|
||||
array_push($pages, ...$exportModel->pages);
|
||||
array_push($chapters, ...$exportModel->chapters);
|
||||
@@ -283,7 +296,7 @@ class ZipImportRunner
|
||||
if (count($chapters) > 0) {
|
||||
$permission = 'chapter-create' . ($parent ? '' : '-all');
|
||||
if (!userCan($permission, $parent)) {
|
||||
$errors[] = 'You are lacking the required permission to create chapters.';
|
||||
$errors[] = trans('errors.import_perms_chapters');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,25 +308,25 @@ class ZipImportRunner
|
||||
if (count($pages) > 0) {
|
||||
if ($parent) {
|
||||
if (!userCan('page-create', $parent)) {
|
||||
$errors[] = 'You are lacking the required permission to create pages.';
|
||||
$errors[] = trans('errors.import_perms_pages');
|
||||
}
|
||||
} else {
|
||||
$hasPermission = userCan('page-create-all') || userCan('page-create-own');
|
||||
if (!$hasPermission) {
|
||||
$errors[] = 'You are lacking the required permission to create pages.';
|
||||
$errors[] = trans('errors.import_perms_pages');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($images) > 0) {
|
||||
if (!userCan('image-create-all')) {
|
||||
$errors[] = 'You are lacking the required permissions to create images.';
|
||||
$errors[] = trans('errors.import_perms_images');
|
||||
}
|
||||
}
|
||||
|
||||
if (count($attachments) > 0) {
|
||||
if (!userCan('attachment-create-all')) {
|
||||
$errors[] = 'You are lacking the required permissions to create attachments.';
|
||||
$errors[] = trans('errors.import_perms_attachments');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -151,7 +151,7 @@ class AttachmentService
|
||||
* Delete a file from the filesystem it sits on.
|
||||
* Cleans any empty leftover folders.
|
||||
*/
|
||||
protected function deleteFileInStorage(Attachment $attachment): void
|
||||
public function deleteFileInStorage(Attachment $attachment): void
|
||||
{
|
||||
$this->storage->delete($attachment->path);
|
||||
}
|
||||
|
@@ -153,11 +153,19 @@ class ImageService
|
||||
*/
|
||||
public function destroy(Image $image): void
|
||||
{
|
||||
$disk = $this->storage->getDisk($image->type);
|
||||
$disk->destroyAllMatchingNameFromPath($image->path);
|
||||
$this->destroyFileAtPath($image->type, $image->path);
|
||||
$image->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the underlying image file at the given path.
|
||||
*/
|
||||
public function destroyFileAtPath(string $type, string $path): void
|
||||
{
|
||||
$disk = $this->storage->getDisk($type);
|
||||
$disk->destroyAllMatchingNameFromPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
|
||||
* Checks based off of only the image name.
|
||||
|
Reference in New Issue
Block a user