◂ Retour à [README](./README.md)
# InWink APIs — Champs de type fichier (guide d'utilisation de l'API)

> **But.** Décrire comment **envoyer**, **lire**, **remplacer** et **supprimer** un fichier
> porté par un champ d'entité, via les APIs d'administration InWink (EventAPI / CommunityAPI /
> CustomerAPI). Tout est décrit au niveau HTTP : verbe + route + corps JSON + réponse JSON.
>
> Auth : jeton porteur (Bearer) avec les claims du scope. La FrontAPI publique (participant,
> anonyme) a des routes différentes et n'est pas couverte (note en fin de document).
>
> Convention JSON : les corps de requête sont acceptés en casse insensible ; les réponses
> sont en `camelCase`. Les exemples ci-dessous utilisent `camelCase`.

---

## 1. Principe (à lire en premier)

Un « champ de type fichier » est un champ d'entité (ex. `prop3` sur `person`,
`videofile` sur `vod`, `audiofile` sur `podcast`…) dont le type déclaré dans le template
est l'un de :

| Type de champ | Effet |
|---------------|-------|
| `file` | fichier générique (document, etc.) |
| `image` | image (miniatures générées automatiquement) |
| `filevideo` | vidéo (encodage + transcripts déclenchés) |

**La valeur du champ change de forme selon le sens :**

- **En écriture** : une simple **chaîne = le nom du blob temporaire** (`blobName`) obtenu à
  l'étape d'upload.
- **En lecture** : un **objet fichier** JSON complet
  (`{ blobName, container, url, fileName, mimeType, size, ... }`).

**L'upload suit un pattern « lien d'upload direct » en 4 temps.** L'API n'est jamais dans le
chemin des octets : elle délivre une URL d'upload signée (SAS), et **le client envoie les
octets directement sur le stockage**.

```
1. POST .../file            → l'API crée un emplacement temporaire, renvoie une URL signée
2. PUT des octets            → le client téléverse directement sur l'URL signée
3. POST .../file/finalize-tmp → l'API valide et fige le fichier temporaire
4. PATCH/PUT de l'entité      → on met la valeur du champ = nom du blob ; l'API le déplace
                                vers son emplacement définitif et réécrit la valeur en objet fichier
```

> ⚠️ **Point crucial** : il n'existe **aucun endpoint « attacher un fichier »**. On référence
> le nom du blob temporaire dans le champ, puis **on sauvegarde l'entité via son endpoint
> d'édition habituel**. C'est cette sauvegarde qui matérialise le fichier.

### Forme des routes

Le point d'entrée fichier d'une entité est `{scopeId}/{entity}/file`. Exemples :
`{eventId}/person/file`, `{eventId}/session/file`, `{communityId}/vod/file`.

`{scopeId}` selon l'API :
- EventAPI : `{eventId}`
- CommunityAPI : `{communityId}`
- CustomerAPI : `{customerId}/audience/{tenantId}` (ou `{customerId}`)

---

## 2. Envoyer un fichier dans un champ d'entité (flux complet)

### Étape 1 — Créer l'emplacement temporaire

```
POST {scopeId}/{entity}/file
Content-Type: application/json

{
  "entity": "person",        // nom de l'entité
  "field": "prop3",          // clé du champ fichier
  "type": "image/png",       // type MIME du fichier
  "name": "photo.png",       // nom de fichier
  "blobType": "Block"        // "Block" (défaut) | "append" | "page"
}
```

Réponse `200` — objet d'accès temporaire :

```json
{
  "url": "https://<stockage>.blob.core.windows.net",
  "container": "tmp",
  "blobName": "a7f3c1e0-....png",
  "sas": "?sv=...&sig=..."
}
```

### Étape 2 — Téléverser les octets directement (hors API)

Le client construit l'URL cible et **envoie les octets** dessus (droits lecture/écriture sur
l'emplacement temporaire) :

```
PUT {url}/{container}/{blobName}{sas}
x-ms-blob-type: BlockBlob
Content-Type: image/png

<octets du fichier>
```

Pour un upload long, renouveler le lien signé si besoin :

