mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-02-21 11:01:12 +08:00
ZIP Imports: Added listing, show view, delete, activity
This commit is contained in:
parent
8ea3855e02
commit
c6109c7087
@ -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.
|
||||
*/
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
38
resources/views/exports/import-show.blade.php
Normal file
38
resources/views/exports/import-show.blade.php
Normal 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
|
@ -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
|
||||
|
19
resources/views/exports/parts/import.blade.php
Normal file
19
resources/views/exports/parts/import.blade.php
Normal 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>
|
@ -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']);
|
||||
|
Loading…
x
Reference in New Issue
Block a user