From c61ce8dee4ea474b16564c3d3337bac25fba1f46 Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 12:45:09 +0530 Subject: [PATCH 1/3] Implement functionality to export a book, along with its pages and chapters, as a ZIP file. --- .../Controllers/BookExportApiController.php | 18 +++++++++++++++++- .../Controllers/ChapterExportApiController.php | 12 +++++++++++- .../Controllers/PageExportApiController.php | 14 +++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/Exports/Controllers/BookExportApiController.php b/app/Exports/Controllers/BookExportApiController.php index 164946b0c..431afef14 100644 --- a/app/Exports/Controllers/BookExportApiController.php +++ b/app/Exports/Controllers/BookExportApiController.php @@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers; use BookStack\Entities\Queries\BookQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,19 @@ class BookExportApiController extends ApiController return $this->download()->directly($markdown, $book->slug . '.md'); } -} + + + /** + * Export a book to a contained ZIP export file. + * @throws NotFoundException + */ + public function exportZip(int $id, ZipExportBuilder $builder) + { + $book = $this->queries->findVisibleByIdOrFail($id); + $bookName= $book->getShortName(); + + $zip = $builder->buildForBook($book); + + return $this->download()->streamedFileDirectly($zip, $bookName . '.zip', filesize($zip), true); + } +} \ No newline at end of file diff --git a/app/Exports/Controllers/ChapterExportApiController.php b/app/Exports/Controllers/ChapterExportApiController.php index 9914e2b7f..58df4c9b0 100644 --- a/app/Exports/Controllers/ChapterExportApiController.php +++ b/app/Exports/Controllers/ChapterExportApiController.php @@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers; use BookStack\Entities\Queries\ChapterQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,13 @@ class ChapterExportApiController extends ApiController return $this->download()->directly($markdown, $chapter->slug . '.md'); } -} + + public function exportZip(int $id, ZipExportBuilder $builder) + { + $chapter = $this->queries->findVisibleByIdOrFail($id); + $chapterName= $chapter->getShortName(); + $zip = $builder->buildForChapter($chapter); + + return $this->download()->streamedFileDirectly($zip, $chapterName . '.zip', filesize($zip), true); + } +} \ No newline at end of file diff --git a/app/Exports/Controllers/PageExportApiController.php b/app/Exports/Controllers/PageExportApiController.php index c6e20b615..ef564da3e 100644 --- a/app/Exports/Controllers/PageExportApiController.php +++ b/app/Exports/Controllers/PageExportApiController.php @@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers; use BookStack\Entities\Queries\PageQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,15 @@ class PageExportApiController extends ApiController return $this->download()->directly($markdown, $page->slug . '.md'); } -} + + + + public function exportZip(int $id, ZipExportBuilder $builder) + { + $page = $this->queries->findVisibleByIdOrFail($id); + $pageSlug = $page->slug; + $zip = $builder->buildForPage($page); + + return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', filesize($zip), true); + } +} \ No newline at end of file From 5fa728f28a0c4f09649da7b653dd66260e49a596 Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 12:48:34 +0530 Subject: [PATCH 2/3] Develop functionality to import ZIP files. Create an API controller and define a route entry for handling the import process. Implement logic to read the list of files within the ZIP, process the directory structure, and automatically create associated pages, chapters, and books based on the ZIP file's contents. --- .../Controllers/ImportApiController.php | 121 ++++++++++++++++++ routes/api.php | 6 + 2 files changed, 127 insertions(+) create mode 100644 app/Exports/Controllers/ImportApiController.php diff --git a/app/Exports/Controllers/ImportApiController.php b/app/Exports/Controllers/ImportApiController.php new file mode 100644 index 000000000..682d340b3 --- /dev/null +++ b/app/Exports/Controllers/ImportApiController.php @@ -0,0 +1,121 @@ +middleware('can:content-import'); + } + + /** + * List existing imports visible to the user. + */ + public function list(): JsonResponse + { + $imports = $this->imports->getVisibleImports(); + + return response()->json([ + 'status' => 'success', + 'imports' => $imports, + ]); + } + + /** + * Upload, validate and store an import file. + */ + public function upload(Request $request): JsonResponse + { + $this->validate($request, [ + 'file' => ['required', ...AttachmentService::getFileValidationRules()] + ]); + + $file = $request->file('file'); + + try { + $import = $this->imports->storeFromUpload($file); + } catch (ZipValidationException $exception) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation failed', + 'errors' => $exception->errors, + ], 422); + } + + return response()->json([ + 'status' => 'success', + 'import' => $import, + ], 201); + } + + /** + * Show details of a pending import. + */ + public function read(int $id): JsonResponse + { + $import = $this->imports->findVisible($id); + + return response()->json([ + 'status' => 'success', + 'import' => $import, + 'data' => $import->decodeMetadata(), + ]); + } + + /** + * Run the import process. + */ + public function create(int $id, Request $request): JsonResponse + { + $import = $this->imports->findVisible($id); + $parent = null; + + if ($import->type === 'page' || $import->type === 'chapter') { + $data = $this->validate($request, [ + 'parent' => ['required', 'string'], + ]); + $parent = $data['parent']; + } + + try { + $entity = $this->imports->runImport($import, $parent); + } catch (ZipImportException $exception) { + return response()->json([ + 'status' => 'error', + 'message' => 'Import failed', + 'errors' => $exception->errors, + ], 500); + } + + return response()->json([ + 'status' => 'success', + 'entity' => $entity, + ]); + } + + /** + * Delete a pending import. + */ + public function delete(int $id): JsonResponse + { + $import = $this->imports->findVisible($id); + $this->imports->deleteImport($import); + + return response()->json([ + 'status' => 'success', + 'message' => 'Import deleted successfully', + ]); + } +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 710364855..bd00ea4b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -92,3 +92,9 @@ Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionAp Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']); Route::get('audit-log', [AuditLogApiController::class, 'list']); + +Route::get('import', [ExportControllers\ImportApiController::class, 'list']); +Route::post('import', [ExportControllers\ImportApiController::class, 'upload']); +Route::get('import/{id}', [ExportControllers\ImportApiController::class, 'read']); +Route::post('import/{id}/create', [ExportControllers\ImportApiController::class, 'create']); +Route::delete('import/{id}', [ExportControllers\ImportApiController::class, 'destroy']); \ No newline at end of file From 64da80cbf4611f0e6a1700e6e2fa399389ee50da Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 13:00:06 +0530 Subject: [PATCH 3/3] added routes for zip export --- routes/api.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/api.php b/routes/api.php index bd00ea4b0..7bc7d7d44 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,6 +36,7 @@ Route::get('books/{id}/export/html', [ExportControllers\BookExportApiController: Route::get('books/{id}/export/pdf', [ExportControllers\BookExportApiController::class, 'exportPdf']); Route::get('books/{id}/export/plaintext', [ExportControllers\BookExportApiController::class, 'exportPlainText']); Route::get('books/{id}/export/markdown', [ExportControllers\BookExportApiController::class, 'exportMarkdown']); +Route::get('books/{id}/export/zip', [ExportControllers\BookExportApiController::class, 'exportZip']); Route::get('chapters', [EntityControllers\ChapterApiController::class, 'list']); Route::post('chapters', [EntityControllers\ChapterApiController::class, 'create']); @@ -46,6 +47,7 @@ Route::get('chapters/{id}/export/html', [ExportControllers\ChapterExportApiContr Route::get('chapters/{id}/export/pdf', [ExportControllers\ChapterExportApiController::class, 'exportPdf']); Route::get('chapters/{id}/export/plaintext', [ExportControllers\ChapterExportApiController::class, 'exportPlainText']); Route::get('chapters/{id}/export/markdown', [ExportControllers\ChapterExportApiController::class, 'exportMarkdown']); +Route::get('chapters/{id}/export/zip', [ExportControllers\ChapterExportApiController::class, 'exportZip']); Route::get('pages', [EntityControllers\PageApiController::class, 'list']); Route::post('pages', [EntityControllers\PageApiController::class, 'create']); @@ -57,6 +59,7 @@ Route::get('pages/{id}/export/html', [ExportControllers\PageExportApiController: Route::get('pages/{id}/export/pdf', [ExportControllers\PageExportApiController::class, 'exportPdf']); Route::get('pages/{id}/export/plaintext', [ExportControllers\PageExportApiController::class, 'exportPlainText']); Route::get('pages/{id}/export/markdown', [ExportControllers\PageExportApiController::class, 'exportMarkdown']); +Route::get('pages/{id}/export/zip', [ExportControllers\PageExportApiController::class, 'exportZip']); Route::get('image-gallery', [ImageGalleryApiController::class, 'list']); Route::post('image-gallery', [ImageGalleryApiController::class, 'create']);