#28020·directus

fix(specs): document POST /assets/folder/{pk} and POST /assets/files/

Author: kheinerCreated Aug 1, 2026Updated Sep 15, 2026
LabelsBugIntegrationsOpenAPILow ReachLow Impact

Part of the spec drift audit tracked in directus/directus#27700.

Scope

packages/specs/src/paths/assets/ only documents GET /assets/{id}. Two POST routes on the same controller that return a zip archive have no spec coverage at all: bundling them into one PR is reasonable since they share the same response shape and controller.

Routes to document

api/src/controllers/assets.ts

  • POST /assets/folder/:pk: archives every file within folder pk (recursively) into a zip and streams it back. No request body. Response is application/zip with a Content-Disposition: attachment header naming the file folder-<folder name>-<timestamp>.zip.
  • POST /assets/files/: archives an arbitrary set of files into a zip and streams it back. Request body is { "ids": string[] } (UUIDs, min length 1; validated with zod, rejects with InvalidPayloadError on a bad shape). Response is application/zip with a Content-Disposition: attachment header naming the file files-<timestamp>.zip.

Both routes run through checkIsLocked('assets') and useCollection('directus_files'), and both construct an AssetsService with the caller's own accountability - access to the underlying files is governed by normal RBAC read permissions on directus_files (and, for the folder route, directus_folders), not a hardcoded check. This is a documentation gap only, not a security/RBAC bug.

Implementation notes

Add a new path file, e.g. packages/specs/src/paths/assets/folder.yaml and packages/specs/src/paths/assets/files.yaml, registered in openapi.yaml as /assets/folder/{pk} and /assets/files. Both should use the Assets tag.

  • POST /assets/files/: set x-collection: directus_files, following the operation-level override pattern established for GET /assets/{id} in directus/directus#27883.
  • POST /assets/folder/{pk}: set x-collection: directus_folders. FoldersService.buildTree() calls validateAccess on the root folder id and throws ForbiddenError if directus_folders:read is missing - that's the only hard gate on the route. The files themselves are filtered silently by directus_files read permissions (missing access just omits files from the zip rather than blocking the request), so directus_folders is the correct single collection to gate on.

Response schema: application/zip, type: string, format: binary (there's no existing zip response schema to reuse; the closest precedent is the text/plain type: string response already used for GET /assets/{id}).

For POST /assets/files/: document the ids request body per the zod schema in the controller (array of string, uuid, min 1), and the InvalidPayloadError (400) response for a malformed body.

Verification

Run pnpm validate in packages/specs and confirm it passes clean. Verify in a Swagger viewer that both new path entries appear under the Assets tag with correct request/response documentation.