# Files, backups and proxies

> File fields on disk or S3, protected files behind short tokens, backups made while the app writes, restores, and the real client address behind a proxy.

## File fields

```ts
orders: base({
    fields: {
        scan: file({ maxSize: 10 << 20, mimeTypes: ['application/pdf', 'image/png'] }),
        docs: file({ maxSelect: 5, protected: true }),
    },
}),
```

- A file field holds stored names: `string`, or `string[]` when `maxSelect > 1`. The default size limit is 5 MB.
- Names are PocketBase's: `<snake base>_<10 random chars>.<ext>`.
- A write sends files in the data and the client makes it multipart ([Client and data layer](https://golocomotive.fr/docs/developers/client.md#files-ride-in-the-data)). `docs+` appends, `docs-` removes by stored name, `scan: ''` clears.
- Files are written before the commit and removed if it rolls back. Replaced and removed files are deleted after the commit.
- The schema form shows the stored files as links, with a remove button and a picker.

## Protected files

A `protected: true` field is served only with a file token, and only to a caller the record's `viewRule` lets through.

```ts
const token = await client.fileToken()                         // POST /api/files/token, valid 180 s
const href = client.fileUrl(order, order.docs[0] ?? '', { token })
```

`GET /api/files/:collection/:record/:filename` serves the bytes under a sandboxing content security policy. `?download=1` asks for an attachment. A plain file needs no token.

## Files on S3

```ts
import { createApp, s3Storage } from 'locosdk/server'

const app = createApp({
    schema,
    storage: s3Storage({
        bucket: 'orders-files',
        region: 'fra1',
        endpoint: 'https://fra1.digitaloceanspaces.com',
        accessKey: process.env.S3_KEY ?? '',
        secret: process.env.S3_SECRET ?? '',
        forcePathStyle: false,
        prefix: 'orders',
    }),
})
```

- A file lives at `<collectionId>/<recordId>/<filename>`, PocketBase's key, so a bucket PocketBase filled serves as is.
- `prefix` puts every key under `<prefix>/`: one bucket for several apps.
- `forcePathStyle: false` puts the bucket in the host (DigitalOcean Spaces, AWS). `true` puts it in the path (MinIO).
- The file route streams the object through the app, protected files included. It uses Bun's own S3 client, no dependency.

## Backups

A backup is PocketBase's zip of the data folder: `app.db`, then the uploaded files on disk storage, the review media and the rest. It never holds the backups themselves or the S3 secret.

```ts
const app = createApp({
    schema,
    backups: { cron: '0 3 * * *', maxKeep: 7 },                  // on disk, in data/backups
})
```

- The database is copied with `VACUUM INTO` while the write lock is held, and disk files are hard linked under the same lock. The zip is one consistent moment, even while the app writes.
- On S3 file storage, the files stay in their bucket and only the database is zipped.
- `backups.storage` is `'disk'` or an S3 config. Give backups their own bucket, or a `prefix`.
- The cron writes `@auto_pb_backup_<app>_<stamp>.zip` and keeps the newest `maxKeep` (0 keeps all).
- One backup or restore runs at a time. A second one gets 400 `Try again later`.
- Hooks: `onBackupCreate` can rename the zip and leave entries out, `onBackupRestore` can keep entries in place.

### Restore

A restore stages first, with the app still serving. It extracts the zip apart, checks every entry and opens the staged database. Only then the app stops, saves what it replaces as `@pre_restore_<app>_<stamp>.zip`, swaps the folder entry by entry, and restarts. A failure before the swap leaves the data as it was.

The process ends on purpose: the hooks and crons of `server.ts` belong to the old database. The host starts the app again on the restored data in about one second. A PocketBase zip restores too: its `data.db` becomes `app.db`.

### Routes and cli

| | what |
|---|---|
| `GET /api/backups` | the list: key, size, modified. Superuser only |
| `POST /api/backups` | make one now, optional `name` |
| `POST /api/backups/upload` | upload a zip, streamed, no size limit |
| `GET /api/backups/:key?token=` | download, with a superuser's file token |
| `DELETE /api/backups/:key` | delete one |
| `POST /api/backups/:key/restore` | restore, then restart |
| `golo backup create [name]` | the same from the app folder, the app running or not |
| `golo backup list [--json]` | the store the app last started with |
| `golo backup restore <key>` | refuses while the app runs: stop it first |

The cli runs no app code, so no backup hook fires from it.

## Behind a proxy

Without help, an app behind a proxy sees every request come from the proxy. The auth rate limit then sees one client for everybody.

```ts
const app = createApp({ schema, trustedProxy: { headers: ['X-Forwarded-For'] } })
```

For each listed header the request carries, the rightmost valid IP is `e.realIp` (the leftmost with `useLeftmostIP: true`). The rate limit keys on `realIp`. `e.remoteIp` stays the socket's address. The setting is off by default, like PocketBase. The app template turns it on for the header our front door sets.

## Not here yet

> **Planned.** Image thumbs (`?thumb=`), the backup failure mail to every superuser (a failure is logged only), and an IP filter on the backup download.