```
POST {scopeId}/{entity}/file/renew        { "blobName": "...", "sas": "..." }   → nouvel objet d'accès
POST {scopeId}/{entity}/file/try-renew    { "blobName": "...", "sas": "..." }   → renouvelé si expiré
```

### Étape 3 — Finaliser le fichier temporaire

```
POST {scopeId}/{entity}/file/finalize-tmp
{ "blobName": "a7f3c1e0-....png", "sas": "?sv=...&sig=..." }
```

- `200` : le contenu correspond au type MIME déclaré ; le fichier temporaire est figé.
- `400` avec corps `101` : le contenu ne correspond pas au MIME annoncé (rejet).
- `400` avec corps `102` : type de fichier non supporté.

### Étape 4 — Sauvegarder l'entité en référençant le fichier

Via l'**endpoint d'édition habituel de l'entité**, mettre la valeur du champ = le `blobName` :

```
PATCH {scopeId}/person/{personId}
{ "prop3": "a7f3c1e0-....png" }
```

À la sauvegarde, l'API valide (taille, MIME autorisés, cohérence du contenu), **déplace le
fichier vers son emplacement définitif**, **réécrit la valeur** du champ en objet fichier
complet, et déclenche les traitements associés (encodage vidéo, miniatures, analyse de document).

La valeur relue devient par exemple :

```json
{
  "blobName": "prop3/9f...png",
  "container": "3fa85f64-...._person-prop3",
  "url": "https://<stockage>.blob.core.windows.net/...",
  "fileName": "photo.png",
  "mimeType": "image/png",
  "size": 20481,
  "contentMD5": "..."
}
```

---

## 3. Lire / télécharger un fichier

```
GET {scopeId}/{entity}/file/{entityId}/{field}?lang=fr&withoutExpiration=false
```

Réponse `200` — objet fichier avec lien d'accès :

```json
{
  "url": "https://<stockage>.blob.core.windows.net",
  "container": "3fa85f64-...._person-prop3",
  "blobName": "prop3/9f...png",
  "fileName": "photo.png",
  "mimeType": "image/png",
  "size": 20481,
  "sas": "?sv=...&sig=...",
  "lastModified": "2026-07-01T10:00:00Z"
}
```

Le client télécharge ensuite les octets directement sur **`url` + `/{container}/{blobName}` + `sas`**.

- `404` si le champ n'a pas de fichier.
- `lang` : langue pour un champ localisé (défaut = langue par défaut du scope).
- `withoutExpiration=true` : lien d'accès de longue durée.

**Fichier d'une sous-entité (collection)** :
```
GET {scopeId}/{entity}/file/{entityId}/{nestedField}/{index}/{field}?lang=&withoutExpiration=
```

**Champ vidéo** : la réponse est enrichie (voir §5).

---

## 4. Remplacer / ré-éditer / supprimer un fichier

**Récupérer une copie éditable** du fichier actuel dans l'espace temporaire, puis reprendre
le flux finalize + save :
```
GET {scopeId}/{entity}/file/{entityId}/{field}/copytotmp?lang=fr
→ 200  { "blobName": "...", "container": "tmp", ... }
```

**Remplacer** : téléverser un nouveau fichier temporaire (§2) et ré-éditer l'entité avec le
nouveau `blobName`. L'ancien fichier est automatiquement retiré.

**Supprimer** : éditer l'entité en mettant le champ à `null`.

---

## 5. Champs vidéo — fonctionnalités avancées

Les champs `filevideo` déclenchent un encodage et exposent transcripts et analyse de document.
Toutes les routes sont sous `{scopeId}/{entity}/file/...`.

### Lecture d'un champ vidéo
`GET {scopeId}/{entity}/file/{entityId}/{field}` renvoie, quand la vidéo est prête :

```json
{
  "url": "...", "blobName": "...", "container": "...", "fileName": "...",
  "sas": "?...", "mimeType": "video/mp4", "size": 123456,
  "token": null,
  "transcripts": { "fr": { "url": "...", "sas": "?..." } },
  "transcriptsEnabled": true,
  "dashEnabled": true,
  "hlsEnabled": true
}
```

### Encodage
```
POST {scopeId}/{entity}/file/video/{entityId}/{field}/encoding/retry?lang=fr   → relance l'encodage
GET  {scopeId}/{entity}/file/{entityId}/{field}/status?lang=fr                 → statut d'encodage
```

