From: Dan Brown Date: Fri, 18 Jul 2025 15:19:14 +0000 (+0100) Subject: ZIP Imports: Added API examples, finished testing X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/32ba3a591f982ddd99aa44e4f67b1ee6e20d91ba?ds=inline ZIP Imports: Added API examples, finished testing Also updated some types on a couple of controllers. --- diff --git a/app/Entities/Controllers/ChapterApiController.php b/app/Entities/Controllers/ChapterApiController.php index 430654330..8ac0c7a60 100644 --- a/app/Entities/Controllers/ChapterApiController.php +++ b/app/Entities/Controllers/ChapterApiController.php @@ -9,12 +9,11 @@ use BookStack\Entities\Repos\ChapterRepo; use BookStack\Exceptions\PermissionsException; use BookStack\Http\ApiController; use Exception; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Http\Request; class ChapterApiController extends ApiController { - protected $rules = [ + protected array $rules = [ 'create' => [ 'book_id' => ['required', 'integer'], 'name' => ['required', 'string', 'max:255'], diff --git a/app/Entities/Controllers/PageApiController.php b/app/Entities/Controllers/PageApiController.php index 40598e209..8fcba3dc6 100644 --- a/app/Entities/Controllers/PageApiController.php +++ b/app/Entities/Controllers/PageApiController.php @@ -12,7 +12,7 @@ use Illuminate\Http\Request; class PageApiController extends ApiController { - protected $rules = [ + protected array $rules = [ 'create' => [ 'book_id' => ['required_without:chapter_id', 'integer'], 'chapter_id' => ['required_without:book_id', 'integer'], diff --git a/app/Exports/Controllers/ImportApiController.php b/app/Exports/Controllers/ImportApiController.php index 0749ff933..cac155c7c 100644 --- a/app/Exports/Controllers/ImportApiController.php +++ b/app/Exports/Controllers/ImportApiController.php @@ -23,6 +23,7 @@ class ImportApiController extends ApiController /** * List existing ZIP imports visible to the user. + * Requires permission to import content. */ public function list(): JsonResponse { @@ -34,12 +35,18 @@ class ImportApiController extends ApiController } /** - * Upload, validate and store a ZIP import file. - * This does not run the import. That is performed via a separate endpoint. + * Start a new import from a ZIP file. + * This does not actually run the import since that is performed via the "run" endpoint. + * This uploads, validates and stores the ZIP file so it's ready to be imported. + * + * This "file" parameter must be a BookStack-compatible ZIP file, and this must be + * sent via a 'multipart/form-data' type request. + * + * Requires permission to import content. */ - public function upload(Request $request): JsonResponse + public function create(Request $request): JsonResponse { - $this->validate($request, $this->rules()['upload']); + $this->validate($request, $this->rules()['create']); $file = $request->file('file'); @@ -57,6 +64,7 @@ class ImportApiController extends ApiController * Read details of a pending ZIP import. * The "details" property contains high-level metadata regarding the ZIP import content, * and the structure of this will change depending on import "type". + * Requires permission to import content. */ public function read(int $id): JsonResponse { @@ -69,8 +77,9 @@ class ImportApiController extends ApiController /** * Run the import process for an uploaded ZIP import. - * The parent_id and parent_type parameters are required when the import type is 'chapter' or 'page'. - * On success, returns the imported item. + * The "parent_id" and "parent_type" parameters are required when the import type is "chapter" or "page". + * On success, this endpoint returns the imported item. + * Requires permission to import content. */ public function run(int $id, Request $request): JsonResponse { @@ -92,11 +101,12 @@ class ImportApiController extends ApiController return $this->jsonError($message); } - return response()->json($entity); + return response()->json($entity->withoutRelations()); } /** - * Delete a pending ZIP import. + * Delete a pending ZIP import from the system. + * Requires permission to import content. */ public function delete(int $id): Response { @@ -109,7 +119,7 @@ class ImportApiController extends ApiController protected function rules(): array { return [ - 'upload' => [ + 'create' => [ 'file' => ['required', ...AttachmentService::getFileValidationRules()], ], 'run' => [ diff --git a/app/Permissions/ContentPermissionApiController.php b/app/Permissions/ContentPermissionApiController.php index 23b75db35..bddbc2c7d 100644 --- a/app/Permissions/ContentPermissionApiController.php +++ b/app/Permissions/ContentPermissionApiController.php @@ -16,7 +16,7 @@ class ContentPermissionApiController extends ApiController ) { } - protected $rules = [ + protected array $rules = [ 'update' => [ 'owner_id' => ['int'], diff --git a/app/Search/SearchApiController.php b/app/Search/SearchApiController.php index 79cd8cfab..cd4a14a39 100644 --- a/app/Search/SearchApiController.php +++ b/app/Search/SearchApiController.php @@ -9,7 +9,7 @@ use Illuminate\Http\Request; class SearchApiController extends ApiController { - protected $rules = [ + protected array $rules = [ 'all' => [ 'query' => ['required'], 'page' => ['integer', 'min:1'], diff --git a/app/Users/Controllers/RoleApiController.php b/app/Users/Controllers/RoleApiController.php index 2e96602fa..2f3638cd3 100644 --- a/app/Users/Controllers/RoleApiController.php +++ b/app/Users/Controllers/RoleApiController.php @@ -16,7 +16,7 @@ class RoleApiController extends ApiController 'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at', ]; - protected $rules = [ + protected array $rules = [ 'create' => [ 'display_name' => ['required', 'string', 'min:3', 'max:180'], 'description' => ['string', 'max:180'], diff --git a/dev/api/requests/imports-run.json b/dev/api/requests/imports-run.json new file mode 100644 index 000000000..836a66f3a --- /dev/null +++ b/dev/api/requests/imports-run.json @@ -0,0 +1,4 @@ +{ + "parent_type": "book", + "parent_id": 28 +} \ No newline at end of file diff --git a/dev/api/responses/imports-create.json b/dev/api/responses/imports-create.json new file mode 100644 index 000000000..997758799 --- /dev/null +++ b/dev/api/responses/imports-create.json @@ -0,0 +1,10 @@ +{ + "type": "chapter", + "name": "Pension Providers", + "created_by": 1, + "size": 2757, + "path": "uploads\/files\/imports\/ghnxmS3u9QxLWu82.zip", + "updated_at": "2025-07-18T14:50:27.000000Z", + "created_at": "2025-07-18T14:50:27.000000Z", + "id": 31 +} \ No newline at end of file diff --git a/dev/api/responses/imports-list.json b/dev/api/responses/imports-list.json new file mode 100644 index 000000000..7451e4437 --- /dev/null +++ b/dev/api/responses/imports-list.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "id": 25, + "name": "IT Department", + "size": 618462, + "type": "book", + "created_by": 1, + "created_at": "2024-12-20T18:40:38.000000Z", + "updated_at": "2024-12-20T18:40:38.000000Z" + }, + { + "id": 27, + "name": "Clients", + "size": 15364, + "type": "chapter", + "created_by": 1, + "created_at": "2025-03-20T12:41:44.000000Z", + "updated_at": "2025-03-20T12:41:44.000000Z" + } + ], + "total": 2 +} \ No newline at end of file diff --git a/dev/api/responses/imports-read.json b/dev/api/responses/imports-read.json new file mode 100644 index 000000000..e256854d1 --- /dev/null +++ b/dev/api/responses/imports-read.json @@ -0,0 +1,51 @@ +{ + "id": 25, + "name": "IT Department", + "path": "uploads\/files\/imports\/7YOpZ6sGIEbYdRFL.zip", + "size": 618462, + "type": "book", + "created_by": 1, + "created_at": "2024-12-20T18:40:38.000000Z", + "updated_at": "2024-12-20T18:40:38.000000Z", + "details": { + "id": 4, + "name": "IT Department", + "chapters": [ + { + "id": 3, + "name": "Server Systems", + "priority": 1, + "pages": [ + { + "id": 22, + "name": "prod-aws-stonehawk", + "priority": 0, + "attachments": [], + "images": [], + "tags": [] + } + ], + "tags": [] + } + ], + "pages": [ + { + "id": 23, + "name": "Member Onboarding Guide", + "priority": 0, + "attachments": [], + "images": [], + "tags": [] + }, + { + "id": 25, + "name": "IT Holiday Party Event", + "priority": 2, + "attachments": [], + "images": [], + "tags": [] + } + ], + "tags": [] + } +} \ No newline at end of file diff --git a/dev/api/responses/imports-run.json b/dev/api/responses/imports-run.json new file mode 100644 index 000000000..90b34d6aa --- /dev/null +++ b/dev/api/responses/imports-run.json @@ -0,0 +1,14 @@ +{ + "id": 1067, + "book_id": 28, + "slug": "pension-providers", + "name": "Pension Providers", + "description": "Details on the various pension providers that are available", + "priority": 7, + "created_at": "2025-07-18T14:53:35.000000Z", + "updated_at": "2025-07-18T14:53:36.000000Z", + "created_by": 1, + "updated_by": 1, + "owned_by": 1, + "default_template_id": null +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 98af4bb26..99df24aed 100644 --- a/routes/api.php +++ b/routes/api.php @@ -89,7 +89,7 @@ Route::put('roles/{id}', [RoleApiController::class, 'update']); Route::delete('roles/{id}', [RoleApiController::class, 'delete']); Route::get('imports', [ExportControllers\ImportApiController::class, 'list']); -Route::post('imports', [ExportControllers\ImportApiController::class, 'upload']); +Route::post('imports', [ExportControllers\ImportApiController::class, 'create']); Route::get('imports/{id}', [ExportControllers\ImportApiController::class, 'read']); Route::post('imports/{id}', [ExportControllers\ImportApiController::class, 'run']); Route::delete('imports/{id}', [ExportControllers\ImportApiController::class, 'delete']); diff --git a/tests/Api/ImportsApiTest.php b/tests/Api/ImportsApiTest.php index 523034324..f6df074ee 100644 --- a/tests/Api/ImportsApiTest.php +++ b/tests/Api/ImportsApiTest.php @@ -14,7 +14,7 @@ class ImportsApiTest extends TestCase protected string $baseEndpoint = '/api/imports'; - public function test_upload_and_run(): void + public function test_create_and_run(): void { $book = $this->entities->book(); $zip = ZipTestHelper::zipUploadFromData([ @@ -44,12 +44,13 @@ class ImportsApiTest extends TestCase 'name' => 'My API import page', 'book_id' => $book->id, ]); + $resp->assertJsonMissingPath('book'); $page = Page::query()->where('name', '=', 'My API import page')->first(); $this->assertEquals('My api tag', $page->tags()->first()->name); } - public function test_upload_validation_error(): void + public function test_create_validation_error(): void { $zip = ZipTestHelper::zipUploadFromData([ 'page' => [