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.
*/
<?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;
$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, [
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');
}
}
namespace BookStack\Exports;
+use BookStack\Activity\Models\Loggable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
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;
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}";
+ }
}
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
) {
}
+ /**
+ * @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();
return $import;
}
+
+ public function deleteImport(Import $import): void
+ {
+ $this->storage->delete($import->path);
+ $import->delete();
+ }
}
/**
* 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);
}
'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',
'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',
--- /dev/null
+@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
</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
--- /dev/null
+@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
// 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']);