
# File Operations

Every bucket exposes a REST API for the core file lifecycle: upload, download, and delete. Operations are authenticated against the bucket's [permissions](/en/docs/buckets-overview#permissions), hardened against path-traversal and XSS, and integrated with Sovrium's [soft-delete pattern](/en/docs/tables-overview).

The `{bucket}` path parameter references a bucket name declared in your `buckets[]` array. See [Buckets Overview](/en/docs/buckets-overview).

## Endpoints

| Method   | Endpoint                            | Description                                                                       |
| -------- | ----------------------------------- | --------------------------------------------------------------------------------- |
| `POST`   | `/api/buckets/{bucket}/files`       | Upload a file (multipart form data). Returns `201` with the generated file key.   |
| `GET`    | `/api/buckets/{bucket}/files/{key}` | Download a file by key. Returns the file content with the correct `Content-Type`. |
| `DELETE` | `/api/buckets/{bucket}/files/{key}` | Delete a file by key from storage.                                                |
| `GET`    | `/api/admin/buckets/quota`          | _(Admin)_ Return current storage usage in bytes.                                  |

## Upload

Send the file as multipart form data. The server validates it against the bucket's `maxFileSize` and `allowedMimeTypes`, generates an unpredictable key, and stores it in the configured backend.

```bash
curl -X POST https://app.example.com/api/buckets/documents/files \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@report.pdf"
```

```json
{
  "key": "documents/9f3c1e2a-7b44-4d10-9e21-8a6f0c1d2e3b.pdf",
  "filename": "report.pdf",
  "size": 245760,
  "mimeType": "application/pdf"
}
```

| Behavior                                           | Result                                                                  |
| -------------------------------------------------- | ----------------------------------------------------------------------- |
| Valid file within limits                           | `201` with the generated file key.                                      |
| Empty request body                                 | `400 Bad Request`.                                                      |
| Exceeds bucket or global max size                  | `413 Payload Too Large` (rejected early, before the full body is read). |
| Exceeds `STORAGE_MAX_TOTAL_SIZE` quota             | `507 Insufficient Storage`.                                             |
| Unauthenticated request to an authenticated bucket | `401 Unauthorized`.                                                     |

:::callout
**Keys are randomized.** Uploads always receive a UUID-based key, so files never overwrite one another and storage URLs are unguessable. Two uploads of the same filename produce two distinct keys.
:::

## Download

```bash
curl https://app.example.com/api/buckets/documents/files/{key} \
  -H "Authorization: Bearer $TOKEN" -O
```

| Behavior                                                        | Result                                              |
| --------------------------------------------------------------- | --------------------------------------------------- |
| File exists, caller has `download` permission                   | `200` with file content and correct `Content-Type`. |
| Non-existent key                                                | `404 Not Found`.                                    |
| Private file, no auth and no [signed URL](/en/docs/signed-urls) | `403 Forbidden`.                                    |

Responses include a `Content-Disposition` header — non-image files are served as `attachment` to prevent the browser from rendering them inline.

## Delete

```bash
curl -X DELETE https://app.example.com/api/buckets/documents/files/{key} \
  -H "Authorization: Bearer $TOKEN"
```

| Behavior                                    | Result                            |
| ------------------------------------------- | --------------------------------- |
| File exists, caller has `delete` permission | `200`, file removed from storage. |
| Non-existent key                            | `404 Not Found`.                  |

## Security Hardening

File operations apply the recurring [pre-launch security baseline](/en/docs/buckets-overview) for uploads automatically:

| Measure                           | Behavior                                                                  |
| --------------------------------- | ------------------------------------------------------------------------- |
| Path traversal                    | `../` sequences in filenames are sanitized or rejected.                   |
| Null bytes                        | Filenames containing null bytes are rejected.                             |
| Special / unicode characters      | Encoded safely for storage.                                               |
| Randomized keys                   | UUID-based keys prevent guessable URLs.                                   |
| `X-Content-Type-Options: nosniff` | Sent on every served file.                                                |
| `Content-Security-Policy`         | Sent on every served file.                                                |
| XSS-prone types (HTML, SVG)       | Served with a safe `Content-Type` or forced to download as an attachment. |

## File Lifecycle

Attachments follow Sovrium's soft-delete-by-default lifecycle. File presence tracks the owning record:

| Action on the record          | File behavior                                        |
| ----------------------------- | ---------------------------------------------------- |
| Soft-delete record            | File preserved in storage.                           |
| Restore record                | File access restored.                                |
| Hard-delete (purge)           | File removed from storage.                           |
| Replace attachment            | Old file removed from storage.                       |
| Clear attachment (set `null`) | File removed from storage.                           |
| Shared reference              | File preserved while any other record references it. |

## Large Uploads & Quota

Uploads stream rather than buffering wholesale, so large files do not spike memory. Limits are operator-controlled via environment variables.

| Variable                 | Default              | Purpose                                                                                |
| ------------------------ | -------------------- | -------------------------------------------------------------------------------------- |
| `STORAGE_MAX_FILE_SIZE`  | `104857600` (100 MB) | Maximum single file size in bytes. A bucket's `maxFileSize` overrides this per-bucket. |
| `STORAGE_MAX_TOTAL_SIZE` | Unlimited            | Maximum total storage per application in bytes (multi-tenant safety).                  |

```bash
STORAGE_MAX_FILE_SIZE=52428800
STORAGE_MAX_TOTAL_SIZE=10737418240
```

The admin quota endpoint reports usage:

```bash
curl https://app.example.com/api/admin/buckets/quota \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

```json
{ "usedBytes": 524288000, "maxBytes": 10737418240 }
```

Deleting files reduces tracked usage. When no `STORAGE_MAX_TOTAL_SIZE` is configured, uploads are unrestricted.

## Related Pages

- [Buckets Overview](/en/docs/buckets-overview) — declaring buckets, backends, and permissions.
- [Signed URLs](/en/docs/signed-urls) — time-limited download and direct browser uploads.
- [Image Transforms](/en/docs/image-transforms) — resizing and reformatting on download.
- [Attachment Fields](/en/docs/attachment-fields) — files attached to table records.
- [Form File Uploads](/en/docs/form-file-uploads) — accepting uploads through forms.
- [Environment Variables](/en/docs/env-vars) — size and quota configuration.
