Skip to content

Commit b7476a9

Browse files
committed
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.
1 parent 48c101a commit b7476a9

File tree

10 files changed

+145
-68
lines changed

10 files changed

+145
-68
lines changed

app/Exports/Controllers/ImportController.php

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace BookStack\Exports\Controllers;
66

7-
use BookStack\Activity\ActivityType;
7+
use BookStack\Exceptions\ZipImportException;
88
use BookStack\Exceptions\ZipValidationException;
99
use BookStack\Exports\ImportRepo;
1010
use BookStack\Http\Controller;
@@ -48,12 +48,9 @@ public function upload(Request $request)
4848
try {
4949
$import = $this->imports->storeFromUpload($file);
5050
} catch (ZipValidationException $exception) {
51-
session()->flash('validation_errors', $exception->errors);
52-
return redirect('/import');
51+
return redirect('/import')->with('validation_errors', $exception->errors);
5352
}
5453

55-
$this->logActivity(ActivityType::IMPORT_CREATE, $import);
56-
5754
return redirect($import->getUrl());
5855
}
5956

@@ -80,20 +77,20 @@ public function run(int $id, Request $request)
8077
$parent = null;
8178

8279
if ($import->type === 'page' || $import->type === 'chapter') {
80+
session()->setPreviousUrl($import->getUrl());
8381
$data = $this->validate($request, [
84-
'parent' => ['required', 'string']
82+
'parent' => ['required', 'string'],
8583
]);
8684
$parent = $data['parent'];
8785
}
8886

89-
$entity = $this->imports->runImport($import, $parent);
90-
if ($entity) {
91-
$this->logActivity(ActivityType::IMPORT_RUN, $import);
92-
return redirect($entity->getUrl());
87+
try {
88+
$entity = $this->imports->runImport($import, $parent);
89+
} catch (ZipImportException $exception) {
90+
return redirect($import->getUrl())->with('import_errors', $exception->errors);
9391
}
94-
// TODO - Redirect to result
95-
// TODO - Or redirect back with errors
96-
return 'failed';
92+
93+
return redirect($entity->getUrl());
9794
}
9895

9996
/**
@@ -104,8 +101,6 @@ public function delete(int $id)
104101
$import = $this->imports->findVisible($id);
105102
$this->imports->deleteImport($import);
106103

107-
$this->logActivity(ActivityType::IMPORT_DELETE, $import);
108-
109104
return redirect('/import');
110105
}
111106
}

app/Exports/ImportRepo.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace BookStack\Exports;
44

5+
use BookStack\Activity\ActivityType;
56
use BookStack\Entities\Models\Entity;
67
use BookStack\Entities\Queries\EntityQueries;
78
use BookStack\Exceptions\FileUploadException;
@@ -14,8 +15,10 @@
1415
use BookStack\Exports\ZipExports\ZipExportReader;
1516
use BookStack\Exports\ZipExports\ZipExportValidator;
1617
use BookStack\Exports\ZipExports\ZipImportRunner;
18+
use BookStack\Facades\Activity;
1719
use BookStack\Uploads\FileStorage;
1820
use Illuminate\Database\Eloquent\Collection;
21+
use Illuminate\Support\Facades\DB;
1922
use Symfony\Component\HttpFoundation\File\UploadedFile;
2023

2124
class ImportRepo
@@ -93,25 +96,42 @@ public function storeFromUpload(UploadedFile $file): Import
9396
$import->path = $path;
9497
$import->save();
9598

99+
Activity::add(ActivityType::IMPORT_CREATE, $import);
100+
96101
return $import;
97102
}
98103

99104
/**
100-
* @throws ZipValidationException|ZipImportException
105+
* @throws ZipImportException
101106
*/
102-
public function runImport(Import $import, ?string $parent = null): ?Entity
107+
public function runImport(Import $import, ?string $parent = null): Entity
103108
{
104109
$parentModel = null;
105110
if ($import->type === 'page' || $import->type === 'chapter') {
106111
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
107112
}
108113

109-
return $this->importer->run($import, $parentModel);
114+
DB::beginTransaction();
115+
try {
116+
$model = $this->importer->run($import, $parentModel);
117+
} catch (ZipImportException $e) {
118+
DB::rollBack();
119+
$this->importer->revertStoredFiles();
120+
throw $e;
121+
}
122+
123+
DB::commit();
124+
$this->deleteImport($import);
125+
Activity::add(ActivityType::IMPORT_RUN, $import);
126+
127+
return $model;
110128
}
111129

