]> BookStack Code Mirror - bookstack/commitdiff
ZIP Imports: Added listing, show view, delete, activity
authorDan Brown <redacted>
Sun, 3 Nov 2024 14:13:05 +0000 (14:13 +0000)
committerDan Brown <redacted>
Sun, 3 Nov 2024 14:13:05 +0000 (14:13 +0000)
app/Activity/ActivityType.php
app/Exports/Controllers/ImportController.php
app/Exports/Import.php
app/Exports/ImportRepo.php
app/Http/Controller.php
lang/en/activities.php
lang/en/entities.php
resources/views/exports/import-show.blade.php [new file with mode: 0644]
resources/views/exports/import.blade.php
resources/views/exports/parts/import.blade.php [new file with mode: 0644]
routes/web.php

index 09b2ae73c561e9b8be68292f370d13afa5dd4de9..5ec9b9cf0dc7f452040b2ca3278c24d0ed0c9855 100644 (file)
@@ -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.
      */
index 640b4c1089147bf1dac2d31d25de4acc6c67ca89..582fff975d2880a4928e3f884fd320d18071e74d 100644 (file)
@@ -1,7 +1,10 @@
 <?php
 
+declare(strict_types=1);
+
 namespace BookStack\Exports\Controllers;
 
+use BookStack\Activity\ActivityType;
 use BookStack\Exceptions\ZipValidationException;
 use BookStack\Exports\ImportRepo;
 use BookStack\Http\Controller;
@@ -16,15 +19,26 @@ class ImportController extends Controller
         $this->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');
     }
 }
index c3ac3d52924bc1c58f8bd6b6847282f419034034..520d8ea6cc8445635460b1a2709dcf307ca6e40a 100644 (file)
@@ -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}";
+    }
 }
index c8157967bc3783a3f3abf02471e67819deda8b05..d7e169ad1663b8327710c210e6a82090820a62ad 100644 (file)
@@ -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<Import>
+     */
+    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();
+    }
 }
index 8facf5dab3c3331d1538b373b68b269d6c6f53b4..090cf523ad28051751f0cca3b325890cf99e7332 100644 (file)
@@ -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);
     }
index 092398ef0e1475089fa71b7e60aa1f49fc056013..7c3454d41ca287406316a6eb3f528250c5361c3d 100644 (file)
@@ -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',
index 1061473353324f73444533337f9a89f8e9e7044f..e2d8e47c5929f4eed985ef37d62e5c7f9006186c 100644 (file)
@@ -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 (file)
index 0000000..843a052
--- /dev/null
@@ -0,0 +1,38 @@
+@extends('layouts.simple')
+
+@section('body')
+
+    <div class="container small">
+
+        <main class="card content-wrap auto-height mt-xxl">
+            <h1 class="list-heading">{{ trans('entities.import_continue') }}</h1>
+            <form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
+                {{ csrf_field() }}
+            </form>
+
+            <div class="text-right">
+                <a href="{{ url('/import') }}" class="button outline">{{ trans('common.cancel') }}</a>
+                <div component="dropdown" class="inline block mx-s">
+                    <button refs="dropdown@toggle"
+                            type="button"
+                            title="{{ trans('common.delete') }}"
+                            class="button outline">{{ trans('common.delete') }}</button>
+                    <div refs="dropdown@menu" class="dropdown-menu">
+                        <p class="text-neg bold small px-m mb-xs">{{ trans('entities.import_delete_confirm') }}</p>
+                        <p class="small px-m mb-xs">{{ trans('entities.import_delete_desc') }}</p>
+                        <button type="submit" form="import-delete-form" class="text-link small text-item">{{ trans('common.confirm') }}</button>
+                    </div>
+                </div>
+                <button type="submit" class="button">{{ trans('entities.import_run') }}</button>
+            </div>
+        </main>
+    </div>
+
+    <form id="import-delete-form"
+          action="{{ $import->getUrl() }}"
+          method="post">
+        {{ method_field('DELETE') }}
+        {{ csrf_field() }}
+    </form>
+
+@stop
index c4d7c881845545f92fb78d2e52b8703ae4fee0a5..be9de4c0e9146be5857c6aa8e9e3e2230c0ef2f8 100644 (file)
                 </div>
             </form>
         </main>
+
+        <main class="card content-wrap auto-height mt-xxl">
+            <h2 class="list-heading">{{ trans('entities.import_pending') }}</h2>
+            @if(count($imports) === 0)
+                <p>{{ trans('entities.import_pending_none') }}</p>
+            @else
+                <div class="item-list my-m">
+                    @foreach($imports as $import)
+                        @include('exports.parts.import', ['import' => $import])
+                    @endforeach
+                </div>
+            @endif
+        </main>
     </div>
 
 @stop
diff --git a/resources/views/exports/parts/import.blade.php b/resources/views/exports/parts/import.blade.php
new file mode 100644 (file)
index 0000000..5ff6600
--- /dev/null
@@ -0,0 +1,19 @@
+@php
+    $type = $import->getType();
+@endphp
+<div class="item-list-row flex-container-row items-center justify-space-between wrap">
+    <div class="px-m py-s">
+        <a href="{{ $import->getUrl() }}"
+           class="text-{{ $type }}">@icon($type) {{ $import->name }}</a>
+    </div>
+    <div class="px-m py-s flex-container-row gap-m items-center">
+        @if($type === 'book')
+            <div class="text-chapter opacity-80 bold">@icon('chapter') {{ $import->chapter_count }}</div>
+        @endif
+        @if($type === 'book' || $type === 'chapter')
+            <div class="text-page opacity-80 bold">@icon('page') {{ $import->page_count }}</div>
+        @endif
+        <div class="bold opacity-80">{{ $import->getSizeString() }}</div>
+        <div class="bold opacity-80 text-muted" title="{{ $import->created_at->toISOString() }}">@icon('time'){{ $import->created_at->diffForHumans() }}</div>
+    </div>
+</div>
\ No newline at end of file
index 91aab13fecf9f55fbd22bee2f90cd0799f5808ef..c490bb3b34ea3bc0b8762aeaa35af381361d96d4 100644 (file)
@@ -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']);
Morty Proxy This is a proxified and sanitized view of the page, visit original site.