using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PowderCoating.Application.Configuration;
using PowderCoating.Application.Interfaces;
namespace PowderCoating.Application.Services;
///
/// Azure Blob Storage wrapper that provides a uniform, result-tuple API for blob operations used
/// throughout the application (company logos, quote photos, documents). Containers are created
/// on first use with private access so blobs are never publicly URL-accessible; all serving goes
/// through controller actions that perform authorization checks before streaming bytes.
/// All methods catch exceptions and return a failure result rather than propagating so that blob
/// storage errors degrade gracefully without crashing the request pipeline.
///
public class AzureBlobStorageService : IAzureBlobStorageService
{
private readonly BlobServiceClient _blobServiceClient;
private readonly ILogger _logger;
///
/// Initializes the service using the storage connection string from .
/// The is created once and reused across requests because it is
/// registered as a singleton in DI, matching the thread-safe design of the Azure SDK client.
///
public AzureBlobStorageService(
IOptions settings,
ILogger logger)
{
_blobServiceClient = new BlobServiceClient(settings.Value.ConnectionString);
_logger = logger;
}
///
/// Uploads a stream to the specified blob path, setting the Content-Type header so that the
/// Azure portal and download responses report the correct MIME type. The container is created
/// with private access if it does not already exist, preventing accidental public exposure.
///
public async Task<(bool Success, string ErrorMessage)> UploadAsync(
string containerName,
string blobName,
Stream content,
string contentType)
{
try
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
await containerClient.CreateIfNotExistsAsync(PublicAccessType.None);
var blobClient = containerClient.GetBlobClient(blobName);
var uploadOptions = new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = contentType }
};
await blobClient.UploadAsync(content, uploadOptions);
_logger.LogInformation("Uploaded blob {BlobName} to container {Container}", blobName, containerName);
return (true, string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error uploading blob {BlobName} to container {Container}", blobName, containerName);
return (false, "An error occurred while uploading the file.");
}
}
///
/// Downloads a blob's full content into memory and returns the byte array along with the
/// stored Content-Type. Checks blob existence before attempting download to return a clean
/// "not found" failure rather than surfacing an Azure SDK exception to the caller.
///
public async Task<(bool Success, byte[] Content, string ContentType, string ErrorMessage)> DownloadAsync(
string containerName,
string blobName)
{
try
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
var blobClient = containerClient.GetBlobClient(blobName);
if (!await blobClient.ExistsAsync())
{
return (false, Array.Empty(), string.Empty, "File not found.");
}
var response = await blobClient.DownloadContentAsync();
var content = response.Value.Content.ToArray();
var contentType = response.Value.Details.ContentType ?? "application/octet-stream";
return (true, content, contentType, string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading blob {BlobName} from container {Container}", blobName, containerName);
return (false, Array.Empty(), string.Empty, "An error occurred while retrieving the file.");
}
}
///
/// Deletes a blob if it exists, returning success in both the deleted and not-found cases.
/// Using DeleteIfExistsAsync makes the operation idempotent so callers do not need to
/// check existence before deleting (e.g., during temp photo cleanup after quote promotion).
///
public async Task<(bool Success, string ErrorMessage)> DeleteAsync(
string containerName,
string blobName)
{
try
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
var blobClient = containerClient.GetBlobClient(blobName);
var deleted = await blobClient.DeleteIfExistsAsync();
if (deleted)
_logger.LogInformation("Deleted blob {BlobName} from container {Container}", blobName, containerName);
return (true, string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting blob {BlobName} from container {Container}", blobName, containerName);
return (false, "An error occurred while deleting the file.");
}
}
///
/// Checks whether a blob exists without downloading its content.
/// Used by controllers to determine whether to show a file download link or a
/// "no file available" placeholder, avoiding a full download just for an existence check.
/// Returns false on exception so that storage errors do not surface as unhandled faults.
///
public async Task ExistsAsync(string containerName, string blobName)
{
try
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
var blobClient = containerClient.GetBlobClient(blobName);
var response = await blobClient.ExistsAsync();
return response.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking existence of blob {BlobName} in container {Container}", blobName, containerName);
return false;
}
}
///
/// Lists all blob names in a container that share the given prefix, used by
/// to enumerate all photos belonging to a temp session or a
/// specific quote. Returns an empty enumerable on exception so callers can safely iterate
/// without null checks.
///
public async Task> ListBlobsByPrefixAsync(string containerName, string prefix)
{
try
{
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
var names = new List();
await foreach (var item in containerClient.GetBlobsAsync(prefix: prefix))
names.Add(item.Name);
return names;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error listing blobs with prefix {Prefix} in container {Container}", prefix, containerName);
return [];
}
}
}