112130
public function deleteImport(Import $import): void
113131
{
114132
$this->storage->delete($import->path);
115133
$import->delete();
134+
135+
Activity::add(ActivityType::IMPORT_DELETE, $import);
116136
}
117137
}

app/Exports/ZipExports/ZipImportReferences.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,21 @@ public function replaceReferences(): void
139139
]);
140140
}
141141
}
142+
143+
144+
/**
145+
* @return Image[]
146+
*/
147+
public function images(): array
148+
{
149+
return $this->images;
150+
}
151+
152+
/**
153+
* @return Attachment[]
154+
*/
155+
public function attachments(): array
156+
{
157+
return $this->attachments;
158+
}
142159
}

app/Exports/ZipExports/ZipImportRunner.php

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ public function __construct(
4747
* Returns the top-level entity item which was imported.
4848
* @throws ZipImportException
4949
*/
50-
public function run(Import $import, ?Entity $parent = null): ?Entity
50+
public function run(Import $import, ?Entity $parent = null): Entity
5151
{
5252
$zipPath = $this->getZipPath($import);
5353
$reader = new ZipExportReader($zipPath);
5454

5555
$errors = (new ZipExportValidator($reader))->validate();
5656
if ($errors) {
57-
throw new ZipImportException(["ZIP failed to validate"]);
57+
throw new ZipImportException([
58+
trans('errors.import_validation_failed'),
59+
...$errors,
60+
]);
5861
}
5962

6063
try {
@@ -65,48 +68,61 @@ public function run(Import $import, ?Entity $parent = null): ?Entity
6568

6669
// Validate parent type
6770
if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
68-
throw new ZipImportException(["Must not have a parent set for a Book import"]);
69-
} else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
70-
throw new ZipImportException(["Parent book required for chapter import"]);
71+
throw new ZipImportException(["Must not have a parent set for a Book import."]);
72+
} else if ($exportModel instanceof ZipExportChapter && !($parent instanceof Book)) {
73+
throw new ZipImportException(["Parent book required for chapter import."]);
7174
} else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
72-
throw new ZipImportException(["Parent book or chapter required for page import"]);
75+
throw new ZipImportException(["Parent book or chapter required for page import."]);
7376
}
7477

75-
$this->ensurePermissionsPermitImport($exportModel);
76-
$entity = null;
78+
$this->ensurePermissionsPermitImport($exportModel, $parent);
7779

7880
if ($exportModel instanceof ZipExportBook) {
7981
$entity = $this->importBook($exportModel, $reader);
8082
} else if ($exportModel instanceof ZipExportChapter) {
8183
$entity = $this->importChapter($exportModel, $parent, $reader);
8284
} else if ($exportModel instanceof ZipExportPage) {
8385
$entity = $this->importPage($exportModel, $parent, $reader);
86+
} else {
87+
throw new ZipImportException(['No importable data found in import data.']);
8488
}
8589

86-
// TODO - In transaction?
87-
// TODO - Revert uploaded files if goes wrong
88-
// TODO - Attachments
89-
// TODO - Images
90-
// (Both listed/stored in references)
91-
9290
$this->references->replaceReferences();
9391

9492
$reader->close();
9593
$this->cleanup();
9694

97-
dd('stop');
95+
return $entity;
96+
}
9897

99-
// TODO - Delete import/zip after import?
100-
// Do this in parent repo?
98+
/**
99+
* Revert any files which have been stored during this import process.
100+
* Considers files only, and avoids the database under the
101+
* assumption that the database may already have been
102+
* reverted as part of a transaction rollback.
103+
*/
104+
public function revertStoredFiles(): void
105+
{
106+
foreach ($this->references->images() as $image) {
107+
$this->imageService->destroyFileAtPath($image->type, $image->path);
108+
}
101109

102-
return $entity;
110+
foreach ($this->references->attachments() as $attachment) {
111+
if (!$attachment->external) {
112+
$this->attachmentService->deleteFileInStorage($attachment);
113+
}
114+
}
115+
116+
$this->cleanup();
103117
}
104118

105-
protected function cleanup()
119+
protected function cleanup(): void
106120
{
107121
foreach ($this->tempFilesToCleanup as $file) {
108122
unlink($file);
109123
}
124+
125+
$this->tempFilesToCleanup = [];
110126
}
111127

