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
|
## 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
|
### 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) :
|
Tarifs Blaze (bucket legacy `*.appspot.com`, hors quotas gratuits) :
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user