using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RealCV.Application.Interfaces; using RealCV.Infrastructure.Configuration; namespace RealCV.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); // Download to memory stream to ensure proper resource management // The caller will own and dispose this stream var memoryStream = new MemoryStream(); await blobClient.DownloadToAsync(memoryStream); memoryStream.Position = 0; _logger.LogDebug("Successfully downloaded blob {BlobName}", blobName); return memoryStream; } 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) { if (!Uri.TryCreate(blobUrl, UriKind.Absolute, out var uri)) { throw new ArgumentException($"Invalid blob URL format: '{blobUrl}'", nameof(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 if (segments.Length <= 2) { throw new ArgumentException($"Blob URL does not contain a valid blob name: '{blobUrl}'", nameof(blobUrl)); } return segments[^1]; } 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" }; } }