mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
ZIP Export: Expanded page & added base attachment handling
This commit is contained in:
parent
bf0262d7d1
commit
21ccfa97dd
@ -6,6 +6,7 @@ use BookStack\Entities\Queries\PageQueries;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exports\ExportFormatter;
|
||||
use BookStack\Exports\ZipExportBuilder;
|
||||
use BookStack\Http\Controller;
|
||||
use Throwable;
|
||||
|
||||
@ -74,4 +75,16 @@ class PageExportController extends Controller
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,70 @@
|
||||
|
||||
namespace BookStack\Exports;
|
||||
|
||||
use BookStack\Activity\Models\Tag;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\ZipExportException;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use ZipArchive;
|
||||
|
||||
class ZipExportBuilder
|
||||
{
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(
|
||||
protected ZipExportFiles $files
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZipExportException
|
||||
*/
|
||||
public function buildForPage(Page $page): string
|
||||
{
|
||||
$this->data['page'] = [
|
||||
'id' => $page->id,
|
||||
$this->data['page'] = $this->convertPage($page);
|
||||
return $this->build();
|
||||
}
|
||||
|
||||
protected function convertPage(Page $page): array
|
||||
{
|
||||
$tags = array_map($this->convertTag(...), $page->tags()->get()->all());
|
||||
$attachments = array_map($this->convertAttachment(...), $page->attachments()->get()->all());
|
||||
|
||||
return [
|
||||
'id' => $page->id,
|
||||
'name' => $page->name,
|
||||
'html' => '', // TODO
|
||||
'markdown' => '', // TODO
|
||||
'priority' => $page->priority,
|
||||
'attachments' => $attachments,
|
||||
'images' => [], // TODO
|
||||
'tags' => $tags,
|
||||
];
|
||||
}
|
||||
|
||||
protected function convertAttachment(Attachment $attachment): array
|
||||
{
|
||||
$data = [
|
||||
'name' => $attachment->name,
|
||||
'order' => $attachment->order,
|
||||
];
|
||||
|
||||
return $this->build();
|
||||
if ($attachment->external) {
|
||||
$data['link'] = $attachment->path;
|
||||
} else {
|
||||
$data['file'] = $this->files->referenceForAttachment($attachment);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function convertTag(Tag $tag): array
|
||||
{
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'value' => $tag->value,
|
||||
'order' => $tag->order,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,7 +75,7 @@ class ZipExportBuilder
|
||||
{
|
||||
$this->data['exported_at'] = date(DATE_ATOM);
|
||||
$this->data['instance'] = [
|
||||
'version' => trim(file_get_contents(base_path('version'))),
|
||||
'version' => trim(file_get_contents(base_path('version'))),
|
||||
'id_ciphertext' => encrypt('bookstack'),
|
||||
];
|
||||
|
||||
@ -43,6 +89,18 @@ class ZipExportBuilder
|
||||
$zip->addFromString('data.json', json_encode($this->data));
|
||||
$zip->addEmptyDir('files');
|
||||
|
||||
$toRemove = [];
|
||||
$this->files->extractEach(function ($filePath, $fileRef) use ($zip, &$toRemove) {
|
||||
$zip->addFile($filePath, "files/$fileRef");
|
||||
$toRemove[] = $filePath;
|
||||
});
|
||||
|
||||
$zip->close();
|
||||
|
||||
foreach ($toRemove as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
return $zipFile;
|
||||
}
|
||||
}
|
||||
|
58
app/Exports/ZipExportFiles.php
Normal file
58
app/Exports/ZipExportFiles.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exports;
|
||||
|
||||
use BookStack\Uploads\Attachment;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ZipExportFiles
|
||||
{
|
||||
/**
|
||||
* References for attachments by attachment ID.
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $attachmentRefsById = [];
|
||||
|
||||
public function __construct(
|
||||
protected AttachmentService $attachmentService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gain a reference to the given attachment instance.
|
||||
* This is expected to be a file-based attachment that the user
|
||||
* has visibility of, no permission/access checks are performed here.
|
||||
*/
|
||||
public function referenceForAttachment(Attachment $attachment): string
|
||||
{
|
||||
if (isset($this->attachmentRefsById[$attachment->id])) {
|
||||
return $this->attachmentRefsById[$attachment->id];
|
||||
}
|
||||
|
||||
do {
|
||||
$fileName = Str::random(20) . '.' . $attachment->extension;
|
||||
} while (in_array($fileName, $this->attachmentRefsById));
|
||||
|
||||
$this->attachmentRefsById[$attachment->id] = $fileName;
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract each of the ZIP export tracked files.
|
||||
* Calls the given callback for each tracked file, passing a temporary
|
||||
* file reference of the file contents, and the zip-local tracked reference.
|
||||
*/
|
||||
public function extractEach(callable $callback): void
|
||||
{
|
||||
foreach ($this->attachmentRefsById as $attachmentId => $ref) {
|
||||
$attachment = Attachment::query()->find($attachmentId);
|
||||
$stream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'bszipfile-');
|
||||
$tmpFileStream = fopen($tmpFile, 'w');
|
||||
stream_copy_to_stream($stream, $tmpFileStream);
|
||||
$callback($tmpFile, $ref);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,14 +13,9 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService
|
||||
{
|
||||
protected FilesystemManager $fileSystem;
|
||||
|
||||
/**
|
||||
* AttachmentService constructor.
|
||||
*/
|
||||
public function __construct(FilesystemManager $fileSystem)
|
||||
{
|
||||
$this->fileSystem = $fileSystem;
|
||||
public function __construct(
|
||||
protected FilesystemManager $fileSystem
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,6 +39,7 @@ return [
|
||||
'export_pdf' => 'PDF File',
|
||||
'export_text' => 'Plain Text File',
|
||||
'export_md' => 'Markdown File',
|
||||
'export_zip' => 'Portable ZIP',
|
||||
'default_template' => 'Default Page Template',
|
||||
'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',
|
||||
'default_template_select' => 'Select a template page',
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
|
||||
<li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
|
||||
<li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
|
||||
<li><a href="{{ $entity->getUrl('/export/zip') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_zip') }}</span><span>.zip</span></a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
@ -91,6 +91,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/export/html', [ExportControllers\PageExportController::class, 'html']);
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/export/markdown', [ExportControllers\PageExportController::class, 'markdown']);
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/export/plaintext', [ExportControllers\PageExportController::class, 'plainText']);
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/export/zip', [ExportControllers\PageExportController::class, 'zip']);
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/edit', [EntityControllers\PageController::class, 'edit']);
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/move', [EntityControllers\PageController::class, 'showMove']);
|
||||
Route::put('/books/{bookSlug}/page/{pageSlug}/move', [EntityControllers\PageController::class, 'move']);
|
||||
|
15
tests/Exports/ZipExportTest.php
Normal file
15
tests/Exports/ZipExportTest.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Exports;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ZipExportTest extends TestCase
|
||||
{
|
||||
public function test_page_export()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
// TODO
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user