feat(12.2-02): add file removal, caption, reorder and protected downloads

- DELETE, PUT and POST reorder under .../{id}/files/{field}, each scoped by one parent query (404 for a foreign file)
- protected download and thumb routes: is_public=false only, nosniff, private no-store, sandbox CSP, inline only for jpeg/png/gif/webp
- the save applies deferred removals, replaces attachOne files and rechecks maxFiles and required
- blobs of deleted files are removed after commit
- swagger2openapi emits binary content for file responses
- admin OpenAPI, TS types, conformance, README and attachments docs
This commit is contained in:
Jakub Zych
2026-10-02 18:11:56 +02:00
parent 044e0450ef
commit e54fd257ee
17 changed files with 2237 additions and 82 deletions

View File

@@ -29,6 +29,17 @@
],
"type": "object"
},
"cabana.AdminFileCaptionRequest": {
"properties": {
"description": {
"type": "string"
},
"title": {
"type": "string"
}
},
"type": "object"
},
"cabana.AdminIDsRequest": {
"properties": {
"ids": {
@@ -353,6 +364,21 @@
],
"type": "object"
},
"cabana.Envelope-cabana_FileMutationResult": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.FileMutationResult"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-cabana_FormView": {
"properties": {
"data": {
@@ -538,6 +564,17 @@
],
"type": "object"
},
"cabana.FileMutationResult": {
"properties": {
"removed": {
"type": "integer"
}
},
"required": [
"removed"
],
"type": "object"
},
"cabana.FilterOption": {
"properties": {
"label": {
@@ -3923,6 +3960,690 @@
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/files/{field}/reorder": {
"post": {
"description": "ids must be exactly the field's visible files (attached minus pending removals plus pending uploads); they receive the existing sort_order values in the submitted order, at once. attachMany only, otherwise 403.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Form session key; needed for pending uploads",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.AdminIDsRequest"
}
}
},
"description": "File ids in the new order",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-array_cabana_FileItem"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"413": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Request Entity Too Large"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Reorder the files of a field",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}": {
"delete": {
"description": "Removing an attached file is deferred to the record's next save with the same X-Session-Key; removing a pending upload deletes it at once.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "File id",
"in": "path",
"name": "file",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "Form session key",
"in": "header",
"name": "X-Session-Key",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_FileMutationResult"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Remove a file",
"tags": [
"admin"
]
},
"put": {
"description": "Saves at once (not deferred). The field must declare useCaption, otherwise 403. Omitted keys are left unchanged; unknown keys are refused.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "File id",
"in": "path",
"name": "file",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "Form session key; needed for a pending upload",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.AdminFileCaptionRequest"
}
}
},
"description": "Title and description",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_FileItem"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"413": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Request Entity Too Large"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Save a file's title and description",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}/download": {
"get": {
"description": "Streams a file of a protected (Public false) relation that belongs to a record the admin may load, or is pending in the admin's own session. Public files are 404. JPEG, PNG, GIF and WebP are served inline with their type; everything else as an application/octet-stream attachment. Responses carry X-Content-Type-Options nosniff, Cache-Control private, no-store and a sandboxing Content-Security-Policy.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "File id",
"in": "path",
"name": "file",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "Form session key; needed for a pending upload",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/octet-stream": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Download a protected file",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}/thumb": {
"get": {
"description": "The preview thumbnail (imageWidth by imageHeight, 240 by 240 by default, in thumbOptions.mode) of a protected image file, scoped like the download route. A file that is not a JPEG, PNG, GIF or WebP image is 404.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "File id",
"in": "path",
"name": "file",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "Form session key; needed for a pending upload",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/octet-stream": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Thumbnail of a protected image",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}": {
"get": {
"parameters": [