Stockage des médias : le plan était en retard sur le code
Le pré-vol de quota et la suppression du blob sont livrés. Le contrôle autoritaire à Create ne peut pas exister tel qu'il était écrit : manager-app crée la ligne en annonçant sizeBytes puis téléverse, donc il n'y a aucun blob à interroger à cet instant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d36394cabd
commit
40323b1440
@ -129,19 +129,49 @@ La ligne `Resource` est créée **avant** l'upload, et l'URL écrite après coup
|
||||
|
||||
## 4. Quota de stockage
|
||||
|
||||
### État actuel : non appliqué
|
||||
### État actuel — relevé dans le code le 2026-09-07
|
||||
|
||||
Le contrôle `StorageQuotaBytes` n'existe que dans l'endpoint legacy `Upload` (chemin base64, plus utilisé). `Create` — le chemin réellement emprunté — **ne vérifie aucun quota et ne renseigne pas `SizeBytes`**.
|
||||
⚠️ **Cette section était en retard sur le code.** Les points 1 et 3 sont livrés, et le point 2
|
||||
tel qu'il était écrit ne peut pas fonctionner. Corrigé ci-dessous.
|
||||
|
||||
### Corrections, dans cet ordre
|
||||
| # | Correction prévue | État réel |
|
||||
|---|---|---|
|
||||
| 1 | Endpoint de pré-vol | ✅ **livré** — `ExceedsStorageQuota` ([ResourceController.cs:966](../../manager-service/ManagerService/Controllers/ResourceController.cs#L966)) résout quota d'instance puis quota de plan, et garde les deux chemins ([:246](../../manager-service/ManagerService/Controllers/ResourceController.cs#L246) multipart, [:330](../../manager-service/ManagerService/Controllers/ResourceController.cs#L330) JSON) |
|
||||
| 2 | Contrôle autoritaire à `Create`, taille lue depuis Firebase | ⛔ **infaisable tel quel** — voir ci-dessous |
|
||||
| 3 | `Delete` doit supprimer le blob | ✅ **codé** — blob avant ligne, 502 qui interrompt plutôt que de créer un orphelin ([:581](../../manager-service/ManagerService/Controllers/ResourceController.cs#L581)). **Mais inerte** : `Firebase:StorageBucket` vaut `""`, donc `IsConfigured` est faux, `DeleteAsync` renvoie `NotConfigured`, et l'appelant ne bloque que sur `Failed`. Verrou = la carte kanban « Poser `Firebase:StorageBucket` » |
|
||||
|
||||
1. **Endpoint de pré-vol** — `POST /api/Resource/check-quota { instanceId, sizeBytes }`, appelé **avant** de pousser dans Firebase. Répond OK / dépassement + octets restants. Purement UX : éviter d'attendre l'upload de 180 Mo pour se faire refuser. Non autoritaire, une course reste possible, sans gravité.
|
||||
### Pourquoi le point 2 ne peut pas être un contrôle synchrone
|
||||
|
||||
2. **Contrôle autoritaire à `Create`, taille lue depuis Firebase.** Ne **jamais** faire confiance au `sizeBytes` envoyé par le client — un appelant qui envoie `0` bypasse le quota définitivement. `FirebaseAdmin` est déjà référencé dans le `.csproj`. En cas de dépassement : supprimer le blob puis renvoyer 413. Sans la suppression, un refus laisse un orphelin qui occupe du stockage sans être compté.
|
||||
manager-app crée la ligne **en annonçant** `sizeBytes`, **puis** téléverse le blob. À l'instant
|
||||
du `Create`, le blob n'existe pas encore : il n'y a rien à interroger dans Firebase. Le
|
||||
commentaire du code le dit déjà.
|
||||
|
||||
3. **`Delete` doit supprimer le blob.** Aujourd'hui il nettoie les références en base et laisse le fichier. Le quota calculé (`SUM(SizeBytes)`) diverge donc du stockage réellement facturé, et l'écart ne fait que croître.
|
||||
Le trou reste réel — `Create` croit le client, donc un appelant qui envoie `0` passe sous le
|
||||
quota définitivement — mais le correctif est une **réconciliation asynchrone**, pas une
|
||||
vérification à l'écriture :
|
||||
|
||||
`SizeBytes` doit être renseigné à la création, pas seulement sur `Update`.
|
||||
- un job Hangfire récurrent (il y en a déjà huit, [Startup.cs:359-401](../../manager-service/ManagerService/Startup.cs#L359)) qui repasse sur les blobs, lit la taille réelle et corrige `SizeBytes`, en signalant les lignes où le déclaré divergeait ;
|
||||
- la logique de sondage existe déjà dans le backfill ([:912](../../manager-service/ManagerService/Controllers/ResourceController.cs#L912)) : il s'agit de la rendre récurrente et de ne plus la limiter à `SizeBytes == 0` ;
|
||||
- il faut ajouter une lecture de taille à `IResourceBlobService`, qui n'expose aujourd'hui que `DeleteAsync` ([ResourceBlobService.cs:32](../../manager-service/ManagerService/Services/ResourceBlobService.cs#L32)).
|
||||
|
||||
### Et le garde-fou déclaratif, qui n'existe pas du tout
|
||||
|
||||
Il n'y a **aucun `storage.rules` dans le repo**. Comme manager-app téléverse directement vers
|
||||
Firebase, une règle `request.resource.size < N` est le seul contrôle qu'un client ne puisse pas
|
||||
contourner — un endpoint de pré-vol l'est par construction. Ça ne plafonne pas le total, mais
|
||||
ça bloque le fichier de 4 Go.
|
||||
|
||||
### Un plafond de taille par bucket n'existe pas — vérifié le 2026-09-07
|
||||
|
||||
Ni chez Google, ni chez OVH. À ne pas rechercher une seconde fois :
|
||||
|
||||
- **GCS / Firebase Storage** : stockage illimité au niveau bucket, seule limite 5 TiB **par objet** ([doc](https://docs.cloud.google.com/storage/quotas)). Le plafond de 1 To concerne les Rapid Buckets zonaux.
|
||||
- **OVH Object Storage S3** : 100 buckets par projet, 48 TiB par objet, nombre d'objets « unlimited », **aucun quota de taille** ([limitations](https://docs.ovhcloud.com/en/guides/storage-and-backup/object-storage/s3-limitations)).
|
||||
- L'ancienne interface **Swift** d'OVH avait un `X-Container-Meta-Quota-Bytes` — c'est Swift, pas S3, et c'est l'offre legacy. Ne s'applique pas.
|
||||
|
||||
Et même s'il existait, ce serait la mauvaise granularité : le quota est **par instance**, alors
|
||||
que tous les clients partagent un bucket préfixé (§ « Un bucket global, préfixé par instance »).
|
||||
Le quota vit en base, il ne peut pas vivre dans le stockage.
|
||||
|
||||
---
|
||||
|
||||
@ -205,7 +235,68 @@ Une fois les tailles réelles connues, certains clients seront peut-être déjà
|
||||
|
||||
### Situation
|
||||
|
||||
Bucket **en Europe**. Or les quotas gratuits Firebase Storage ne s'appliquent qu'aux régions `us-central1`, `us-west1` et `us-east1`. **Il n'y a donc aucun quota gratuit** : la facturation court depuis le premier octet.
|
||||
Bucket **en Europe** — vérifié le 2026-09-07 : `gs://mymuseum-3b97f.appspot.com` est en `EU`, `multi-region`. Or les quotas gratuits Firebase Storage ne s'appliquent qu'aux régions `us-central1`, `us-west1` et `us-east1`. **Il n'y a donc aucun quota gratuit** : la facturation court depuis le premier octet.
|
||||
|
||||
### 📏 Le volume réel, mesuré le 2026-09-07 : **1,43 Go**
|
||||
|
||||
```sh
|
||||
gcloud storage du -s gs://mymuseum-3b97f.appspot.com # 1433427569 octets
|
||||
```
|
||||
|
||||
Ce chiffre était attendu du backfill (§5) alors que **le bucket le connaissait depuis le début**.
|
||||
Le backfill reste nécessaire pour la comptabilité *par client* et l'inventaire des orphelins,
|
||||
mais plus pour dimensionner un stockage ou arbitrer un fournisseur.
|
||||
|
||||
Conséquence directe : les médias tiennent dans le bucket de sauvegarde déjà créé
|
||||
(`gs://unov-myinfomate-backups`, préfixe `media/`), et **aucun second fournisseur n'est
|
||||
nécessaire**. Voir `manager-service/ManagerService/Deployment/backup/README.md`.
|
||||
|
||||
### 🔍 Inventaire des orphelins — première moitié faite le 2026-09-07
|
||||
|
||||
Obtenu en comparant les 2408 objets du bucket aux instances réelles, sans backfill.
|
||||
**~27 objets ne peuvent appartenir à aucun client :**
|
||||
|
||||
| Quoi | Objets | Pourquoi c'est un orphelin |
|
||||
|---|---|---|
|
||||
| `pictures/63514fd67ed8c735aaa4b8f1/` | 19 | L'id finit par **f1** ; l'instance MyInfoMate est `…f2`. Un caractère d'écart — instance supprimée ou faute de frappe historique |
|
||||
| `pictures/3181820d61fb46639d234dc7/` | 2 | C'est MNAHA, qui existe en base **de dev** mais dans aucune instance de la prod Mongo |
|
||||
| Racine du bucket | 6 | `giphy.gif`, `All 24 Cybertruck Accessories Revealed!.mp4`, `file_example_{AVI,MOV,WEBM}`, `video_2023-12-13_14-49-12.mp4` — des fichiers de test |
|
||||
|
||||
Les 6 fichiers de la racine sont le cas le plus net : ils ne suivent pas le schéma
|
||||
`pictures/{instanceId}/{id}`, donc **aucun code actuel ne peut les produire ni les désigner**.
|
||||
Ils sont invisibles au quota et au `StoragePath`, et facturés depuis 2023.
|
||||
|
||||
Répartition des 2402 objets restants, par instance :
|
||||
|
||||
| Instance | Objets |
|
||||
|---|---|
|
||||
| Fort Saint Héribert (`633ee379…`) | 1204 |
|
||||
| VisitNamur (`65c5e576…`) | 711 |
|
||||
| MDLF (`65ccc672…`) | 430 |
|
||||
| MyInfoMate démo (`63514fd6…f2`) | 36 |
|
||||
|
||||
Fort Saint Héribert porte **la moitié du bucket** — cohérent avec l'instance la plus ancienne,
|
||||
et avec le fait qu'elle n'a plus rien modifié depuis avril (voir STATUS.md §4).
|
||||
|
||||
⚠️ À confirmer avant toute suppression : croiser ces 27 objets avec la table `Resources`. Le
|
||||
raisonnement ci-dessus repose sur les préfixes, pas sur les références.
|
||||
|
||||
### ✅ Versioning activé le 2026-09-07 — avant la clé, comme il fallait
|
||||
|
||||
Le bucket n'avait **ni versioning ni aucune règle de cycle de vie**. Les deux sont posés :
|
||||
|
||||
```sh
|
||||
gcloud storage buckets update gs://mymuseum-3b97f.appspot.com --versioning
|
||||
# + lifecycle : {"condition":{"daysSinceNoncurrentTime":30},"action":{"type":"Delete"}}
|
||||
```
|
||||
|
||||
L'ordre importait. Tant que `Firebase:StorageBucket` est vide, `Delete` ne supprime aucun blob
|
||||
et le risque est nul. Le jour où cette clé est renseignée, la suppression devient réelle — un
|
||||
mauvais préfixe ou un bug emporterait les médias d'un client **sans retour**. Le filet est
|
||||
désormais antérieur à ce basculement.
|
||||
|
||||
La purge des versions non courantes à 30 jours évite que les écrasements s'accumulent sans fin.
|
||||
Et le bucket portait déjà une `soft_delete_policy` de 7 jours depuis mars 2024 — seconde couche.
|
||||
|
||||
Tarifs Blaze (bucket legacy `*.appspot.com`, hors quotas gratuits) :
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user