112128
protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
@@ -256,17 +272,14 @@ protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|
256272
{
257273
$errors = [];
258274

259-
// TODO - Extract messages to language files
260-
// TODO - Ensure these are shown to users on failure
261-
262275
$chapters = [];
263276
$pages = [];
264277
$images = [];
265278
$attachments = [];
266279

267280
if ($exportModel instanceof ZipExportBook) {
268281
if (!userCan('book-create-all')) {
269-
$errors[] = 'You are lacking the required permission to create books.';
282+
$errors[] = trans('errors.import_perms_books');
270283
}
271284
array_push($pages, ...$exportModel->pages);
272285
array_push($chapters, ...$exportModel->chapters);
@@ -283,7 +296,7 @@ protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|
283296
if (count($chapters) > 0) {
284297
$permission = 'chapter-create' . ($parent ? '' : '-all');
285298
if (!userCan($permission, $parent)) {
286-
$errors[] = 'You are lacking the required permission to create chapters.';
299+
$errors[] = trans('errors.import_perms_chapters');
287300
}
288301
}
289302

@@ -295,25 +308,25 @@ protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|
295308
if (count($pages) > 0) {
296309
if ($parent) {
297310
if (!userCan('page-create', $parent)) {
298-
$errors[] = 'You are lacking the required permission to create pages.';
311+
$errors[] = trans('errors.import_perms_pages');
299312
}
300313
} else {
301314
$hasPermission = userCan('page-create-all') || userCan('page-create-own');
302315
if (!$hasPermission) {
303-
$errors[] = 'You are lacking the required permission to create pages.';
316+
$errors[] = trans('errors.import_perms_pages');
304317
}
305318
}
306319
}
307320

308321
if (count($images) > 0) {
309322
if (!userCan('image-create-all')) {
310-
$errors[] = 'You are lacking the required permissions to create images.';
323+
$errors[] = trans('errors.import_perms_images');
311324
}
312325
}
313326

314327
if (count($attachments) > 0) {
315328
if (!userCan('attachment-create-all')) {
316-
$errors[] = 'You are lacking the required permissions to create attachments.';
329+
$errors[] = trans('errors.import_perms_attachments');
317330
}
318331
}
319332

app/Uploads/AttachmentService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public function deleteFile(Attachment $attachment)
151151
* Delete a file from the filesystem it sits on.
152152
* Cleans any empty leftover folders.
153153
*/
154-
protected function deleteFileInStorage(Attachment $attachment): void
154+
public function deleteFileInStorage(Attachment $attachment): void
155155
{
156156
$this->storage->delete($attachment->path);
157157
}

app/Uploads/ImageService.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,19 @@ public function getImageStream(Image $image): mixed
153153
*/
154154
public function destroy(Image $image): void
155155
{
156-
$disk = $this->storage->getDisk($image->type);
157-
$disk->destroyAllMatchingNameFromPath($image->path);
156+
$this->destroyFileAtPath($image->type, $image->path);
158157
$image->delete();
159158
}
160159

160+
/**
161+
* Destroy the underlying image file at the given path.
162+
*/
163+
public function destroyFileAtPath(string $type, string $path): void
164+
{
165+
$disk = $this->storage->getDisk($type);
166+
$disk->destroyAllMatchingNameFromPath($path);
167+
}
168+
161169
/**
162170
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
163171
* Checks based off of only the image name.

lang/en/entities.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
'import_pending_none' => 'No imports have been started.',
5353
'import_continue' => 'Continue Import',
5454
'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',
55+
'import_details' => 'Import Details',
5556
'import_run' => 'Run Import',
5657
'import_size' => ':size Import ZIP Size',
5758
'import_uploaded_at' => 'Uploaded :relativeTime',
@@ -60,6 +61,8 @@
6061
'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.',
6162
'import_delete_confirm' => 'Are you sure you want to delete this import?',
6263
'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',
64+
'import_errors' => 'Import Errors',
65+
'import_errors_desc' => 'The follow errors occurred during the import attempt:',
6366

6467
// Permissions and restrictions
6568
'permissions' => 'Permissions',

lang/en/errors.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@
109109
'import_zip_cant_read' => 'Could not read ZIP file.',
110110
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
111111
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
112+
'import_validation_failed' => 'Import ZIP failed to validate with errors:',
113+
'import_perms_books' => 'You are lacking the required permissions to create books.',
114+
'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',
115+
'import_perms_pages' => 'You are lacking the required permissions to create pages.',
116+
'import_perms_images' => 'You are lacking the required permissions to create images.',
117+
'import_perms_attachments' => 'You are lacking the required permission to create attachments.',
112118

113119
// API errors
114120
'api_no_authorization_found' => 'No authorization token found on the request',

0 commit comments

Comments
 (0)