121 lines
3.8 KiB
C#
121 lines
3.8 KiB
C#
|
|
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"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|