mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-28 18:01:48 +08:00
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
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:
@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user