
# Téléversement et signature par lot

Deux problèmes que le seul jeton de téléchargement ne résout pas : faire entrer de gros fichiers _dans_ le stockage sans faire transiter chaque octet par le serveur applicatif, et obtenir beaucoup d'URL à la fois sans un aller-retour par URL.

## URL de téléversement

Une URL signée de téléversement est un permis d'écriture à usage unique pour un chemin. Le navigateur y envoie un `PUT` direct ; votre serveur ne touche jamais les octets.

```http
POST /api/buckets/uploads/sign
Content-Type: application/json
Cookie: <session>

{
  "path": "uploads/user-123/profile-photo.jpg",
  "operation": "upload",
  "expiresIn": 600,
  "contentType": "image/jpeg",
  "maxSize": 5242880
}
```

```json
{
  "success": true,
  "signedUrl": "https://app.example.com/api/buckets/uploads/signed?path=uploads%2Fuser-123%2Fprofile-photo.jpg&op=upload&expires=1775383800000&token=8c2d…&ct=image%2Fjpeg&max=5242880",
  "expiresAt": "2026-04-05T10:10:00Z",
  "operation": "upload"
}
```

| Champ         | Type   | Requis | Défaut             | Remarques                                             |
| ------------- | ------ | ------ | ------------------ | ----------------------------------------------------- |
| `path`        | chaîne | Oui    | —                  | La clé exacte à écrire. C'est vous qui la choisissez. |
| `operation`   | chaîne | Oui    | —                  | Doit valoir `upload` ; le défaut est `download`.      |
| `expiresIn`   | entier | Non    | `3600`             | De 60 à 604800 secondes.                              |
| `contentType` | chaîne | Non    | tout type          | Vérifié lors du `PUT`.                                |
| `maxSize`     | entier | Non    | `10485760` (10 Mo) | Plafond en octets, vérifié lors du `PUT`.             |

Contrairement à un jeton de téléchargement, un jeton de téléversement n'exige pas que le fichier existe — c'est tout l'intérêt. `permissions.signUpload` détermine qui peut en émettre un, et sa valeur par défaut est « administrateurs uniquement ».

### Les contraintes sont signées

`contentType` et `maxSize` apparaissent dans la chaîne de requête sous les noms `ct` et `max`, et les deux entrent dans la charge utile HMAC. Modifier l'un ou l'autre invalide le jeton : un client ne peut donc pas élargir lui-même le permis qu'on lui a remis.

```bash
curl -X PUT "$SIGNED_UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg
```

| Au moment du `PUT`                        | Résultat                                    |
| ----------------------------------------- | ------------------------------------------- |
| Dans la fenêtre, type et taille respectés | `200` avec `{ "success": true, "path": … }` |
| `Content-Type` différent du `ct` signé    | `400`                                       |
| Corps plus lourd que le `max` signé       | `413`                                       |
| Après expiration, ou paramètre modifié    | `403`                                       |
| Jeton de téléchargement utilisé en `PUT`  | `403` (opération non concordante)           |

Le fichier stocké atterrit exactement au `path` signé — aucun préfixe UUID n'est ajouté, contrairement à un [téléversement multipart](/fr/docs/file-operations). Vous avez choisi la clé, vous assumez donc les collisions : signer deux fois le même chemin et utiliser les deux permis écrase.

:::callout
**Un permis d'écriture est un droit plus large qu'un permis de lecture — dimensionnez-le en conséquence.** Quiconque détient l'URL peut écrire ces octets jusqu'à son expiration. Dix minutes est généreux pour une soumission de formulaire ; une heure est déjà long pour un permis que personne ne peut révoquer. Définissez toujours explicitement `contentType` et `maxSize` plutôt que d'accepter « tout type, 10 Mo ».
:::

## Signature par lot

Une requête, jusqu'à 100 URL. L'usage évident est une galerie dont chaque vignette est privée.

```http
POST /api/buckets/photos/sign/batch
Content-Type: application/json
Cookie: <session>

{
  "files": [
    { "path": "photo-1.jpg", "expiresIn": 3600 },
    { "path": "photo-2.jpg", "expiresIn": 3600 },
    { "path": "uploads/new.jpg", "expiresIn": 600, "operation": "upload" }
  ]
}
```

```json
{
  "results": [
    { "path": "photo-1.jpg", "signedUrl": "https://…", "expiresAt": "2026-04-05T11:00:00Z" },
    { "path": "photo-2.jpg", "error": "not_found" },
    { "path": "uploads/new.jpg", "signedUrl": "https://…", "expiresAt": "2026-04-05T10:10:00Z" }
  ]
}
```

Chaque entrée porte son propre `expiresIn` et sa propre `operation`. Les entrées de téléchargement dont le fichier manque reviennent avec `error: "not_found"` au lieu de faire échouer le lot — une galerie comportant une référence morte affiche quand même les quatre-vingt-dix-neuf autres.

Deux choses font en revanche échouer toute la requête : plus de 100 entrées, et un seul `expiresIn` hors bornes. Les deux répondent `400`. L'asymétrie est intentionnelle — un fichier manquant est un état de données que vous pouvez afficher, tandis qu'une expiration invalide est un bogue dans l'appelant.

## Pages associées

- [URL signées](/fr/docs/signed-urls) — le modèle de signature et qui peut signer.
- [URL de téléchargement](/fr/docs/signed-urls-download) — les jetons de lecture.
- [Permissions de bucket](/fr/docs/buckets-permissions) — `signUpload` est réservé aux administrateurs par défaut.
- [Opérations sur les fichiers](/fr/docs/file-operations) — l'alternative du téléversement multipart.
- [Sécurité des téléversements](/fr/docs/file-security) — ce qu'un `PUT` direct valide, et ce qu'il ne valide pas.
