# Stockage de fichiers booster

> Téléverser, lister, partager par lien et supprimer les fichiers d'un panneau Booster depuis votre propre serveur avec une clé API.

Un panneau Booster peut conserver des fichiers : documents téléversés par ses utilisateurs,
factures et exports générés par un workflow, images de produits. Les endpoints de cette page
permettent à votre serveur de travailler avec ces fichiers avec une clé API : téléverser un
rapport généré, lister ce que les visiteurs ont envoyé, obtenir un lien de téléchargement, faire le ménage.

Les endpoints se trouvent sous `/api/v3/booster/storage/private/`. Les panneaux eux-mêmes
accèdent à leurs fichiers par un autre chemin d'accès, qui n'est pas décrit ici.

## Concepts

### Dossiers et chemins

Les fichiers sont désignés par un **chemin** : un **dossier** de premier niveau, des
sous-dossiers facultatifs et le nom du fichier, séparés par `/`, par exemple `invoices/2026/INV-0042.pdf`.

- Le nom du dossier peut contenir des lettres, des chiffres, `_` et `-`.
- Les noms de sous-dossiers et de fichiers peuvent contenir n'importe quel caractère sauf les
  caractères de contrôle et `/ \ : * ? " < > | # % '` ; ils ne peuvent pas valoir `.` ou `..`
  ni commencer ou se terminer par une espace.
- Un chemin a au moins deux parties (`<folder>/<name>`), au plus 8 niveaux et 512 caractères.
- Les dossiers et sous-dossiers ne sont pas créés séparément : ils existent tant qu'ils
  contiennent un fichier. Un chemin est unique au sein d'un panneau.

### Paramètres du stockage de fichiers

Le propriétaire du panneau active le stockage de fichiers et définit les dossiers dans les
paramètres des modules du panneau dans l'application. Pour chaque dossier (ou `*` pour tous
les autres), les paramètres indiquent qui peut téléverser, lire, lister, modifier et supprimer
des fichiers, quels types de fichiers il accepte, quelle taille un fichier peut avoir, combien
de fichiers il peut contenir et si les fichiers sont supprimés après un certain nombre de jours.
Un dossier peut fournir des **liens publics permanents** (pour des images affichées sur une page publique).

### Qui peut accéder aux fichiers d'un panneau

