Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Portable ZIP Export Format #5260

Merged
merged 41 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e088d09
ZIP Export: Started defining format
ssddanbrown Oct 13, 2024
1930af9
ZIP Export: Started types in format doc
ssddanbrown Oct 13, 2024
42bd07d
ZIP Export: Continued expanding format doc types
ssddanbrown Oct 15, 2024
42b9700
ZIP Exports: Finished up format doc, move files, started builder
ssddanbrown Oct 15, 2024
bf0262d
Testing: Split export tests into multiple files
ssddanbrown Oct 19, 2024
21ccfa9
ZIP Export: Expanded page & added base attachment handling
ssddanbrown Oct 19, 2024
7c39dd5
ZIP Export: Started building link/ref handling
ssddanbrown Oct 20, 2024
06ffd8e
Zip Exports: Added attachment/image link resolving & JSON null handling
ssddanbrown Oct 21, 2024
4fb4fe0
ZIP Exports: Added working image handling/inclusion
ssddanbrown Oct 21, 2024
f732ef0
ZIP Exports: Reorganised files, added page md parsing
ssddanbrown Oct 23, 2024
42ada66
ZIP Exports: Added core logic for books/chapters
ssddanbrown Oct 23, 2024
484342f
ZIP Exports: Added entity cross refs, Started export tests
ssddanbrown Oct 23, 2024
d1f69fe
ZIP Exports: Tested each type and model of export
ssddanbrown Oct 27, 2024
4051d5b
ZIP Exports: Added new import permission
ssddanbrown Oct 29, 2024
a56a28f
ZIP Exports: Built out initial import view
ssddanbrown Oct 29, 2024
b50b7b6
ZIP Exports: Started import validation
ssddanbrown Oct 30, 2024
c4ec50d
ZIP Exports: Got zip format validation functionally complete
ssddanbrown Oct 30, 2024
259aa82
ZIP Imports: Added validation message display, added testing
ssddanbrown Nov 2, 2024
74fce96
ZIP Import: Added model+migration, and reader class
ssddanbrown Nov 2, 2024
8ea3855
ZIP Import: Added upload handling
ssddanbrown Nov 2, 2024
c6109c7
ZIP Imports: Added listing, show view, delete, activity
ssddanbrown Nov 3, 2024
8f6f819
ZIP Imports: Fleshed out continue page, Added testing
ssddanbrown Nov 3, 2024
14578c2
ZIP Imports: Added parent selector for page/chapter imports
ssddanbrown Nov 4, 2024
92cfde4
ZIP Imports: Added full contents view to import display
ssddanbrown Nov 5, 2024
7b84558
ZIP Imports: Added parent and permission check pre-import
ssddanbrown Nov 5, 2024
d13e4d2
ZIP imports: Started actual import logic
ssddanbrown Nov 9, 2024
378f0d5
ZIP Imports: Built out reference parsing/updating logic
ssddanbrown Nov 10, 2024
48c101a
ZIP Imports: Finished off core import logic
ssddanbrown Nov 11, 2024
b7476a9
ZIP Import: Finished base import process & error handling
ssddanbrown Nov 14, 2024
7681e32
ZIP Imports: Added high level import run tests
ssddanbrown Nov 16, 2024
8645aea
ZIP Imports: Started testing core import logic
ssddanbrown Nov 16, 2024
c2c64e2
ZIP Imports: Covered import runner with further testing
ssddanbrown Nov 16, 2024
e2f6e50
ZIP Exports: Added ID checks and testing to validator
ssddanbrown Nov 18, 2024
59cfc08
ZIP Imports: Added image type validation/handling
ssddanbrown Nov 18, 2024
c0dff6d
ZIP Imports: Added book content ordering to import preview
ssddanbrown Nov 22, 2024
f79c6ae
ZIP Imports: Updated import form to show loading indicator
ssddanbrown Nov 22, 2024
9ecc919
ZIP Import & Exports: Addressed issues during testing
ssddanbrown Nov 25, 2024
95d62e7
ZIP Imports/Exports: Fixed some lint and test issues
ssddanbrown Nov 25, 2024
0a182a4
ZIP Exports: Added detection/handling of images with external storage
ssddanbrown Nov 26, 2024
edb684c
ZIP Exports: Updated format doc with advisories regarding html/md
ssddanbrown Nov 26, 2024
bdca9fc
ZIP Exports: Changed the instance id mechanism
ssddanbrown Nov 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Activity/ActivityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions app/Entities/Models/Chapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function defaultTemplate(): BelongsTo

/**
* Get the visible pages in this chapter.
* @returns Collection<Page>
*/
public function getVisiblePages(): Collection
{
Expand Down
13 changes: 12 additions & 1 deletion app/Entities/Repos/PageRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ public function publishDraft(Page $draft, array $input): Page
return $draft;
}

/**
* Directly update the content for the given page from the provided input.
* Used for direct content access in a way that performs required changes
* (Search index & reference regen) without performing an official update.
*/
public function setContentFromInput(Page $page, array $input): void
{
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, []);
}

/**
* Update a page in the system.
*/
Expand Down Expand Up @@ -121,7 +132,7 @@ public function update(Page $page, array $input): Page
return $page;
}

protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
protected function updateTemplateStatusAndContentFromInput(Page $page, array $input): void
{
if (isset($input['template']) && userCan('templates-manage')) {
$page->template = ($input['template'] === 'true');
Expand Down
17 changes: 6 additions & 11 deletions app/Entities/Tools/Cloner.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@

class Cloner
{
protected PageRepo $pageRepo;
protected ChapterRepo $chapterRepo;
protected BookRepo $bookRepo;
protected ImageService $imageService;

public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
{
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->bookRepo = $bookRepo;
$this->imageService = $imageService;
public function __construct(
protected PageRepo $pageRepo,
protected ChapterRepo $chapterRepo,
protected BookRepo $bookRepo,
protected ImageService $imageService,
) {
}

/**
Expand Down
7 changes: 7 additions & 0 deletions app/Exceptions/ZipExportException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace BookStack\Exceptions;

class ZipExportException extends \Exception
{
}
13 changes: 13 additions & 0 deletions app/Exceptions/ZipImportException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace BookStack\Exceptions;

class ZipImportException extends \Exception
{
public function __construct(
public array $errors
) {
$message = "Import failed with errors:" . implode("\n", $this->errors);
parent::__construct($message);
}
}
12 changes: 12 additions & 0 deletions app/Exceptions/ZipValidationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace BookStack\Exceptions;

class ZipValidationException extends \Exception
{
public function __construct(
public array $errors
) {
parent::__construct();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exports\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use Throwable;

Expand Down Expand Up @@ -63,4 +65,16 @@ public function markdown(string $bookSlug)

return $this->download()->directly($textContent, $bookSlug . '.md');
}

/**
* Export a book to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, ZipExportBuilder $builder)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$zip = $builder->buildForBook($book);

return $this->download()->streamedDirectly(fopen($zip, 'r'), $bookSlug . '.zip', filesize($zip));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exports\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use Throwable;

Expand Down Expand Up @@ -70,4 +71,16 @@ public function markdown(string $bookSlug, string $chapterSlug)

return $this->download()->directly($chapterText, $chapterSlug . '.md');
}

/**
* Export a book to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, string $chapterSlug, ZipExportBuilder $builder)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$zip = $builder->buildForChapter($chapter);

return $this->download()->streamedDirectly(fopen($zip, 'r'), $chapterSlug . '.zip', filesize($zip));
}
}
110 changes: 110 additions & 0 deletions app/Exports/Controllers/ImportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace BookStack\Exports\Controllers;

use BookStack\Exceptions\ZipImportException;
use BookStack\Exceptions\ZipValidationException;
use BookStack\Exports\ImportRepo;
use BookStack\Http\Controller;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\Request;

class ImportController extends Controller
{
public function __construct(
protected ImportRepo $imports,
) {
$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()
{
$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, [
'file' => ['required', ...AttachmentService::getFileValidationRules()]
]);

$file = $request->file('file');
try {
$import = $this->imports->storeFromUpload($file);
} catch (ZipValidationException $exception) {
return redirect('/import')->with('validation_errors', $exception->errors);
}

return redirect($import->getUrl());
}

/**
* Show a pending import, with a form to allow progressing
* with the import process.
*/
public function show(int $id)
{
$import = $this->imports->findVisible($id);

$this->setPageTitle(trans('entities.import_continue'));

return view('exports.import-show', [
'import' => $import,
'data' => $import->decodeMetadata(),
]);
}

/**
* Run the import process against an uploaded import ZIP.
*/
public function run(int $id, Request $request)
{
$import = $this->imports->findVisible($id);
$parent = null;

if ($import->type === 'page' || $import->type === 'chapter') {
session()->setPreviousUrl($import->getUrl());
$data = $this->validate($request, [
'parent' => ['required', 'string'],
]);
$parent = $data['parent'];
}

try {
$entity = $this->imports->runImport($import, $parent);
} catch (ZipImportException $exception) {
session()->flush();
$this->showErrorNotification(trans('errors.import_zip_failed_notification'));
return redirect($import->getUrl())->with('import_errors', $exception->errors);
}

return redirect($entity->getUrl());
}

/**
* Delete an active pending import from the filesystem and database.
*/
public function delete(int $id)
{
$import = $this->imports->findVisible($id);
$this->imports->deleteImport($import);

return redirect('/import');
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exports\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<?php

namespace BookStack\Entities\Controllers;
namespace BookStack\Exports\Controllers;

use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use Throwable;

Expand Down Expand Up @@ -74,4 +75,16 @@ public function markdown(string $bookSlug, string $pageSlug)

return $this->download()->directly($pageText, $pageSlug . '.md');
}

/**
* Export a page to a contained ZIP export file.
* @throws NotFoundException
*/
public function zip(string $bookSlug, string $pageSlug, ZipExportBuilder $builder)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$zip = $builder->buildForPage($page);

return $this->download()->streamedDirectly(fopen($zip, 'r'), $pageSlug . '.zip', filesize($zip));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?php

namespace BookStack\Entities\Tools;
namespace BookStack\Exports;

use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\ImageService;
use BookStack\Util\CspService;
use BookStack\Util\HtmlDocument;
Expand Down
Loading
Loading