Files
RealCV/src/RealCV.Infrastructure/Services/FileStorageService.cs
Peter Foster 998e9a8ab8 Rename project to RealCV with new logo and font updates
- Rename all TrueCV references to RealCV across the codebase
- Add new transparent RealCV logo
- Switch from JetBrains Mono to Inter font for better number clarity
- Update solution, project files, and namespaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 15:07:20 +00:00

134 lines
4.3 KiB
C#

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<FileStorageService> _logger;
public FileStorageService(
IOptions<AzureBlobSettings> settings,
ILogger<FileStorageService> logger)
{
_logger = logger;
var blobServiceClient = new BlobServiceClient(settings.Value.ConnectionString);
_containerClient = blobServiceClient.GetBlobContainerClient(settings.Value.ContainerName);
}
public async Task<string> 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<string, string>
{
["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<Stream> 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"
};
}
}