What this is about
A file reaches CDMS in one of two ways:
- Multipart: the request consists of several parts. One part is the JSON, the others are the files. This is the normal way, especially for larger files.
- Base64: the file is text inside the JSON itself. This is convenient for small files, for example from a script, but it is about a third larger than the file.
The same rule applies to both: the file part and the file object in data find each other by name.
Multipart
The /upload endpoints expect multipart/form-data with two kinds of parts:
| Part | Content | Count |
|---|---|---|
data | the JSON with data and response, as for a normal create or update, with content type application/json | exactly one |
files | one file; its file name must match a name in data | any number, each with a different name |
POST /api/rest/fileasset/create/upload
Content-Type: multipart/form-data; boundary=XYZ
--XYZ
Content-Disposition: form-data; name="data"
Content-Type: application/json
{ "data": { "name": "Contract.pdf", "title": "Framework contract" },
"response": ["id", "name", "mimeType", "fileSize"] }
--XYZ
Content-Disposition: form-data; name="files"; filename="Contract.pdf"
Content-Type: application/pdf
…bytes of the file…
--XYZ--{ "data": { "id": "f1…", "name": "Contract.pdf",
"mimeType": "application/pdf", "fileSize": 48213 },
"meta": { "error": false } }The server has set mimeType and fileSize. It does not evaluate the Content-Type of the file part; the file type comes from the extension of the name.
In the browser you build such a request with FormData:
const form = new FormData();
form.append("data", new Blob([JSON.stringify({
data: { name: file.name, title: "Framework contract" },
response: ["id", "name"]
})], { type: "application/json" }));
form.append("files", file, file.name);
fetch("/api/rest/fileasset/create/upload",
{ method: "POST", body: form, headers: { Authorization: "Bearer " + token } });append data as a Blob with type application/json,
the file under the part name "files",
and do NOT set the request's Content-Type yourself:
the browser adds the boundary.Base64 in the JSON
Instead of a file part, you write the content into the field content of the file object. This works on the normal JSON endpoints POST /create, PUT and PATCH /update/{id}:
POST /api/rest/fileasset/create
{ "data": { "name": "note.txt", "title": "Note",
"content": "SGFsbG8gV2VsdA==" },
"response": ["id", "name", "fileSize"] }{ "data": { "id": "f2…", "name": "note.txt", "fileSize": 10 },
"meta": { "error": false } }content may also come as a data URL, that is with a prefix such as data:text/plain;base64,. CDMS cuts off the prefix. content is only read for the upload and is never stored or returned.
/upload endpoints- bytes unchanged, no conversion
- good for large files and forms with a file picker
- 25 MB per file, 500 MB per request, see Size limits
- everything in one JSON, easy from scripts
- about a third larger
- invalid Base64 is skipped, then the file is missing
- the same size limits as multipart
Several files and nested files
A request can carry several files, including for file models that hang as children on another object. Each file finds its object by name, no matter how deep it sits in data:
POST /api/rest/dossier/create/upload
data: { "data": { "title": "Dossier 1",
"attachments": [ { "name": "first.txt" },
{ "name": "second.txt" } ] },
"response": ["id", { "field": "attachments", "response": ["+"] }] }
files: first.txt (file part)
files: second.txt (file part)The dossier and both attachments are created,
each attachment with its content.For this, models without a file of their own, like the dossier here, also get the /upload variants. As always, the child needs the CREATE flag on the relation. See The four cases in nested writing.
The flow
-
1Client→CDMSsends
dataand the file parts -
2CDMSchecks the file parts: each has a name, no name occurs twice
-
3CDMSstores each part as a temporary file, likewise each
contentfrom the JSON -
4CDMSwalks through
data; for each file object: check the role, look up the file part byname -
5CDMS→File storageprepares the content and sets
fileId,fileSize,mimeType,fileVersion -
6CDMS→Databasehooks, validation, save the record, read back, commit
The content only becomes current once the database has committed. If the request fails before that, it is discarded. More on this in Files and transaction.
When it does not match
| Situation | Operation | Result |
|---|---|---|
part matches the name of a file object | all | content is stored |
| new file object, no matching part | create | 400 file-part-missing|<name> |
new file object without name | create | 400 file-name-missing |
| existing file object, no matching part | replace, change | content stays as it is |
| two parts with the same file name | all | 400 duplicate-filenames|<name> |
| part without a file name | all | 400 missing-filename |
| part that matches no file object | all | 400 file-part-unused|<names> |
Pitfalls
What comes next
- Swapping the content: Replacing and renaming
- How large a file may be: Size limits
- Fetching the file again: Downloading