# Comment ça marche

> Un transfert est un objet dans votre bucket sous un code de huit caractères, avec une expiration vérifiée à chaque lecture. Fichiers, snapshots, le protocole à quatre routes, les écritures conditionnelles, et pourquoi les packages sont découpés ainsi.

Canonical: https://s3nd.sh/fr/how-it-works · Markdown: https://s3nd.sh/fr/how-it-works.md · English: https://s3nd.sh/how-it-works

s3nd est délibérément petit. Cette page en fait le tour complet : ce qu’est un transfert, ce qu’est un code, le protocole entre un serveur et ses clients, et pourquoi les packages sont découpés ainsi.

## Un objet, un code, une expiration.
Un fichier est stocké tel quel, avec le nom, le type de contenu et l’expiration dans les métadonnées de l’objet. Des données structurées sont stockées comme un snapshot : une enveloppe qui se décrit elle-même, gzippée. Les deux vivent sous le code.

**un fichier, tel qu’il arrive dans le bucket**
```text
drop/K7QP2M4X
  Content-Type:         application/pdf
  Content-Disposition:  attachment; filename="report.pdf"
  x-amz-meta-s3nd-kind:        file
  x-amz-meta-s3nd-expires-at:  2026-08-27T13:00:00.000Z
  Body:                 the bytes, untouched
```
**un snapshot, tel qu’il arrive dans le bucket**
```jsonc
{
  "s3nd": 1,              // envelope format, not your data's
  "app": "notes",
  "version": 3,           // your schema version
  "device": "Pixel 8",
  "createdAt": "2026-08-27T12:00:00.000Z",
  "expiresAt": "2026-08-27T13:00:00.000Z",
  "data": { /* whatever you passed */ }
}                          // gzipped, typically 5–10× smaller
```

