- 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>
134 lines
4.3 KiB
C#
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"
|
|
};
|
|
}
|
|
}
|