From c6109c708735434fdb30333ff4c24b4a80b0b749 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 3 Nov 2024 14:13:05 +0000 Subject: [PATCH] ZIP Imports: Added listing, show view, delete, activity --- app/Activity/ActivityType.php | 4 ++ app/Exports/Controllers/ImportController.php | 50 ++++++++++++++++++- app/Exports/Import.php | 23 ++++++++- app/Exports/ImportRepo.php | 32 ++++++++++++ app/Http/Controller.php | 4 +- lang/en/activities.php | 8 +++ lang/en/entities.php | 6 +++ resources/views/exports/import-show.blade.php | 38 ++++++++++++++ resources/views/exports/import.blade.php | 13 +++++ .../views/exports/parts/import.blade.php | 19 +++++++ routes/web.php | 2 + 11 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 resources/views/exports/import-show.blade.php create mode 100644 resources/views/exports/parts/import.blade.php diff --git a/app/Activity/ActivityType.php b/app/Activity/ActivityType.php index 09b2ae73c..5ec9b9cf0 100644 --- a/app/Activity/ActivityType.php +++ b/app/Activity/ActivityType.php @@ -67,6 +67,10 @@ class ActivityType const WEBHOOK_UPDATE = 'webhook_update'; const WEBHOOK_DELETE = 'webhook_delete'; + const IMPORT_CREATE = 'import_create'; + const IMPORT_RUN = 'import_run'; + const IMPORT_DELETE = 'import_delete'; + /** * Get all the possible values. */ diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php index 640b4c108..582fff975 100644 --- a/app/Exports/Controllers/ImportController.php +++ b/app/Exports/Controllers/ImportController.php @@ -1,7 +1,10 @@ middleware('can:content-import'); } + /** + * Show the view to start a new import, and also list out the existing + * in progress imports that are visible to the user. + */ public function start(Request $request) { - // TODO - Show existing imports for user (or for all users if admin-level user) + // TODO - Test visibility access for listed items + $imports = $this->imports->getVisibleImports(); + + $this->setPageTitle(trans('entities.import')); return view('exports.import', [ + 'imports' => $imports, 'zipErrors' => session()->pull('validation_errors') ?? [], ]); } + /** + * Upload, validate and store an import file. + */ public function upload(Request $request) { $this->validate($request, [ @@ -39,6 +53,38 @@ class ImportController extends Controller return redirect('/import'); } - return redirect("imports/{$import->id}"); + $this->logActivity(ActivityType::IMPORT_CREATE, $import); + + return redirect($import->getUrl()); + } + + /** + * Show a pending import, with a form to allow progressing + * with the import process. + */ + public function show(int $id) + { + // TODO - Test visibility access + $import = $this->imports->findVisible($id); + + $this->setPageTitle(trans('entities.import_continue')); + + return view('exports.import-show', [ + 'import' => $import, + ]); + } + + /** + * Delete an active pending import from the filesystem and database. + */ + public function delete(int $id) + { + // TODO - Test visibility access + $import = $this->imports->findVisible($id); + $this->imports->deleteImport($import); + + $this->logActivity(ActivityType::IMPORT_DELETE, $import); + + return redirect('/import'); } } diff --git a/app/Exports/Import.php b/app/Exports/Import.php index c3ac3d529..520d8ea6c 100644 --- a/app/Exports/Import.php +++ b/app/Exports/Import.php @@ -2,6 +2,7 @@ namespace BookStack\Exports; +use BookStack\Activity\Models\Loggable; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -17,7 +18,7 @@ use Illuminate\Database\Eloquent\Model; * @property Carbon $created_at * @property Carbon $updated_at */ -class Import extends Model +class Import extends Model implements Loggable { use HasFactory; @@ -38,4 +39,24 @@ class Import extends Model return self::TYPE_PAGE; } + + public function getSizeString(): string + { + $mb = round($this->size / 1000000, 2); + return "{$mb} MB"; + } + + /** + * Get the URL to view/continue this import. + */ + public function getUrl(string $path = ''): string + { + $path = ltrim($path, '/'); + return url("/import/{$this->id}" . ($path ? '/' . $path : '')); + } + + public function logDescriptor(): string + { + return "({$this->id}) {$this->name}"; + } } diff --git a/app/Exports/ImportRepo.php b/app/Exports/ImportRepo.php index c8157967b..d7e169ad1 100644 --- a/app/Exports/ImportRepo.php +++ b/app/Exports/ImportRepo.php @@ -6,6 +6,7 @@ use BookStack\Exceptions\ZipValidationException; use BookStack\Exports\ZipExports\ZipExportReader; use BookStack\Exports\ZipExports\ZipExportValidator; use BookStack\Uploads\FileStorage; +use Illuminate\Database\Eloquent\Collection; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImportRepo @@ -15,6 +16,31 @@ class ImportRepo ) { } + /** + * @return Collection + */ + public function getVisibleImports(): Collection + { + $query = Import::query(); + + if (!userCan('settings-manage')) { + $query->where('created_by', user()->id); + } + + return $query->get(); + } + + public function findVisible(int $id): Import + { + $query = Import::query(); + + if (!userCan('settings-manage')) { + $query->where('created_by', user()->id); + } + + return $query->findOrFail($id); + } + public function storeFromUpload(UploadedFile $file): Import { $zipPath = $file->getRealPath(); @@ -45,4 +71,10 @@ class ImportRepo return $import; } + + public function deleteImport(Import $import): void + { + $this->storage->delete($import->path); + $import->delete(); + } } diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 8facf5dab..090cf523a 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -152,10 +152,8 @@ abstract class Controller extends BaseController /** * Log an activity in the system. - * - * @param string|Loggable $detail */ - protected function logActivity(string $type, $detail = ''): void + protected function logActivity(string $type, string|Loggable $detail = ''): void { Activity::add($type, $detail); } diff --git a/lang/en/activities.php b/lang/en/activities.php index 092398ef0..7c3454d41 100644 --- a/lang/en/activities.php +++ b/lang/en/activities.php @@ -84,6 +84,14 @@ return [ 'webhook_delete' => 'deleted webhook', 'webhook_delete_notification' => 'Webhook successfully deleted', + // Imports + 'import_create' => 'created import', + 'import_create_notification' => 'Import successfully uploaded', + 'import_run' => 'updated import', + 'import_run_notification' => 'Content successfully imported', + 'import_delete' => 'deleted import', + 'import_delete_notification' => 'Import successfully deleted', + // Users 'user_create' => 'created user', 'user_create_notification' => 'User successfully created', diff --git a/lang/en/entities.php b/lang/en/entities.php index 106147335..e2d8e47c5 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -48,6 +48,12 @@ return [ 'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to import then press "Validate Import" to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.', 'import_zip_select' => 'Select ZIP file to upload', 'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:', + 'import_pending' => 'Pending Imports', + 'import_pending_none' => 'No imports have been started.', + 'import_continue' => 'Continue Import', + 'import_run' => 'Run Import', + 'import_delete_confirm' => 'Are you sure you want to delete this import?', + 'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.', // Permissions and restrictions 'permissions' => 'Permissions', diff --git a/resources/views/exports/import-show.blade.php b/resources/views/exports/import-show.blade.php new file mode 100644 index 000000000..843a05246 --- /dev/null +++ b/resources/views/exports/import-show.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.simple') + +@section('body') + +
+ +
+

{{ trans('entities.import_continue') }}

+
+ {{ csrf_field() }} +
+ +
+ {{ trans('common.cancel') }} +
+ + +
+ +
+
+
+ +
+ {{ method_field('DELETE') }} + {{ csrf_field() }} +
+ +@stop diff --git a/resources/views/exports/import.blade.php b/resources/views/exports/import.blade.php index c4d7c8818..be9de4c0e 100644 --- a/resources/views/exports/import.blade.php +++ b/resources/views/exports/import.blade.php @@ -38,6 +38,19 @@ + +
+

{{ trans('entities.import_pending') }}

+ @if(count($imports) === 0) +

{{ trans('entities.import_pending_none') }}

+ @else +
+ @foreach($imports as $import) + @include('exports.parts.import', ['import' => $import]) + @endforeach +
+ @endif +
@stop diff --git a/resources/views/exports/parts/import.blade.php b/resources/views/exports/parts/import.blade.php new file mode 100644 index 000000000..5ff6600f2 --- /dev/null +++ b/resources/views/exports/parts/import.blade.php @@ -0,0 +1,19 @@ +@php + $type = $import->getType(); +@endphp +
+ +
+ @if($type === 'book') +
@icon('chapter') {{ $import->chapter_count }}
+ @endif + @if($type === 'book' || $type === 'chapter') +
@icon('page') {{ $import->page_count }}
+ @endif +
{{ $import->getSizeString() }}
+
@icon('time'){{ $import->created_at->diffForHumans() }}
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 91aab13fe..c490bb3b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -209,6 +209,8 @@ Route::middleware('auth')->group(function () { // Importing Route::get('/import', [ExportControllers\ImportController::class, 'start']); Route::post('/import', [ExportControllers\ImportController::class, 'upload']); + Route::get('/import/{id}', [ExportControllers\ImportController::class, 'show']); + Route::delete('/import/{id}', [ExportControllers\ImportController::class, 'delete']); // Other Pages Route::get('/', [HomeController::class, 'index']);