What this is about
The content of every file lives as a file of its own in the file storage, usually a directory on a volume. The path to it is built in a fixed way. The separation of tenants follows from it too: every tenant has its own subdirectory.
The path
<basePath>/[<tenant>/]<model path>/<fileId>/files/t1/fileasset/910e2c19-d5d1-4dae-8229-ea5fcbcc6194| Part | from | Example |
|---|---|---|
basePath | configuration codamai.cdms.persistence.file.basePath | /files/ |
| tenant | tenant of the request, only when needed (see below) | t1 |
| model path | from the API path of the model, fixed at generation | fileasset/, company/logo/ |
fileId | UUID that the server assigns at the first save | 910e2c19-… |
No part of the path comes directly from the user. The display name Contract.pdf is only in the record. CDMS checks the tenant before it goes into the path: letters, digits, ., _ and - are allowed, but no ...
When the tenant is in the path
| Operating mode | Level of the model | Path |
|---|---|---|
| MULTI | tenant or user | <basePath>/<tenant>/<model>/<fileId> |
| MULTI | system | <basePath>/<model>/<fileId>, shared by all tenants |
| SINGLE | all | <basePath>/<model>/<fileId>, one shared directory |
The user level does not separate further in the storage than the tenant level. Who may see which file of a tenant is decided by the permissions and the owner filter when the record is read. See Model levels: system, tenant, user.
A directory tree
flowchart TB
B["/files/"] --> T1["t1/"]
B --> T2["t2/"]
B --> S["country-flags/ (system model)"]
T1 --> T1F["fileasset/"]
T1F --> F1["910e2c19-… (current)"]
T1F --> F1V["910e2c19-….1790000100000 (version)"]
T1 --> T1L["company/logo/"]
T1L --> L1["3b7a…"]
T2 --> T2F["fileasset/"]
T2F --> F2["5c01…"]
S --> S1["a9e2…"]
Tenant isolation
The separation lies in the path alone. A request from tenant t2 only looks under /files/t2/. Even with the fileId of a file of t1, it finds nothing there.
| Situation | Result |
|---|---|
| t1 writes, t1 reads | found |
t1 writes, t2 reads the same fileId | not found, different directory |
| tenant model, request without tenant | rejected, CDMS_FILE_TENANT_REQUIRED |
| tenant with disallowed characters | rejected, CDMS_FILE_TENANT_INVALID |
| system model, any tenant | found, shared directory |
The database has already separated before that: a request does not even find the record of a foreign file. See Tenant isolation in CDMS.
Storing atomically
An upload first arrives as a temporary file, often on a different file system than the storage. If CDMS copied it directly to the target, a concurrent download could get a half-written file. Therefore:
-
1CDMS→File storagefor audited models: renames the previous content into a version
-
2CDMS→File storagewrites the content under a private name in the target directory:
<fileId>.staging-<random>That can take a while for large files. Nobody reads this name. -
3CDMS→File storagerenames the temporary file to
<fileId>in one stepResult: There is never half-written content under<fileId>.
Directory permissions
CDMS creates new directories with the permissions rwxr-x---: the application user may do everything, its group may read, everyone else nothing. So tenant data stays closed even to other processes on the same server. A backup that runs under the same group can read. CDMS does not create the base directory itself, see Storage backends.
Pitfalls
What comes next
- Where the storage comes from: Storage backends
- Old states next to the current one: File versions
- How an upload arrives: Uploading