using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; namespace ManagerService.Helpers { /// /// Sonde le poids d'un blob par requête HEAD sur son URL publique, par lots. /// /// Deux appelants : MigrationController (écart e de la bascule) et le backfill /// des lignes existantes. Comme , c'est un calculateur /// unique plutôt que deux implémentations qui divergeraient — ici sur la taille de lot, /// le délai d'attente et surtout le sort réservé aux échecs. /// /// ⚠️ Le serveur n'a aucun client de bucket : ni Firebase Admin ni Google.Cloud.Storage /// ne sont référencés dans le projet, les blobs étant téléversés depuis manager-app /// directement. L'URL publique est donc le seul moyen de connaître une taille. /// public static class ResourceSizeProbe { /// Trente requêtes en vol à la fois, valeur héritée de la migration. public const int BatchSize = 30; public sealed class Result { private readonly ConcurrentDictionary _sizes = new(); private readonly ConcurrentDictionary _unsized = new(); public bool TryGetSize(string id, out long size) => _sizes.TryGetValue(id, out size); /// /// Vrai si la sonde n'a pas su donner de taille : HEAD en échec, ou réponse /// sans Content-Length. Les deux cas laissent la ressource à 0 octet, /// donc invisible au quota — l'appelant doit le signaler, pas l'avaler. /// public bool IsUnsized(string id) => _unsized.ContainsKey(id); public int SizedCount => _sizes.Count; public int UnsizedCount => _unsized.Count; internal void Record(string id, long size) => _sizes[id] = size; internal void RecordUnsized(string id) => _unsized[id] = true; } /// /// Le est fourni par l'appelant, qui reste maître du /// délai d'attente : la migration accepte 10 s par ressource, un backfill lancé /// à la main peut vouloir plus. /// public static async Task ProbeAsync(HttpClient client, IEnumerable<(string Id, string Url)> targets) { var result = new Result(); var list = targets.Where(t => !string.IsNullOrEmpty(t.Url)).ToList(); for (int i = 0; i < list.Count; i += BatchSize) { var batch = list.Skip(i).Take(BatchSize); await Task.WhenAll(batch.Select(async target => { try { var request = new HttpRequestMessage(HttpMethod.Head, target.Url); var response = await client.SendAsync(request); // Un 404 ne lève pas : il répond simplement sans Content-Length. // Le traiter comme un échec est le seul moyen de ne pas confondre // « blob absent » et « blob de 0 octet ». if (response.Content.Headers.ContentLength.HasValue) result.Record(target.Id, response.Content.Headers.ContentLength.Value); else result.RecordUnsized(target.Id); } catch { result.RecordUnsized(target.Id); } })); } return result; } } }