using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TrueCV.Application.Interfaces; using TrueCV.Infrastructure.Configuration; namespace TrueCV.Infrastructure.Services; public sealed class FileStorageService : IFileStorageService { private readonly BlobContainerClient _containerClient; private readonly ILogger _logger; public FileStorageService( IOptions settings, ILogger logger) { _logger = logger; var blobServiceClient = new BlobServiceClient(settings.Value.ConnectionString); _containerClient = blobServiceClient.GetBlobContainerClient(settings.Value.ContainerName); } public async Task UploadAsync(Stream fileStream, string fileName) { ArgumentNullException.ThrowIfNull(fileStream); ArgumentException.ThrowIfNullOrWhiteSpace(fileName); var extension = Path.GetExtension(fileName); var uniqueBlobName = $"{Guid.NewGuid()}{extension}"; _logger.LogDebug("Uploading file {FileName} as blob {BlobName}", fileName, uniqueBlobName); var blobClient = _containerClient.GetBlobClient(uniqueBlobName); await _containerClient.CreateIfNotExistsAsync(); var httpHeaders = new BlobHttpHeaders { ContentType = GetContentType(extension) }; await blobClient.UploadAsync(fileStream, new BlobUploadOptions { HttpHeaders = httpHeaders, Metadata = new Dictionary { ["originalFileName"] = fileName, ["uploadedAt"] = DateTime.UtcNow.ToString("O") } }); var blobUrl = blobClient.Uri.ToString(); _logger.LogInformation("Successfully uploaded file {FileName} to {BlobUrl}", fileName, blobUrl); return blobUrl; } public async Task DownloadAsync(string blobUrl) { ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl); var blobName = ExtractBlobNameFromUrl(blobUrl); _logger.LogDebug("Downloading blob {BlobName} from {BlobUrl}", blobName, blobUrl); var blobClient = _containerClient.GetBlobClient(blobName); var response = await blobClient.DownloadStreamingAsync(); _logger.LogDebug("Successfully downloaded blob {BlobName}", blobName); return response.Value.Content; } public async Task DeleteAsync(string blobUrl) { ArgumentException.ThrowIfNullOrWhiteSpace(blobUrl); var blobName = ExtractBlobNameFromUrl(blobUrl); _logger.LogDebug("Deleting blob {BlobName}", blobName); var blobClient = _containerClient.GetBlobClient(blobName); var deleted = await blobClient.DeleteIfExistsAsync(); if (deleted) { _logger.LogInformation("Successfully deleted blob {BlobName}", blobName); } else { _logger.LogWarning("Blob {BlobName} did not exist when attempting to delete", blobName); } } private static string ExtractBlobNameFromUrl(string blobUrl) { var uri = new Uri(blobUrl); var segments = uri.Segments; // The blob name is the last segment after the container name // URL format: https://account.blob.core.windows.net/container/blobname return segments.Length > 2 ? segments[^1] : throw new ArgumentException("Invalid blob URL", nameof(blobUrl)); } private static string GetContentType(string extension) { return extension.ToLowerInvariant() switch { ".pdf" => "application/pdf", ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".doc" => "application/msword", _ => "application/octet-stream" }; } }