Storage Management
Storage limits and management in TMA Cloud.
Storage limits and management in TMA Cloud.
Storage Driver
- S3: Files stored in S3-compatible object storage. Object keys stored in database.
- The first user connects the bucket in Settings → Storage (see Storage Bucket). The backend starts without one; until it is saved, file content requests return
503 STORAGE_NOT_CONFIGURED. - Each API and worker process keeps the decrypted setting in memory only and re-reads it from the database every 15 seconds, so a change applies without a restart. A new setting gets a new S3 client; the previous client is closed after 5 minutes so transfers already running on it can finish.
- Uploads stream directly to the bucket with encryption. Downloads, copies, deletion, and sharing also use bucket storage. Bulk deletion uses the S3 multi-object API in batches of up to 1,000 keys, up to eight batches at once.
- The driver uses operations supported by AWS S3 and Cloudflare R2:
PutObject,GetObject,HeadObject,ListObjectsV2,DeleteObject,DeleteObjects,CopyObject, and multipart upload and copy operations. - A single upload or copy request is limited to 5,000,000,000 bytes. Larger objects use multipart operations, which is the lower portable boundary across AWS S3 and R2.
Storage Limits
Per-User Limits
- Configurable storage limits per user
- Without a limit an account is unlimited, and Settings shows its usage with No storage limit. With one it shows how much of the limit is used, the percentage, and the space left
- Set by the administrator; usage updates with every write
Storage Calculation
- Files: The account owner's
users.storage_usedcounter tracks the sum of all file sizes - Folders: Counted as 0 bytes
- Trash: Counted until permanently deleted
Storage Usage
Tracking
- Reads are constant-time from
users.storage_used; database triggers update the counter once per statement for inserts and deletes, and per row for the updates that change a file's size, type or owner - Upload and replacement transactions lock the account row before checking quota. Long object-store copies reserve their bytes first and release the reservation in the metadata transaction, so concurrent API instances cannot approve the same free space twice.
- The storage API reports
totalas the account's limit andfreeas what is left, bothnullwithout a limit
Monitoring
- The usage bar turns amber at 80% of the limit and red at 95%
- Uploads and saves that would pass the limit are refused with the space used, the limit and what is left
Per-User Access (Strict DB Permissions)
File access is enforced with strict database permissions so that User A cannot download or access User B's files, even if they guess a file ID.
- Main file API: Every file operation (download, rename, move, and so on) looks the row up by both its id and the caller's account (
user_id), and skips trashed rows. Only the account that owns a file sees it. - OnlyOffice: The signed link and the document key name the account. The file-serving and callback endpoints only serve or save a file whose row matches both the file id and that account.
- Share links: Shared-item download requires a valid share token and returns the file only if it is in that share (join with
share_link_files), so access is scoped to the share.
So guessing or enumerating file IDs does not give access to another user's data.
File Encryption
Files are automatically encrypted. Encryption uses AES-256-GCM in Google Tink's AES-GCM-HKDF-STREAMING format (AES256_GCM_HKDF_1MB), which splits each object into 1 MB segments, each with its own authentication tag. Each newly encrypted object version receives a random data key (DEK); FILE_ENCRYPTION_KEY is the key-encryption key (KEK) that wraps the DEK, and the wrapped DEK is stored on the file's database row.
Behavior
- Scope: File contents in the S3-compatible bucket are encrypted
- Transparent: Encryption and decryption happen automatically
- Streaming: Files pass through in streams. Upload buffers share a server-wide budget (
UPLOAD_MEMORY_MB, 512 MB by default), so many large uploads at once wait their turn instead of exhausting memory - Seekable: Because segments are independent, a single-file download can serve an HTTP
Rangerequest by decrypting only the overlapping segments
File Operations
- Read/Write: All file operations use streaming (S3 object keys)
- Upload: Files stream from the client through encryption to storage. A known object over 5,000,000,000 bytes uses multipart upload. For an unknown-length encrypted stream, the part size is calculated from the configured maximum upload size.
- Download: Files streamed from storage to client (S3: GetObject stream); supports HTTP
Rangefor partial reads - Copy: Copy runs on the background worker. The object store copies ciphertext directly. Objects up to 5,000,000,000 bytes use one copy request; larger objects use multipart copy. Part size grows with object size so the copy stays within 10,000 parts. A logical copy reuses the source object's wrapped DEK because its ciphertext is unchanged.
- Deep copies: The recursive plan is kept in a PostgreSQL temporary table. The application reads one cursor page at a time and runs up to 24 server-side copies at once; no file data passes through TMA Cloud.
- Share: Share link downloads use the same bucket object keys and streaming decryption
- Folder ZIP: Authenticated and public-share folder archives read database cursor pages and append one decrypted file stream at a time.
- OnlyOffice save: A callback writes to a new object key, rechecks replacement quota under row locks, and commits the metadata swap. The old object is then queued for deletion. A rejected or failed save leaves the old version intact and queues any unused new object for deletion.
Key Configuration
- Set
FILE_ENCRYPTION_KEY— the key-encryption key (KEK) that wraps each file's data key and the stored bucket secret. After a rotation it is a keyring ofversion:keyentries; the highest version is the current KEK - Generate a key:
tma-server key generate, oropenssl rand -base64 32 - Production requires a random 32-byte key (base64 or hex). A passphrase stops startup, because PBKDF2 with a fixed salt is only as strong as the passphrase
- Outside production a passphrase is accepted, and a development fallback key is used if none is set
- The key can be read from a file with
FILE_ENCRYPTION_KEY_FILE(Docker or Kubernetes secrets) - The KEK is never stored in the database. A database backup holds only wrapped keys, which cannot be opened without it
- At startup the API and worker check every key against the
kek_checkstable and refuse to start on a mismatch, or when stored data uses a version the keyring lacks
Key Rotation
- Rotating the KEK only rewraps each file's stored DEK and the stored bucket secret; the encrypted objects are never read or rewritten
./rotate.sh key(Docker) ortma-server rotate keyadds the next key version and keeps the older ones, which database backups from before the rotation still need. The worker rewraps the stored keys when it starts. See Key Rotation- Rotation is also how a deployment moves from a passphrase to a random key
Storage Operations
Upload Limits
- Maximum upload size: Per-file size limit, configurable by admin in Settings → Storage (default 10 GB). Stored in
app_settings.max_upload_size_bytes. Checked by the app before an upload starts and enforced by the server during the upload. - Multipart upload sizing uses the maximum encrypted size derived from this setting. The default multipart part size is 16 MiB, parts are equal-sized except for the last, and the plan may not exceed 10,000 parts.
- Checked before the upload from its declared size, and again while the bytes arrive
- A refused upload gets its reason (limit, quota, permission) rather than a dropped connection
- Stored bytes of a refused upload are deleted
Cleanup
- Trash cleanup frees space
- Automatic background processes handle trash, expired share links, audit logs, old sessions, expired quota reservations, expired file-operation results, leftover bulk-import manifests, change-feed entries older than a week, and a weekly recheck of stored folder totals
- Failed uploads, replacements, and OnlyOffice saves queue unused storage objects for retryable deletion
- Orphaned files are not removed automatically; an admin reviews and deletes them from Settings → Administration → Maintenance → Review orphans
- S3: Upload validation (e.g.
parentId) runs after stream upload. Failed bulk-upload and abandoned stream objects are queued for deletion. A process crash can still leave an orphan; review them periodically.
Orphans
An orphan is either an object in storage that no files row points at, or a files row whose stored object is missing. Both are reported by an on-demand scan.
- Scanning is read-only and makes a single paged pass over the bucket. Each storage page is staged in a temporary PostgreSQL table, so application memory is bounded to one storage page plus the capped report.
- Scan and delete work runs on the standalone worker. The API queues the job and the admin client polls its status.
- A grace window (1 hour minimum, 24 hours default, 1 year maximum) hides anything younger on either side, so an upload or paste still in progress is never reported.
- Row age comes from
files.created_at, notfiles.modified, because uploads and copies preserve the client's original mtime. - Deletion is itemised. Every entry is re-verified against the database and the storage timestamp at the moment of deletion, so a stale scan cannot remove a live file.
- Trashed rows still own their objects and count as referenced.
See Orphan Review for the admin workflow.
S3 Bucket Protection
tma-server s3 commands apply bucket settings to the bucket saved in Settings → Storage. They read it from the database, so .env must point at the database and hold the same FILE_ENCRYPTION_KEY as the app. On Docker, run them in the app container, for example docker compose exec app tma-server s3 protect-all.
Apply All Protections
tma-server s3 protect-allApplies in one run: block public access; bucket policy that denies HTTP (HTTPS only); versioning; default SSE if supported; lifecycle rules.
Lifecycle Rules
- Abort incomplete multipart uploads after 1 day (no effect on completed objects).
- Delete noncurrent versions after 7 days (versioning cleanup).
- Remove expired delete markers (tombstones from deletes on versioned bucket).
tma-server s3 lifecycle applies the lifecycle rules, and tma-server s3 check-lifecycle shows the rules the bucket has. See CLI Commands.
Individual Commands
| Command | Effect |
|---|---|
tma-server s3 protect-all | All of the below in one run |
tma-server s3 lifecycle | Lifecycle: abort incomplete multipart + delete old versions and delete markers |
tma-server s3 policy-https | Bucket policy: deny HTTP (HTTPS only) |
tma-server s3 public-block | Block public access (private bucket) |
tma-server s3 versioning | Enable versioning |
tma-server s3 encryption | Enable default SSE (AES256); not supported by all S3-compatible stores |
PutBucketPolicy replaces the entire bucket policy. If you add other policy statements in the storage UI, re-running s3 policy-https or s3 protect-all overwrites them.
Related Topics
- File System - How files are stored
- Admin Guide: Storage Limits - Configure limits
- CLI Commands - S3 scripts reference
Last updated on