using System; using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; using Google.Cloud.Storage.V1; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace ManagerService.Services { public enum BlobDeleteOutcome { /// Objet supprimé. Deleted, /// /// Rien à supprimer : blob jamais téléversé, ou déjà supprimé. Ce n'est pas un /// échec — c'est même l'état recherché, donc l'appelant peut continuer. /// AlreadyAbsent, /// /// Le bucket n'est pas configuré. La suppression est alors sautée, jamais /// silencieusement réussie : un environnement mal configuré ne doit pas /// ressembler à un environnement qui nettoie. /// NotConfigured, /// Le bucket a répondu autre chose. L'appelant ne doit pas supprimer la ligne. Failed } public interface IResourceBlobService { bool IsConfigured { get; } Task DeleteAsync(string storagePath); } /// /// Suppression d'un blob dans le bucket, côté serveur. /// /// Jusqu'ici seul manager-app supprimait, après avoir supprimé la ligne et en /// avalant l'échec dans un print : un échec laissait un orphelin définitif, et /// comme sa ligne n'existait plus, le quota ne le comptait pas — il occupait des /// octets facturés que personne ne voyait. /// /// Aucun secret nouveau : le credential est celui que Startup charge déjà pour les /// notifications push (Firebase:CredentialsPath). Seul le nom du bucket s'ajoute. /// public class ResourceBlobService : IResourceBlobService { private readonly ILogger _logger; private readonly string _bucket; private readonly Lazy _client; public ResourceBlobService(IConfiguration configuration, ILogger logger) { _logger = logger; _bucket = configuration["Firebase:StorageBucket"]; var credentialsPath = configuration["Firebase:CredentialsPath"]; // Lazy : ne pas lire le fichier de credentials au démarrage pour un service // dont la plupart des requêtes n'ont pas besoin, et ne pas faire échouer le // boot d'un environnement de développement qui n'a pas le fichier. _client = new Lazy(() => StorageClient.Create(GoogleCredential.FromFile(credentialsPath))); IsConfigured = !string.IsNullOrEmpty(_bucket) && !string.IsNullOrEmpty(credentialsPath) && System.IO.File.Exists(credentialsPath); } public bool IsConfigured { get; } public async Task DeleteAsync(string storagePath) { if (string.IsNullOrEmpty(storagePath)) return BlobDeleteOutcome.AlreadyAbsent; if (!IsConfigured) return BlobDeleteOutcome.NotConfigured; try { await _client.Value.DeleteObjectAsync(_bucket, storagePath); return BlobDeleteOutcome.Deleted; } catch (Google.GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) { return BlobDeleteOutcome.AlreadyAbsent; } catch (Exception ex) { _logger.LogError(ex, "Suppression du blob {StoragePath} en échec sur le bucket {Bucket}", storagePath, _bucket); return BlobDeleteOutcome.Failed; } } } }