namespace BookStack\Exports\Controllers;
use BookStack\Entities\Queries\BookQueries;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
+use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use Throwable;
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));
+ }
}
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exports\ExportFormatter;
+use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\Controller;
use Throwable;
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));
+ }
}
namespace BookStack\Exports\ZipExports;
use BookStack\App\Model;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExports\Models\ZipExportAttachment;
use BookStack\Exports\ZipExports\Models\ZipExportBook;
use BookStack\Exports\ZipExports\Models\ZipExportChapter;
protected function handleModelReference(Model $model, ZipExportModel $exportModel, ZipExportFiles $files): ?string
{
- // TODO - References to other entities
-
// Handle attachment references
// No permission check needed here since they would only already exist in this
// reference context if already allowed via their entity access.
return null;
}
+ // Handle entity references
+ if ($model instanceof Book && isset($this->books[$model->id])) {
+ return "[[bsexport:book:{$model->id}]]";
+ } else if ($model instanceof Chapter && isset($this->chapters[$model->id])) {
+ return "[[bsexport:chapter:{$model->id}]]";
+ } else if ($model instanceof Page && isset($this->pages[$model->id])) {
+ return "[[bsexport:page:{$model->id}]]";
+ }
+
return null;
}
}
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ExportControllers\ChapterExportController::class, 'html']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ExportControllers\ChapterExportController::class, 'markdown']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ExportControllers\ChapterExportController::class, 'plainText']);
+ Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/zip', [ExportControllers\ChapterExportController::class, 'zip']);
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [PermissionsController::class, 'updateForChapter']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [EntityControllers\ChapterController::class, 'showDelete']);
namespace Tests\Exports;
-use BookStack\Entities\Models\Book;
+use Illuminate\Support\Carbon;
+use Illuminate\Testing\TestResponse;
use Tests\TestCase;
+use ZipArchive;
class ZipExportTest extends TestCase
{
- public function test_page_export()
+ public function test_export_results_in_zip_format()
+ {
+ $page = $this->entities->page();
+ $response = $this->asEditor()->get($page->getUrl("/export/zip"));
+
+ $zipData = $response->streamedContent();
+ $zipFile = tempnam(sys_get_temp_dir(), 'bstesta-');
+ file_put_contents($zipFile, $zipData);
+ $zip = new ZipArchive();
+ $zip->open($zipFile, ZipArchive::RDONLY);
+
+ $this->assertNotFalse($zip->locateName('data.json'));
+ $this->assertNotFalse($zip->locateName('files/'));
+
+ $data = json_decode($zip->getFromName('data.json'), true);
+ $this->assertIsArray($data);
+ $this->assertGreaterThan(0, count($data));
+
+ $zip->close();
+ unlink($zipFile);
+ }
+
+ public function test_export_metadata()
{
$page = $this->entities->page();
+ $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
+ $zip = $this->extractZipResponse($zipResp);
+
+ $this->assertEquals($page->id, $zip->data['page']['id'] ?? null);
+ $this->assertArrayNotHasKey('book', $zip->data);
+ $this->assertArrayNotHasKey('chapter', $zip->data);
+
+ $now = time();
+ $date = Carbon::parse($zip->data['exported_at'])->unix();
+ $this->assertLessThan($now + 2, $date);
+ $this->assertGreaterThan($now - 2, $date);
+
+ $version = trim(file_get_contents(base_path('version')));
+ $this->assertEquals($version, $zip->data['instance']['version']);
+
+ $instanceId = decrypt($zip->data['instance']['id_ciphertext']);
+ $this->assertEquals('bookstack', $instanceId);
+ }
+
+ public function test_page_export()
+ {
+ // TODO
+ }
+
+ public function test_book_export()
+ {
+ // TODO
+ }
+
+ public function test_chapter_export()
+ {
// TODO
}
+
+ protected function extractZipResponse(TestResponse $response): ZipResultData
+ {
+ $zipData = $response->streamedContent();
+ $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
+
+ file_put_contents($zipFile, $zipData);
+ $extractDir = tempnam(sys_get_temp_dir(), 'bstestextracted-');
+ if (file_exists($extractDir)) {
+ unlink($extractDir);
+ }
+ mkdir($extractDir);
+
+ $zip = new ZipArchive();
+ $zip->open($zipFile, ZipArchive::RDONLY);
+ $zip->extractTo($extractDir);
+
+ $dataJson = file_get_contents($extractDir . DIRECTORY_SEPARATOR . "data.json");
+ $data = json_decode($dataJson, true);
+
+ return new ZipResultData(
+ $zipFile,
+ $extractDir,
+ $data,
+ );
+ }
}
--- /dev/null
+<?php
+
+namespace Tests\Exports;
+
+class ZipResultData
+{
+ public function __construct(
+ public string $zipPath,
+ public string $extractedDirPath,
+ public array $data,
+ ) {
+ }
+}