### Transcripts (requiert une licence IA)
```
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/enabled         → active le traitement
GET  {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/status          → { status, progress, duration }
GET  {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr   (corps = manifeste)
GET  {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}             (corps = transcript)
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}/delete       (corps = transcript)
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/reset/from-artifacts
GET  {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/artifacts/{data}        (redirection 302 vers le fichier)
```
`status` ∈ `NotSet`, `Pending`/`Processing`, `Ready`. Un `Ready` non enregistré est
persisté automatiquement au premier appel de `status`/`manifest`/`get`.

### Analyse de document (requiert une licence IA)
```
GET  {scopeId}/{entity}/file/document/{entityId}/{field}/analysis         → statut de l'opération
POST {scopeId}/{entity}/file/document/{entityId}/{field}/analysis/start   → démarre l'analyse
```

---

## 6. Formes JSON de référence

**Corps de requête**
- Créer l'emplacement temporaire : `{ entity, field, type, name, blobType }`
- Renouveler / finaliser : `{ blobName, sas }`

**Réponses**
- Objet d'accès temporaire (create/renew) : `{ url, container, blobName, sas }`
- Objet fichier avec lien d'accès (get/download) :
  `{ url, container, blobName, fileName, size, mimeType, sas, lastModified }`
- Objet fichier persisté (valeur du champ) :
  `{ url, container, blobName, fileName, size, mimeType, linkedBlobs, contentMD5, properties }`
  - Image : ajoute `thumb`, `thumbSmall`, `thumbLarge` (chacun un objet fichier).
  - Vidéo : ajoute `video { id, isReady, transcriptsEnabled, transcripts, transcriptData }`
    et `encodings { <provider>: { hlsUrl, dashUrl, status, progress, timestamp } }`.

**Codes d'erreur de transfert** (corps de la réponse `400`) : `101` = fichier invalide
(contenu ≠ MIME), `102` = type de fichier non supporté.

---

## 7. Exemple de bout en bout

Entité `person`, champ `prop3` de type `file`.

1. Créer l'emplacement temporaire :
   ```
   POST {eventId}/person/file
   { "entity": "person", "field": "prop3", "name": "photo.jpg", "type": "image/jpeg" }
   → 200  { "blobName": "<blobName>", "container": "tmp", "url": "...", "sas": "..." }
   ```
2. Téléverser les octets sur `url + /tmp/<blobName> + sas`.
3. Finaliser : `POST {eventId}/person/file/finalize-tmp { "blobName": "<blobName>", "sas": "..." }` → `200`.
4. Éditer la personne : `PATCH {eventId}/person/<personId> { "prop3": "<blobName>" }` → `200`.
   → le fichier est déplacé de `tmp` vers le conteneur `{eventId}_person-prop3`, et la valeur
   du champ est relue comme objet fichier complet.

---

## 8. Points d'attention

- **Double validation MIME** : au `finalize-tmp` (contenu vs MIME déclaré) **et** à la
  sauvegarde de l'entité (taille max, MIME autorisés du champ, cohérence du contenu, sûreté
  SVG). Un fichier invalide fait échouer/vider le champ.
- **Emplacement définitif** = conteneur `{scopeId}_{entity}-{field}` (minuscules) — utile
  pour interpréter les URLs retournées.
- **Langue** : pour un champ localisé, transmettre `lang` en lecture.
- **`withoutExpiration=true`** : lien de longue durée (à réserver aux usages qui le justifient).
- **Remplacement** : ré-uploader et ré-éditer suffit ; l'ancien fichier est nettoyé automatiquement.

---

## Note — FrontAPI (hors périmètre)

Côté public/participant, un flux équivalent existe mais avec des routes et une auth
différentes : `POST {scopeId}/file` (+ `/renew`, `/try-renew`, `/finalize-tmp`), lecture
`GET community/{communityId}/file/{Entity}/{entityId}/{field}`, en accès anonyme avec
contrôles par droits/groupes (peut renvoyer `403` selon l'appartenance).

---

*Voir aussi : `assets` (bibliothèque médias — mêmes primitives d'upload, avec identité propre et `inwinkasset://`), `mutations`.*
