using Azure; using Azure.Storage; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; using Azure.Storage.Sas; using Core.Blueprint.Storage.Adapters; using Core.Blueprint.Storage.Contracts; using Microsoft.Extensions.Configuration; using System.Threading.Tasks; namespace Core.Blueprint.Storage.Provider { public sealed class BlobStorageProvider : IBlobStorageProvider { private readonly BlobServiceClient _blobServiceClient; private readonly BlobContainerClient _blobContainerClient; private readonly string _containerName; private readonly Trie _trie = new Trie(); private readonly IConfiguration _configuration; public BlobStorageProvider(BlobServiceClient blobServiceClient, IConfiguration configuration) { _blobServiceClient = blobServiceClient; _configuration = configuration; _containerName = configuration.GetSection("BlobStorage:ContainerName").Value ?? ""; if (string.IsNullOrEmpty(_containerName)) throw new ArgumentException("Blob container cannot be null or empty."); _blobContainerClient = blobServiceClient.GetBlobContainerClient(_containerName); } /// /// Creates the blob container if it does not exist. /// public async Task> CreateIfNotExistsAsync() { return await _blobContainerClient.CreateIfNotExistsAsync(); } /// /// Deletes the blob container if it exists. /// public async Task DeleteIfExistsAsync() { await _blobContainerClient.DeleteIfExistsAsync(); } /// /// Gets properties of the blob container. /// public async Task> GetPropertiesAsync() { return await _blobContainerClient.GetPropertiesAsync(); } /// /// Sets metadata for the blob container. /// public async Task SetMetadataAsync(IDictionary metadata) { await _blobContainerClient.SetMetadataAsync(metadata); } /// /// Uploads a blob to the container. /// public async Task> UploadBlobAsync(string blobName, Stream content) { var blobClient = _blobContainerClient.GetBlobClient(blobName); return await blobClient.UploadAsync(content, overwrite: true); } /// /// Downloads a blob from the container. /// public async Task> DownloadBlobAsync(string blobName) { var blobClient = _blobContainerClient.GetBlobClient(blobName); return await blobClient.DownloadAsync(); } /// /// Deletes a blob from the container. /// public async Task DeleteBlobAsync(string blobName) { var blobClient = _blobContainerClient.GetBlobClient(blobName); return await blobClient.DeleteIfExistsAsync(); } /// /// Lists all blobs in the container with an optional prefix. /// public async Task> ListBlobItemAsync(string? prefix = null) { var blobs = new List(); await foreach (var blobItem in _blobContainerClient.GetBlobsAsync(prefix: prefix)) { blobs.Add(blobItem); } return blobs; } /// /// Retrieves the account information for the associated Blob Service Client. /// /// /// A that can be used to cancel the operation. /// /// /// A task that represents the asynchronous operation. The task result contains the /// object, which provides details about the account, such as the SKU /// and account kind. public async Task GetAccountInfoAsync(CancellationToken cancellation) { return await _blobServiceClient.GetAccountInfoAsync(cancellation); } /// /// Gets a blob client for a specific blob. /// public BlobClient GetBlobClient(string blobName) { return _blobContainerClient.GetBlobClient(blobName); } /// /// Lists blobs hierarchically using a delimiter. /// public async Task> ListBlobsByHierarchyAsync(string? prefix = null, string delimiter = "/") { var blobs = new List(); await foreach (var blobHierarchyItem in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: delimiter)) { blobs.Add(blobHierarchyItem); } return blobs; } /// /// Generates a SAS token for the container with specified permissions. /// public Uri GenerateContainerSasUri(BlobContainerSasPermissions permissions, DateTimeOffset expiresOn) { if (!_blobContainerClient.CanGenerateSasUri) { throw new InvalidOperationException("Cannot generate SAS URI. Ensure the client is authorized with account key credentials."); } var sasBuilder = new BlobSasBuilder { BlobContainerName = _blobContainerClient.Name, Resource = "c", // c for container ExpiresOn = expiresOn }; sasBuilder.SetPermissions(permissions); return _blobContainerClient.GenerateSasUri(sasBuilder); } /// /// Acquires a lease on the blob container. /// public async Task> AcquireLeaseAsync(string? proposedId = null, TimeSpan? duration = null) { return await _blobContainerClient.GetBlobLeaseClient(proposedId).AcquireAsync(duration ?? TimeSpan.FromSeconds(60)); } /// /// Releases a lease on the blob container. /// public async Task ReleaseLeaseAsync(string leaseId) { await _blobContainerClient.GetBlobLeaseClient(leaseId).ReleaseAsync(); } /// /// Sets access policies for the blob container. /// public async Task SetAccessPolicyAsync(PublicAccessType accessType, IEnumerable? identifiers = null) { await _blobContainerClient.SetAccessPolicyAsync(accessType, identifiers); } /// /// Lists blobs in the container with an optional prefix. /// public async Task> ListBlobsAsync(string? prefix = null) { var blobs = new List(); await foreach (BlobItem blob in _blobContainerClient.GetBlobsAsync(prefix: prefix)) { blobs.Add(new BlobFileAdapter { Name = blob.Name, Uri = $"{_blobContainerClient.Uri}/{blob.Name}", ContentType = blob.Properties.ContentType, Size = blob.Properties.ContentLength, DateUpload = blob.Properties.LastModified?.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), ShortDate = blob.Properties.LastModified?.UtcDateTime.ToString("MM-dd-yyyy"), Status = "Available" }); } return blobs; } /// /// Uploads a blob to the container. /// public async Task UploadBlobAsync(BlobAddDto newBlob) { var blobClient = _blobContainerClient.GetBlobClient(newBlob.FileName); using var stream = new MemoryStream(newBlob.FileContent); await blobClient.UploadAsync(stream, overwrite: true); var properties = await blobClient.GetPropertiesAsync(); return new BlobFileAdapter { Name = newBlob.FileName ?? "", Uri = blobClient.Uri.ToString(), ContentType = properties.Value.ContentType, Size = properties.Value.ContentLength, DateUpload = properties.Value.LastModified.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), ShortDate = properties.Value.LastModified.UtcDateTime.ToString("MM-dd-yyyy"), Status = "Uploaded" }; } /// /// Deletes a blob from the container. /// public async Task DeleteBlobsAsync(string fileName) { var blobClient = _blobContainerClient.GetBlobClient(fileName); if (await blobClient.ExistsAsync()) { var properties = await blobClient.GetPropertiesAsync(); var _response = await blobClient.DeleteIfExistsAsync(); return new BlobFileAdapter { Name = fileName, Uri = blobClient.Uri.ToString(), ContentType = properties.Value.ContentType, Size = properties.Value.ContentLength, DateUpload = properties.Value.LastModified.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss"), ShortDate = properties.Value.LastModified.UtcDateTime.ToString("MM-dd-yyyy"), Status = _response ? "Deleted" : "Failed to delete" }; } return null; } /// /// Downloads a blob's content. /// public async Task DownloadBlobsAsync(string blobName) { var blobClient = _blobContainerClient.GetBlobClient(blobName); if (!await blobClient.ExistsAsync()) { throw new FileNotFoundException($"Blob '{blobName}' does not exist in the container '{_containerName}'."); } return await blobClient.DownloadAsync(); } /// /// Generates a secure download URI for a specified blob in the storage container. /// /// The name of the blob for which the download URI is being generated. /// /// An instance of containing the generated URI, blob name, and status, /// or null if the blob does not exist. /// /// /// The generated URI includes a Shared Access Signature (SAS) token, which allows secure, time-limited access to the blob. /// The SAS token grants read-only access to the blob for a duration of 5 minutes starting from the current time. /// /// Thrown if is null or empty. /// Thrown if there is an issue communicating with the Azure Blob service. public async ValueTask GenerateBlobDownloadUri(string blobName) { if (string.IsNullOrWhiteSpace(blobName)) throw new ArgumentNullException(nameof(blobName), "Blob name cannot be null or empty."); var blob = _blobContainerClient.GetBlobClient(blobName); if (!await blob.ExistsAsync()) return null; var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; if (environment == "Local") { return GenerateDownloadUri(blob); } var delegationKey = await _blobServiceClient.GetUserDelegationKeyAsync( DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddHours(2)); var sasBuilder = new BlobSasBuilder { BlobContainerName = blob.BlobContainerName, BlobName = blob.Name, Resource = "b", StartsOn = DateTimeOffset.UtcNow, ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5) }; sasBuilder.SetPermissions(BlobSasPermissions.Read); sasBuilder.Protocol = SasProtocol.Https; var blobUriBuilder = new BlobUriBuilder(blob.Uri) { Sas = sasBuilder.ToSasQueryParameters(delegationKey, _blobServiceClient.AccountName) }; return new BlobDownloadUriAdapter { Uri = blobUriBuilder.ToUri(), Name = blob.Name, Status = "Available" }; } /// /// Generates a download URI for a blob using a Shared Access Signature in local (Azurite) environment. /// /// The blob client for which the URI is being generated. /// An instance of containing the SAS URI and metadata. private BlobDownloadUriAdapter GenerateDownloadUri(BlobClient blob) { var sasBuilder = new BlobSasBuilder { BlobContainerName = blob.BlobContainerName, BlobName = blob.Name, Resource = "b", StartsOn = DateTimeOffset.UtcNow, ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5) }; sasBuilder.SetPermissions(BlobSasPermissions.Read); sasBuilder.Protocol = SasProtocol.HttpsAndHttp; var accountName = _configuration["BlobStorage:AccountName"]; var accountKey = _configuration["BlobStorage:AccountKey"]; var storageCredentials = new StorageSharedKeyCredential(accountName, accountKey); var sasToken = sasBuilder.ToSasQueryParameters(storageCredentials); var blobUriBuilder = new BlobUriBuilder(blob.Uri) { Sas = sasToken }; return new BlobDownloadUriAdapter { Uri = blobUriBuilder.ToUri(), Name = blob.Name, Status = "Available" }; } /// /// Retrieves the hierarchical folder structure. /// public async Task> GetFolderHierarchyAsync(string prefix) { var rootFolder = new BlobStorageFolder { Name = prefix }; await PopulateFolderAsync(rootFolder, prefix); return new List { rootFolder }; } private async Task PopulateFolderAsync(BlobStorageFolder folder, string? prefix) { await foreach (var blobHierarchy in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/")) { if (blobHierarchy.IsPrefix) { var subFolder = new BlobStorageFolder { Name = blobHierarchy.Prefix.TrimEnd('/') }; folder.SubFolders.Add(subFolder); await PopulateFolderAsync(subFolder, blobHierarchy.Prefix); } else { folder.Files.Add(new BlobStorageFilesAdapter(Content: blobHierarchy.Prefix, //Fix Name: blobHierarchy.Blob.Name, ContentType: "", DownloadUrl: $"{_blobContainerClient.Uri}/{blobHierarchy.Blob.Name}")); } } } public async Task>> ListNeighborFoldersAsync(string? prefix) { await ListFoldersInTrieAsync(prefix); var groupedFolders = _trie.SearchByPrefix(prefix) .OrderBy(folder => folder) .GroupBy(folder => folder.Substring(0, 1)) .ToDictionary(group => group.Key, group => group.ToList()); return groupedFolders; } private async Task ListFoldersInTrieAsync(string? prefix) { await foreach (var blobHierarchy in _blobContainerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/")) { if (blobHierarchy.IsPrefix) { var folderName = blobHierarchy.Prefix.TrimEnd('/').Split('/').Last(); _trie.Insert(folderName); } } } } }