CodamAIDocs
Topicdone

Uploading

The two ways multipart and Base64, the matching endpoints, and how file part and file object find each other by name.

Variants
POST /create/uploadPUT /update/{id}/uploadPATCH /update/{id}/uploadBase64 in the JSONseveral files in one requestname does not match → 400duplicate names → rejected

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:

PartContentCount
datathe JSON with data and response, as for a normal create or update, with content type application/jsonexactly one
filesone file; its file name must match a name in dataany number, each with a different name
Create an attachment with a file
Request
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--
Response
{ "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:

Multipart from the browser
Request
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 } });
Important
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}:

The same file as Base64
Request
POST /api/rest/fileasset/create
{ "data": { "name": "note.txt", "title": "Note",
            "content": "SGFsbG8gV2VsdA==" },
  "response": ["id", "name", "fileSize"] }
Response
{ "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.

Multipart or Base64?
Multipart
/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
Base64
normal JSON endpoints
  • 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:

Dossier with two attachments
Request
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)
Response
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

  1. 1
    Client→CDMS
    sends data and the file parts
  2. 2
    CDMS
    checks the file parts: each has a name, no name occurs twice
  3. 3
    CDMS
    stores each part as a temporary file, likewise each content from the JSON
  4. 4
    CDMS
    walks through data; for each file object: check the role, look up the file part by name
  5. 5
    CDMS→File storage
    prepares the content and sets fileId, fileSize, mimeType, fileVersion
  6. 6
    CDMS→Database
    hooks, 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

File parts and file objects
SituationOperationResult
part matches the name of a file objectallcontent is stored
new file object, no matching partcreate400 file-part-missing|<name>
new file object without namecreate400 file-name-missing
existing file object, no matching partreplace, changecontent stays as it is
two parts with the same file nameall400 duplicate-filenames|<name>
part without a file nameall400 missing-filename
part that matches no file objectall400 file-part-unused|<names>

Pitfalls

What comes next

Sources in the code and the knowledge base
  • CDMS/cdms-generator – ApiProcessor (createMultipart, updateMultipart, patchMultipart: @RequestPart data, files), ApiHubProcessor, RestPayloadProcessor (name, content)
  • CDMS/cdms-rest-api – AbstractRestApi.getFileMap (missing-filename, duplicate-filenames), addFilesFromBase64/tryWriteTempFile (content, data: prefix), FileUploadException (413, size only)
  • CDMS/cdms-system-layer – AbstractLayer.recursivePrepare (file-part-missing, file-name-missing, saveFile), recursivePatch (file branch)
  • CDMS/cdms-integrationtest – AbstractRecursiveFileDeleteTest (company/create/upload with logo.png, dossier with two attachments), AbstractFileRollbackTest
Search