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>
This commit is contained in:
133
src/RealCV.Infrastructure/Services/FileStorageService.cs
Normal file
133
src/RealCV.Infrastructure/Services/FileStorageService.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user