Initial commit: TrueCV CV verification platform
Clean architecture solution with: - Domain: Entities (User, CVCheck, CVFlag, CompanyCache) and Enums - Application: Service interfaces, DTOs, and models - Infrastructure: EF Core, Identity, Hangfire, external API clients, services - Web: Blazor Server UI with pages and components Features: - CV upload and parsing (PDF/DOCX) using Claude API - Employment verification against Companies House API - Timeline analysis for gaps and overlaps - Veracity scoring algorithm - Background job processing with Hangfire - Azure Blob Storage for file storage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
120
src/TrueCV.Infrastructure/Services/FileStorageService.cs
Normal file
120
src/TrueCV.Infrastructure/Services/FileStorageService.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
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<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);
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user