- **expiration à la lecture** — Un transfert expiré n’est jamais remis, même si l’objet est encore dans le bucket. Un code expiré répond le même `NOT_FOUND` qu’un code qui n’a jamais existé, donc personne ne peut sonder quels codes ont servi.
- **règle de cycle de vie** — Supprimer l’objet est le travail de votre bucket, via une règle de cycle de vie sur le préfixe. `s3nd doctor` vérifie que vous en avez une, parce qu’un bucket qui se remplit discrètement de transferts expirés est la façon la plus courante de se tromper.
- **versions de schéma** — Un snapshot porte votre version de schéma. Passez `maxVersion` à la lecture et un snapshot issu d’une version plus récente lève `SNAPSHOT_TOO_NEW` au lieu d’atterrir dans une app qui va le mal lire.
- [Les snapshots](https://doc.s3nd.sh/docs/snapshots)
- [Quelle taille peut faire un transfert](https://doc.s3nd.sh/docs/limits)

## Quarante bits qui survivent à un appel téléphonique.
Un code, c’est toute l’expérience utilisateur d’un transfert. Il apparaît sur un écran, quelqu’un le tape sur un autre, et tout dans le code découle de là.

- **Base32 Crockford** — Ni `I`, ni `L`, ni `O`, ni `U`. Les trois premiers sont ceux que l’on confond ; retirer le quatrième évite qu’un code aléatoire n’épelle quelque chose de malheureux.
- **Normalisé au retour** — Séparateurs retirés, casse repliée, et `O` lu comme zéro uniquement quand il n’y a aucune lettre O avec laquelle le confondre. La réparation se fait dans le navigateur, avant toute requête.
- **Réservé par une écriture conditionnelle** — Le serveur choisit le code et écrit avec `ifAbsent`, donc une collision échoue bruyamment et réessaie avec un code neuf au lieu d’écraser le transfert d’un inconnu.
- [Codes de synchronisation](https://doc.s3nd.sh/docs/sync-codes)
- [Longueur, alphabet, et ce que chacun coûte](https://doc.s3nd.sh/docs/code-configuration)

## Quatre routes, un format d’erreur. Écrits noir sur blanc.
Un navigateur ne peut pas détenir vos identifiants S3, donc dès qu’il participe, un serveur se place au milieu. La forme de ce milieu est un protocole, pas ce que le handler fait par hasard.

**relatives à l’endroit où vous l’avez monté**
```text
POST   /                # create a transfer, get the code back
GET    /:code           # metadata; the state inline for a snapshot
GET    /:code/raw       # the bytes, or a 302 to a presigned URL
DELETE /:code           # burn it

# every error, same shape
{ "error": { "code": "NOT_FOUND", "message": "Unknown or expired code." } }
```
**le client, dans un navigateur**
```ts
import { createTransferClient } from '@s3nd/protocol'

const transfers = createTransferClient({ baseUrl: '/api/transfers' })

const { code } = await transfers.createFile({ body: file, filename: file.name })
const meta = await transfers.read(typed)        // null when unknown or expired
const bytes = await transfers.readBytes(code)   // the file back
await transfers.remove(code)
```

Un client fonctionne avec n’importe quel serveur qui répond à ces routes, pas seulement avec `createTransferHandler()`. Un serveur en Go ou en Rails fonctionne avec tous les clients s3nd. Et la CLI pointée sur `--remote` ne peut pas savoir à qui elle parle, ce qui est exactement pourquoi `s3nd put` fonctionne contre votre propre déploiement.

Ce que vous envoyez décide de ce que contient un transfert : un corps JSON est un snapshot, tout autre type de contenu est un fichier avec son nom dans `X-S3nd-Filename`. Les clients lèvent une `TransferError` qui porte le code d’erreur ; branchez sur le code, jamais sur le message.

[Le protocole de transfert, route par route](https://doc.s3nd.sh/docs/protocol)

## Quand deux machines écrivent, « le dernier gagne » est une perte de données.
Un transfert unique a un seul écrivain. Une sauvegarde par utilisateur en a deux, et le comportement par défaut de S3 garde silencieusement celui qui est arrivé en dernier.

```ts
// Claim a fresh code: write only if nothing sits under it.
await store.putSnapshot(code, state, { ifAbsent: true })

// Rewrite a shared backup: fail if someone wrote since you read.
const current = await store.getSnapshot(`user-${userId}`)
await store.putSnapshot(`user-${userId}`, merged, { ifMatch: current?.etag })
// → PRECONDITION_FAILED when another device won. Read again, merge again.
```

Les deux options sont de simples en-têtes conditionnels S3. `ifAbsent` est la façon de réserver un code neuf ; `ifMatch` est la façon dont un second appareil apprend qu’il a perdu la course. Elles fonctionnent chez tous les fournisseurs qui les implémentent, et l’exemple Node est là pour vérifier que le vôtre en fait partie.

[Deux appareils, un snapshot](https://doc.s3nd.sh/docs/two-devices)

## Une seule contrainte décide du découpage.
Un navigateur ne doit jamais se retrouver avec un client de stockage dans son arbre de dépendances. Le package protocol est ce que les deux moitiés partagent, et c’est la seule raison de son existence.

- **@s3nd/cli** (Le binaire; s3nd) — put, get, rm, doctor, init et config. Une seule implémentation, le client du protocole, branchée soit sur fetch, soit directement sur le handler dans le même processus.
- **s3nd** (La primitive; aws-sdk, protocol) — Fichiers, snapshots, écritures conditionnelles et le handler de transfert. Le seul package qui détient des identifiants, donc le seul qui tourne sur un serveur.
- **@s3nd/protocol** (Le contrat; nanoid) — Le format sur le fil, un client basé sur fetch, et les codes de synchronisation. Rien ici n’importe de client de stockage, et c’est ce qui permet à un navigateur de le partager.
- **@s3nd/react** (Les hooks; protocol, react (peer)) — Envoyer, recevoir, et un champ de code. Dépend du protocole et jamais de S3, donc aucun chemin de votre bundle n’atteint le SDK AWS.

- **Runtime serveur**: Node 20 ou plus, ce que le SDK AWS v3 exige
- **Handler**: Request en entrée, Response en sortie : Next.js, Hono, Bun.serve, Deno, workers
- **Packages navigateur**: fetch et rien d’autre : navigateur, worker, React Native, Deno
- **Tests**: Hors ligne, contre un S3 en mémoire qui honore les en-têtes conditionnels
