ZIP Imports: Added listing, show view, delete, activity

This commit is contained in:
Dan Brown 2024-11-03 14:13:05 +00:00
parent 8ea3855e02
commit c6109c7087
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 193 additions and 6 deletions

View 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.
*/

View 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');
}
}

View 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}";
}
}

View 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();
}
}

View 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);
}

View 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',

View 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',

View File

@ -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

View File

@ -38,6 +38,19 @@
</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

View File

@ -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>

View 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']);