Les requêtes agissent au nom de l'utilisateur propriétaire de la clé API. Cet utilisateur doit
être propriétaire du panneau (en être le créateur, ou membre du groupe qui le possède) ou avoir
reçu le panneau en partage, directement ou via un groupe. La clé API doit porter la
[portée](authentication.md#portees) correspondante :

| Portée | Endpoints |
|---|---|
| `ayeto.booster.storage` | `list`, `count`, `get`, `url`, `usage` |
| `ayeto.booster.storage.write` | `upload`, `upload-b64`, `update`, `delete`, `delete-many` |

La portée d'écriture n'inclut pas la portée de lecture ; une clé qui lit et écrit a besoin des deux.
Les endpoints fonctionnent aussi lorsque le stockage de fichiers du panneau est désactivé, de sorte
que vous pouvez toujours lire et nettoyer les fichiers stockés.

Pour les requêtes de cette page, les paramètres des dossiers s'appliquent ainsi :

- Une opération réglée sur **none** pour un dossier est désactivée pour tout le monde, y compris pour cette API (`403`).
- Tous les autres niveaux autorisent les requêtes API, sur tous les fichiers du dossier.
- Un dossier sans paramètres (et sans entrée `*`) n'est accessible qu'à l'API et au propriétaire.
- Les types de fichiers acceptés, la limite de taille des fichiers et les limites de nombre de
  fichiers et de volume de données par dossier **s'appliquent** aux téléversements via cette API.

### Fichiers

| Champ | Type | Description |
|---|---|---|
| `id` | UUID | Identifiant du fichier. |
| `path` | chaîne | Chemin complet, par ex. `invoices/2026/INV-0042.pdf`. |
| `folder` | chaîne | Dossier de premier niveau. |
| `name` | chaîne | Nom du fichier (dernière partie du chemin). |
| `size` | entier | Taille en octets. |
| `content_type` | chaîne | Type du contenu. Les formats connus (PNG, JPEG, GIF, WebP, AVIF, PDF, audio et vidéo courants) sont reconnus à partir du contenu et l'emportent sur le type déclaré ; HTML, SVG et XML sont également reconnus. |
| `sha256` | chaîne | SHA-256 du contenu (hex). |
| `metadata` | objet | Vos données sur le fichier (objet JSON, au plus 4 Ko, les clés ne peuvent pas commencer par `$` ni contenir `.`). |
| `record` | objet ou `null` | L'enregistrement de la [base de données booster](booster-database.md) auquel le fichier appartient : `{collection, key}`. |
| `owner` | UUID ou `null` | L'utilisateur du panneau qui a téléversé le fichier ; `null` pour les fichiers téléversés via l'API. |
| `created`, `updated` | entier | Date du téléversement et de la dernière modification en millisecondes depuis l'epoch. |
| `expires_at` | chaîne ou `null` | Date de suppression du fichier, dans les dossiers qui conservent les fichiers un certain nombre de jours. |
| `url` | chaîne ou `null` | Lien qui affiche ou télécharge le fichier (voir [Liens de téléchargement](#liens-de-telechargement)). |
| `download_url` | chaîne ou `null` | Lien qui télécharge toujours le fichier. |

### Fichiers et enregistrements de la base de données

Un fichier peut appartenir à un enregistrement de la [base de données](booster-database.md) du
panneau : envoyez la collection et la clé de l'enregistrement avec le téléversement, ou
définissez-les plus tard avec `update`. L'enregistrement doit exister dans la base de données du
panneau. Par défaut, un fichier est supprimé lorsque son enregistrement est supprimé ; les
paramètres du dossier peuvent le conserver. Stockez l'`id` (ou le `path`) du fichier dans la
valeur de l'enregistrement lorsque votre panneau doit retrouver le fichier à partir de l'enregistrement.

### Liens de téléchargement

Les liens sont des URL absolues sous `/api/v3/booster/storage/file/` et ne nécessitent aucun
en-tête : ils fonctionnent donc dans `<img src>`, `<a href>` et depuis d'autres serveurs.

- Un lien est **signé** et valable 60 minutes. Demandez-en un nouveau avec
  [Obtenir des liens de téléchargement](#obtenir-des-liens-de-telechargement) au moment où vous en avez
  besoin, plutôt que de stocker des liens.
- Dans les dossiers avec des liens publics permanents, `url` n'expire jamais. Il cesse de
  fonctionner lorsque le dossier ne fournit plus de liens publics ou que le fichier est supprimé.
- Les images, l'audio, la vidéo et les PDF sont affichés dans le navigateur ; les autres types (et
  chaque `download_url`) sont téléchargés. Les fichiers HTML, SVG et XML sont toujours téléchargés.
- Les liens d'images acceptent `?w=` et `?h=` (pixels) pour une copie redimensionnée, par ex. des vignettes.
- Les liens prennent en charge les requêtes `Range` (déplacement dans l'audio et la vidéo).

### Limites

| Limite | Valeur |
|---|---|
| Taille d'un fichier | la limite du dossier, au plus 100 Mo |
| Données par panneau | la limite du panneau, au plus 1 Go par défaut |
| Métadonnées | 4 Ko de JSON |
| Fichiers renvoyés par un `list` | 100 sans pagination, jusqu'à 1000 avec `limit_from` / `limit_to` |
| `ids` dans `url` et `delete-many` | 100 |
| Téléversement en base64 | utilisez `upload` (multipart) pour les fichiers de plus de quelques Mo, le [téléversement direct](#televerser-directement-un-gros-fichier) pour les gros |

Les fichiers comptent également dans le quota de stockage du propriétaire du panneau ou, pour les
panneaux d'une organisation, dans celui de l'organisation.

## En-têtes de requête et erreurs communs

| En-tête | Description |
|---|---|
| `uni-api-key` | Votre clé API. Voir [Authentification](authentication.md). |
| `panel-id` | ID du panneau dont vous accédez aux fichiers. |
| `Content-Type` | `application/json`, ou `multipart/form-data` pour `upload`. |

Les erreurs sont du JSON de la forme `{"detail": "..."}` (voir [Erreurs](conventions.md#erreurs)).
Celles-ci peuvent provenir de tous les endpoints :

| Statut | `detail` | Cause |
|---|---|---|
| `401` | `API key not provided` / `API key is invalid` | La clé est vide, inconnue, désactivée ou n'a pas la portée de l'endpoint. |
| `403` | `permission denied` | Vous n'êtes pas propriétaire du panneau et il n'est pas partagé avec vous. |
| `403` | `operation '<operation>' is disabled for folder '<folder>'` | Les paramètres du dossier règlent cette opération sur **none**. L'opération est `upload`, `read`, `list`, `update` ou `delete`. |
| `404` | `panel not found` | Aucun panneau avec cet ID, ou le panneau est désactivé. |
| `404` | `file not found` | Aucun fichier avec cet ID ou ce chemin dans le panneau. |
| `422` | erreur de validation | L'en-tête `panel-id` est absent ou n'est pas un UUID, ou le corps ne correspond pas au modèle de requête. |
| `429` | message de limite de débit | Trop de requêtes ; voir [Limites de débit](conventions.md#limites-de-debit). |

## Téléverser un fichier

Stocke un fichier. Envoyez-le en `multipart/form-data`.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/upload` |
| Portée | `ayeto.booster.storage.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

### Requête

| Champ du formulaire | Type | Obligatoire | Description |
|---|---|---|---|
| `file` | fichier | oui | Le contenu. |
| `path` | chaîne | oui | Chemin cible. Un chemin qui se termine par `/` est un répertoire : le nom du fichier téléversé y est ajouté. |
| `metadata` | chaîne | non | Objet JSON avec vos données sur le fichier. |
| `record_collection`, `record_key` | chaîne, UUID | non | L'enregistrement de la base de données auquel le fichier appartient (les deux ou aucun). |
| `overwrite` | booléen | non | Remplacer un fichier situé au même chemin (nécessite le droit de suppression dans son dossier). `false` par défaut. |
| `rename` | booléen | non | Lorsque le chemin est pris, stocker sous `name (1).ext`, `name (2).ext`, … `false` par défaut. |

### Réponse

Le [fichier](#fichiers) stocké, avec ses liens.

### Erreurs

| Statut | `detail` | Cause |
|---|---|---|
| `409` | `a file '<path>' already exists` | Le chemin est pris et ni `overwrite` ni `rename` n'a été envoyé. |
| `422` | `path must start with a folder: '<folder>/<name>'` | Le chemin n'a qu'une seule partie. |
| `422` | `folder name can only contain letters, digits, '_' and '-'` | Nom de dossier invalide. |
| `422` | `path segments cannot …` | Nom de sous-dossier ou de fichier invalide (voir [Dossiers et chemins](#dossiers-et-chemins)). |
| `422` | `folder '<folder>' does not accept <type> files` | Le dossier n'accepte pas ce type de fichier. |
| `422` | `the file is larger than <n> MB` | Le fichier dépasse la limite de taille du dossier ou la limite générale. |
| `422` | `the file content is <type>, not <declared>` / `the file content is not <declared>` | Le contenu ne correspond pas au type d'image ou PDF déclaré. |
| `422` | `files in this folder must belong to a database record` | Le dossier exige un enregistrement et aucun n'a été envoyé. |
| `422` | `record '<key>' is not in collection '<collection>'` | L'enregistrement existe dans une autre collection. |
| `404` | `Database record not found` | Aucun enregistrement de ce type dans la base de données du panneau. |
| `403` | `database access is not allowed for this panel` | Un enregistrement a été envoyé, mais le panneau n'a pas de base de données. |
| `422` | `metadata must be a JSON object` / `metadata is larger than 4096 bytes` | Métadonnées invalides. |
| `403` | `folder '<folder>' has reached the maximum of <n> files` / `folder '<folder>' is full` | Limite du dossier atteinte. |
| `403` | `the file storage of this panel is full` | Limite du panneau atteinte. |
| `422` | `storage quota exceeded` / `organization storage quota exceeded` | Le quota de stockage du propriétaire (de l'organisation) est plein. |

### Exemples

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/storage/private/upload" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -F "file=@INV-0042.pdf;type=application/pdf" \
  -F "path=invoices/2026/" \
  -F 'metadata={"customer": "ACME", "total": 1250}' \
  -F "record_collection=orders" \
  -F "record_key=8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70"
```

```json
{
  "id": "5b0a1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "path": "invoices/2026/INV-0042.pdf",
  "folder": "invoices",
  "name": "INV-0042.pdf",
  "size": 48213,
  "content_type": "application/pdf",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "metadata": {"customer": "ACME", "total": 1250},
  "record": {"collection": "orders", "key": "8f14e45f-ceea-467a-9b1c-4e1f2a6b3c70"},
  "owner": null,
  "created": 1791374100000,
  "updated": 1791374100000,
  "expires_at": null,
  "url": "https://ayeto.ai/api/v3/booster/storage/file/3d2c5a8e-…/5b0a1c2d-…/1791377700i.Xk2…/INV-0042.pdf",
  "download_url": "https://ayeto.ai/api/v3/booster/storage/file/3d2c5a8e-…/5b0a1c2d-…/1791377700a.p9Q…/INV-0042.pdf"
}
```

## Téléverser un fichier en base64

Identique à [Téléverser un fichier](#televerser-un-fichier) avec un corps JSON, pour les petits fichiers.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/upload-b64` |
| Portée | `ayeto.booster.storage.write` |

### Requête

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `path` | chaîne | oui | Chemin cible ; sa dernière partie est le nom du fichier. |
| `data` | chaîne | oui | Contenu en base64 ou data URL (`data:text/csv;base64,…`). |
| `content_type` | chaîne | non | Type déclaré (sinon déduit de la data URL ou de l'extension du fichier). |
| `metadata`, `record`, `overwrite`, `rename` | | non | Comme pour [Téléverser un fichier](#televerser-un-fichier) ; `record` est `{collection, key}`. |

### Réponse et erreurs

Comme pour [Téléverser un fichier](#televerser-un-fichier), plus `422 data is not valid base64`.

### Exemple

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/storage/private/upload-b64" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"path": "exports/orders-2026-10.csv", "data": "data:text/csv;base64,aWQsdG90YWwKMSwxMjUw"}'
```

## Téléverser directement un gros fichier

Les gros fichiers (de dizaines à centaines de Mo) peuvent aller directement au service de
stockage au lieu de passer par l'API, en trois étapes : annoncer le fichier, l'envoyer en POST
à l'URL renvoyée, terminer le téléversement. La disponibilité du téléversement direct dépend de
l'installation AYETO ; lorsqu'il n'est pas disponible, `upload/init` répond
`409 direct upload is not available, use the upload route` et vous utilisez
[Téléverser un fichier](#televerser-un-fichier).

| | |
|---|---|
| Endpoints | `POST /api/v3/booster/storage/private/upload/init`, `POST /api/v3/booster/storage/private/upload/complete` |
| Portée | `ayeto.booster.storage.write` |

1. `upload/init` avec `path`, `filename` (lorsque le chemin se termine par `/`), `content_type`,
   `size` (taille exacte en octets) et éventuellement `metadata`, `record`, `overwrite`, `rename`.
   Les règles du dossier, le type et les limites sont vérifiés comme pour un téléversement. La
   réponse est `{upload_id, url, fields, expires_at}`.
2. `POST` vers `url` en `multipart/form-data` : chaque entrée de `fields` comme champ du
   formulaire, puis le fichier comme champ `file` (il doit être la dernière partie). Le fichier
   doit avoir exactement la taille et le type annoncés. Cette requête va au service de stockage,
   sans votre clé API.
3. `upload/complete` avec `{"upload_id": "…"}` avant `expires_at` (une heure). Le contenu
   téléversé est vérifié à nouveau (taille, type d'après le contenu) et le [fichier](#fichiers)
   est renvoyé. Un téléversement jamais terminé est supprimé.

| Statut | `detail` | Cause |
|---|---|---|
| `409` | `direct upload is not available, use the upload route` | Utilisez [Téléverser un fichier](#televerser-un-fichier). |
| `404` | `upload not found` | `upload_id` inconnu ou expiré, ou déjà terminé. |
| `422` | `the file was not uploaded` / `the uploaded file has another size than announced` | L'étape 2 n'a pas enregistré le fichier annoncé. |

Autres erreurs comme pour [Téléverser un fichier](#televerser-un-fichier).

## Lister les fichiers

Renvoie les fichiers situés directement dans un répertoire ainsi que ses sous-dossiers. Sans
`folder`, renvoie les dossiers de premier niveau du panneau.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/list` |
| Portée | `ayeto.booster.storage` |

### Requête

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `folder` | chaîne | non | Dossier de premier niveau. Sans lui, seul `folders` est renvoyé. |
| `prefix` | chaîne | non | Sous-dossier à l'intérieur du dossier, par ex. `2026` ou `2026/q1`. |
| `recursive` | booléen | non | Inclure aussi les fichiers des sous-dossiers à toute profondeur. `false` par défaut. |
| `params` | objet | non | `filters`, `sort_key`, `sort_order`, `limit_from`, `limit_to` comme dans la [base de données booster](booster-database.md#interroger-les-enregistrements). Champs : `name`, `path`, `size`, `content_type`, `owner`, `created`, `updated`, `record.collection`, `record.key`, `metadata.<path>`. Du plus récent au plus ancien par défaut. |
| `links` | booléen | non | Inclure `url` et `download_url`. `true` par défaut. |

### Réponse

`{"files": [...], "folders": [...]}` : les [fichiers](#fichiers) de la page et les noms des
sous-dossiers situés directement dans le répertoire.

### Exemple

```bash
curl -X POST "https://ayeto.ai/api/v3/booster/storage/private/list" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "panel-id: 3d2c5a8e-7b41-4f0e-9a6d-2c8b1e4f7a90" \
  -H "Content-Type: application/json" \
  -d '{"folder": "invoices", "prefix": "2026", "params": {"filters": [["metadata.customer", "==", "ACME"]]}}'
```

## Compter les fichiers

Le nombre de fichiers qu'une [liste](#lister-les-fichiers) avec les mêmes `folder`, `prefix`,
`recursive` (ici `true` par défaut) et `params.filters` renverrait sur l'ensemble des pages.
Sans `folder`, tous les fichiers du panneau.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/count` |
| Portée | `ayeto.booster.storage` |

## Obtenir un fichier

Un fichier avec de nouveaux liens, par `id` ou par `path`.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/get` |
| Portée | `ayeto.booster.storage` |

```json
{"path": "invoices/2026/INV-0042.pdf"}
```

## Obtenir des liens de téléchargement

De nouveaux liens pour 100 fichiers au plus.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/url` |
| Portée | `ayeto.booster.storage` |

### Requête

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `ids` | tableau d'UUID | oui | Fichiers (1–100). |
| `ttl_minutes` | entier | non | Durée de validité plus courte que les 60 minutes par défaut. |
| `download` | booléen | non | Liens qui téléchargent toujours le fichier. `false` par défaut. |

### Réponse

Un tableau de `{id, url, expires_at}` ; `expires_at` (millisecondes) vaut `null` pour les liens
publics permanents.

## Mettre à jour un fichier

Renomme ou déplace un fichier, remplace ses métadonnées ou son enregistrement. Un déplacement vers
un autre dossier nécessite le droit de téléversement dans celui-ci et le droit de suppression dans le
dossier actuel, et le fichier doit respecter les règles de type et de taille du nouveau dossier. Le
contenu ne change jamais ; pour le remplacer, téléversez avec `overwrite`.

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/update` |
| Portée | `ayeto.booster.storage.write` |

### Requête

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `id` | UUID | oui | Le fichier. |
| `path` | chaîne | non | Nouveau chemin. |
| `metadata` | objet | non | Nouvelles métadonnées (remplacent les anciennes). |
| `record` | objet | non | Nouvel enregistrement `{collection, key}`. |
| `unlink_record` | booléen | non | Supprimer le lien avec l'enregistrement. |

### Réponse et erreurs

Le [fichier](#fichiers) mis à jour. Erreurs comme pour [Téléverser un fichier](#televerser-un-fichier) ;
`409` lorsque le nouveau chemin est pris.

## Supprimer des fichiers

| | |
|---|---|
| Endpoints | `POST /api/v3/booster/storage/private/delete` (`{"id": "…"}`), `POST /api/v3/booster/storage/private/delete-many` (`{"ids": [...]}`, jusqu'à 100) |
| Portée | `ayeto.booster.storage.write` |

Renvoie `{"accepted": true}`. Les liens d'un fichier supprimé cessent immédiatement de fonctionner.
`delete-many` supprime les fichiers dans l'ordre et s'arrête au premier qui échoue.

## Utilisation du stockage

| | |
|---|---|
| Endpoint | `POST /api/v3/booster/storage/private/usage` (corps vide) |
| Portée | `ayeto.booster.storage` |

```json
{"files": 214, "size": 73400320, "limit": 1073741824, "folders": {"invoices": 52428800, "products": 20971520}}
```

`limit` est la limite du panneau en octets (`0` = seul le quota de stockage du propriétaire s'applique).

## Événements

Les fichiers enregistrés, modifiés (déplacés, nouvelles métadonnées ou nouvel enregistrement)
et supprimés via cette API sont des événements du panneau comme les autres : ils démarrent les
[workflows](workflows.md) ayant un déclencheur de fichier booster sur le panneau, et les webhooks
du panneau abonnés aux événements de fichiers les reçoivent. Les suppressions dues à la durée de
conservation ou à la suppression d'un enregistrement sont aussi des événements.
