ZIP Export: Expanded page & added base attachment handling
Some checks failed
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.1) (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-php / build (8.1) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled

This commit is contained in:
Dan Brown
2024-10-19 15:41:07 +01:00
parent bf0262d7d1
commit 21ccfa97dd
8 changed files with 154 additions and 12 deletions

View File

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

View File

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

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

View File

@ -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
) {
}
/**