From 6d70e78cbd630721e85151ea0b4ddbfef19300cd Mon Sep 17 00:00:00 2001 From: Suhaib <93185683+suhaib-mousa@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:04:56 +0300 Subject: [PATCH] dynamic password retrieval via a distributed cache in BunnyClientFactory --- .../Volo.Abp.BlobStoring.Bunny.csproj | 4 +- .../Bunny/AbpBunnyBlobStoringModule .cs | 10 +- .../BlobStoring/Bunny/BunnyBlobProvider.cs | 97 ++++---- .../Volo/Abp/BlobStoring/Bunny/BunnyClient.cs | 229 ------------------ .../BlobStoring/Bunny/BunnyClientFactory.cs | 153 ++++++++++++ .../BlobStoring/Bunny/IBunnyClientFactory.cs | 10 + 6 files changed, 222 insertions(+), 281 deletions(-) delete mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj index bd16c59713..2cd9c74ac6 100644 --- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj +++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj @@ -15,10 +15,12 @@ - + + + diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs index 5c8e1c79b7..49e97e861b 100644 --- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs +++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs @@ -1,11 +1,17 @@ -using Volo.Abp.Modularity; +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Caching; +using Volo.Abp.Modularity; namespace Volo.Abp.BlobStoring.Bunny; -[DependsOn(typeof(AbpBlobStoringModule))] +[DependsOn( + typeof(AbpBlobStoringModule), + typeof(AbpCachingModule))] public class AbpBlobStoringBunnyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddHttpClient(); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs index 596f977874..b154ab2160 100644 --- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs +++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs @@ -12,13 +12,16 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency { protected IBunnyBlobNameCalculator BunnyBlobNameCalculator { get; } protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; } + protected IBunnyClientFactory BunnyClientFactory { get; } public BunnyBlobProvider( IBunnyBlobNameCalculator bunnyBlobNameCalculator, - IBlobNormalizeNamingService blobNormalizeNamingService) + IBlobNormalizeNamingService blobNormalizeNamingService, + IBunnyClientFactory bunnyClientFactory) { BunnyBlobNameCalculator = bunnyBlobNameCalculator; BlobNormalizeNamingService = blobNormalizeNamingService; + BunnyClientFactory = bunnyClientFactory; } public override async Task SaveAsync(BlobProviderSaveArgs args) @@ -27,14 +30,14 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency var containerName = GetContainerName(args); var blobName = BunnyBlobNameCalculator.Calculate(args); - using var bunnyClient = GetBunnyClient(args); - - await ValidateContainerExistsAsync(bunnyClient, containerName, configuration); + await ValidateContainerExistsAsync(containerName, configuration); - if (!args.OverrideExisting && await BlobExistsAsync(bunnyClient, containerName, blobName)) + var bunnyStorage = await GetBunnyCDNStorageAsync(args); + + if (!args.OverrideExisting && await BlobExistsAsync(bunnyStorage, containerName, blobName)) { throw new BlobAlreadyExistsException( - $"BLOB '{args.BlobName}' already exists in container '{containerName}'. " + + $"Blob '{args.BlobName}' already exists in container '{containerName}'. " + $"Set {nameof(args.OverrideExisting)} to true to overwrite."); } @@ -42,57 +45,53 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency await args.BlobStream.CopyToAsync(memoryStream); memoryStream.Position = 0; - await bunnyClient.UploadAsync(memoryStream, $"{containerName}/{blobName}"); + await bunnyStorage.UploadAsync(memoryStream, $"{containerName}/{blobName}"); } public override async Task DeleteAsync(BlobProviderDeleteArgs args) { var blobName = BunnyBlobNameCalculator.Calculate(args); - var configuration = args.Configuration.GetBunnyConfiguration(); var containerName = GetContainerName(args); - using var bunnyClient = GetBunnyClient(args); - if (!await BlobExistsAsync(bunnyClient, containerName, blobName)) + var bunnyStorage = await GetBunnyCDNStorageAsync(args); + + if (!await BlobExistsAsync(bunnyStorage, containerName, blobName)) { return false; } try { - await bunnyClient.DeleteObjectAsync($"{containerName}/{blobName}"); - return true; + return await bunnyStorage.DeleteObjectAsync($"{containerName}/{blobName}"); } catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404")) { return false; } - } public override async Task ExistsAsync(BlobProviderExistsArgs args) { var blobName = BunnyBlobNameCalculator.Calculate(args); var containerName = GetContainerName(args); - var configuration = args.Configuration.GetBunnyConfiguration(); - using var bunnyClient = GetBunnyClient(args); + var bunnyStorage = await GetBunnyCDNStorageAsync(args); - return await BlobExistsAsync(bunnyClient, containerName, blobName); + return await BlobExistsAsync(bunnyStorage, containerName, blobName); } public override async Task GetOrNullAsync(BlobProviderGetArgs args) { var blobName = BunnyBlobNameCalculator.Calculate(args); var containerName = GetContainerName(args); - var configuration = args.Configuration.GetBunnyConfiguration(); - using var bunnyClient = GetBunnyClient(args); + var bunnyStorage = await GetBunnyCDNStorageAsync(args); - if (!await BlobExistsAsync(bunnyClient, containerName, blobName)) + if (!await BlobExistsAsync(bunnyStorage, containerName, blobName)) { return null; } try { - return await bunnyClient.DownloadObjectAsStreamAsync($"{containerName}/{blobName}"); + return await bunnyStorage.DownloadObjectAsStreamAsync($"{containerName}/{blobName}"); } catch (WebException ex) when ((HttpStatusCode)ex.Status == HttpStatusCode.NotFound) { @@ -100,14 +99,11 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency } } - protected virtual async Task BlobExistsAsync(BunnyClient bunnyClient, string containerName, string blobName) + protected virtual async Task BlobExistsAsync(BunnyCDNStorage bunnyStorage, string containerName, string blobName) { try { - // Combine container name and blob name to create the full path var fullBlobPath = $"/{containerName}/{blobName}"; - - // Extract the directory path var directoryPath = Path.GetDirectoryName(fullBlobPath)?.Replace('\\', '/') + "/"; if (string.IsNullOrWhiteSpace(directoryPath)) @@ -115,9 +111,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency throw new Exception("Invalid directory path generated from blob name."); } - var objects = await bunnyClient.GetStorageObjectsAsync(directoryPath); - - // Check if the specific blob exists in the returned objects + var objects = await bunnyStorage.GetStorageObjectsAsync(directoryPath); return objects?.Any(o => o.FullPath == fullBlobPath) == true; } catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404")) @@ -129,16 +123,19 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency throw new Exception($"Error while checking blob existence: {ex.Message}", ex); } } - protected virtual BunnyClient GetBunnyClient(BlobProviderArgs args) + + protected virtual async Task GetBunnyCDNStorageAsync(BlobProviderArgs args) { - var containerName = GetContainerName(args); var configuration = args.Configuration.GetBunnyConfiguration(); - if (configuration.Region.IsNullOrEmpty()) - { - return new BunnyClient(configuration.AccessKey, containerName); - } - return new BunnyClient(configuration.AccessKey, containerName, configuration.Region); + var containerName = GetContainerName(args); + var region = configuration.Region ?? "de"; + + return await BunnyClientFactory.CreateAsync( + configuration.AccessKey, + containerName, + region); } + protected virtual string GetContainerName(BlobProviderArgs args) { var configuration = args.Configuration.GetBunnyConfiguration(); @@ -146,23 +143,25 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency ? args.ContainerName : BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName!); } + protected virtual async Task ValidateContainerExistsAsync( - BunnyClient client, - string containerName, - BunnyBlobProviderConfiguration configuration) + string containerName, + BunnyBlobProviderConfiguration configuration + ) { - if (configuration.CreateContainerIfNotExists) + try { - try - { - await client.EnsureStorageZoneExistsAsync(containerName); - } - catch (BunnyApiException ex) - { - throw new AbpException( - $"Failed to ensure Bunny storage zone '{containerName}' exists: {ex.Message}", - ex); - } + await BunnyClientFactory.EnsureStorageZoneExistsAsync( + configuration.AccessKey, + containerName, + configuration.Region ?? "de", + configuration.CreateContainerIfNotExists); + } + catch (Exception ex) + { + throw new AbpException( + $"Failed to validate storage zone '{containerName}': {ex.Message}", + ex); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs deleted file mode 100644 index cac007225b..0000000000 --- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Json; -using System.Threading.Tasks; -using BunnyCDN.Net.Storage; -using BunnyCDN.Net.Storage.Models; - -namespace Volo.Abp.BlobStoring.Bunny; - -public class BunnyClient : IDisposable -{ - private readonly string _region; - private readonly string _storageZoneName; - private readonly HttpClient _httpClient; - - private static BunnyCDNStorage? BunnyCDNStorage = null; - - public BunnyClient(string accessKey, string storageZoneName, string region = "de", HttpMessageHandler? handler = null) - { - if (string.IsNullOrWhiteSpace(accessKey)) - { - throw new ArgumentException("Account access key must not be null or empty.", nameof(accessKey)); - } - _region = region; - _storageZoneName = storageZoneName; - _httpClient = handler != null ? new HttpClient(handler) : new HttpClient(); - _httpClient.Timeout = TimeSpan.FromSeconds(120); - _httpClient.DefaultRequestHeaders.Add("AccessKey", accessKey); - } - - public async Task DoesStorageZoneExistAsync(string zoneName) - { - if (string.IsNullOrWhiteSpace(zoneName)) - { - throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName)); - } - - try - { - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("https://api.bunny.net/storagezone?page=0&perPage=1000&includeDeleted=false"), - Headers = { { "accept", "application/json" } } - }; - - using var response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var responseBody = await response.Content.ReadAsStringAsync(); - var storageZones = JsonSerializer.Deserialize>(responseBody); - - return storageZones?.Any(zone => zone.Name!.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) == true; - } - catch (HttpRequestException ex) - { - throw new BunnyApiException("Error occurred while checking storage zone existence.", ex); - } - catch (JsonException ex) - { - throw new BunnyApiException("Failed to parse the response from Bunny API.", ex); - } - } - - // Ensure the storage zone exists, creating it if not found, and return the zone - public async Task EnsureStorageZoneExistsAsync(string zoneName, string? originUrl = "") - { - try - { - if (!await DoesStorageZoneExistAsync(zoneName)) - { - return await CreateStorageZoneAsync(zoneName, originUrl); - } - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("https://api.bunny.net/storagezone?page=0&perPage=1000&includeDeleted=false"), - Headers = { { "accept", "application/json" } } - }; - - using var response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var responseBody = await response.Content.ReadAsStringAsync(); - var storageZones = JsonSerializer.Deserialize>(responseBody); - - return storageZones?.FirstOrDefault(zone => zone.Name!.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) - ?? throw new BunnyApiException($"Storage zone '{zoneName}' not found even though it should exist."); - } - catch (BunnyApiException ex) - { - throw new BunnyApiException($"Failed to ensure the existence of storage zone '{zoneName}'.", ex); - } - } - - public async Task UploadAsync(Stream stream, string path) => await (await GetBunnyCDNStorage()).UploadAsync(stream, path); - - public async Task DeleteObjectAsync(string path) => await (await GetBunnyCDNStorage()).DeleteObjectAsync(path); - - public async Task> GetStorageObjectsAsync(string path) => await (await GetBunnyCDNStorage()).GetStorageObjectsAsync(path); - - public async Task DownloadObjectAsStreamAsync(string path) => await (await GetBunnyCDNStorage()).DownloadObjectAsStreamAsync(path); - - public async Task DeleteStorageZoneAsync(string zoneName) - { - if (string.IsNullOrWhiteSpace(zoneName)) - { - throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName)); - } - var storageZone = (await GetStorageZoneAsync()); - - var request = new HttpRequestMessage - { - Method = HttpMethod.Delete, - RequestUri = new Uri($"https://api.bunny.net/storagezone/{storageZone.Id}") - }; - using (var response = await _httpClient.SendAsync(request)) - { - response.EnsureSuccessStatusCode(); - } - } - private async Task GetStorageZoneAsync() - { - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri("https://api.bunny.net/storagezone") - }; - - var response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var content = await response.Content.ReadAsStringAsync(); - var zones = JsonSerializer.Deserialize>(content); - - var targetZone = zones?.FirstOrDefault(z => - z.Name.Equals(_storageZoneName, StringComparison.OrdinalIgnoreCase)); - - return targetZone - ?? throw new AbpException( - $"Storage zone '{_storageZoneName}' is not found"); - } - private async Task CreateStorageZoneAsync(string zoneName, string? originUrl = "", int zoneTier = 0) - { - if (string.IsNullOrWhiteSpace(zoneName)) - { - throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName)); - } - - try - { - var payload = new Dictionary - { - { "ZoneTier", zoneTier }, - { "Name", zoneName }, - { "Region", _region } - }; - - // Add OriginUrl only if it is not empty - if (!string.IsNullOrEmpty(originUrl)) - { - payload.Add("OriginUrl", originUrl!); - } - - var request = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri("https://api.bunny.net/storagezone"), - Headers = { { "accept", "application/json" } }, - Content = new StringContent(JsonSerializer.Serialize(payload)) - { - Headers = { ContentType = new MediaTypeHeaderValue("application/json") } - } - }; - - using var response = await _httpClient.SendAsync(request); - if (!response.IsSuccessStatusCode) - { - var responseBody = await response.Content.ReadAsStringAsync(); - throw new BunnyApiException($"Failed to create storage zone. Response: {responseBody}"); - } - - var createdZoneJson = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(createdZoneJson) - ?? throw new BunnyApiException("Failed to deserialize the created storage zone response."); - } - catch (HttpRequestException ex) - { - throw new BunnyApiException("Error occurred while creating storage zone.", ex); - } - catch (JsonException ex) - { - throw new BunnyApiException("Failed to parse the response for the created storage zone.", ex); - } - } - private async Task GetBunnyCDNStorage() - { - if (BunnyCDNStorage != null) { - return BunnyCDNStorage; - } - var storageZonePassword = (await GetStorageZoneAsync()).Password; - BunnyCDNStorage = new BunnyCDNStorage(_storageZoneName, storageZonePassword, _region); - - return BunnyCDNStorage; - } - - public void Dispose() => _httpClient?.Dispose(); - - public class StorageZone - { - public int Id { get; set; } - public string Password { get; set; } = null!; - public string Name { get; set; } = null!; - public string? Region { get; set; } = null!; - public bool Deleted { get; set; } - } -} - -public class BunnyApiException : Exception -{ - public BunnyApiException(string message) : base(message) { } - - public BunnyApiException(string message, Exception innerException) : base(message, innerException) { } -} diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs new file mode 100644 index 0000000000..e4fb3d8fd5 --- /dev/null +++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using BunnyCDN.Net.Storage; +using Microsoft.Extensions.Caching.Distributed; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BlobStoring.Bunny; + +public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency +{ + private readonly IDistributedCache _cache; + private readonly IHttpClientFactory _httpClientFactory; + private const string CacheKeyPrefix = "BunnyStorageZone:"; + private static TimeSpan CacheDuration = TimeSpan.FromHours(12); + + public BunnyClientFactory( + IHttpClientFactory httpClient, + IDistributedCache cache) + { + _cache = cache; + _httpClientFactory = httpClient; + } + + public async Task CreateAsync(string accessKey, string containerName, string region = "de") + { + var cacheKey = $"{CacheKeyPrefix}{containerName}"; + var storageZoneInfo = await _cache.GetOrAddAsync( + cacheKey, + async () => { + var result = await GetStorageZoneAsync(accessKey, containerName); + if (result == null) + { + throw new AbpException($"Storage zone '{containerName}' not found"); + } + return result; + }, + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.Add(CacheDuration) + } + ); + + if (storageZoneInfo == null) + { + throw new AbpException($"Could not retrieve storage zone information for container '{containerName}'"); + } + + return new BunnyCDNStorage(containerName, storageZoneInfo.Password, region); + } + + public async Task EnsureStorageZoneExistsAsync( + string accessKey, + string containerName, + string region = "de", + bool createIfNotExists = false) + { + var storageZone = await GetStorageZoneAsync(accessKey, containerName); + + if (storageZone == null) + { + if (!createIfNotExists) + { + throw new AbpException( + $"Storage zone '{containerName}' does not exist. " + + "Set createIfNotExists to true to create it automatically."); + } + + await CreateStorageZoneAsync(accessKey, containerName, region); + + // Clear the cache to force a refresh of the storage zone info + var cacheKey = $"{CacheKeyPrefix}{containerName}"; + await _cache.RemoveAsync(cacheKey); + } + } + + private async Task CreateStorageZoneAsync( + string accessKey, + string containerName, + string region) + { + using var _client = _httpClientFactory.CreateClient("BunnyApiClient"); + _client.DefaultRequestHeaders.Add("AccessKey", accessKey); + + var payload = new Dictionary + { + { "Name", containerName }, + { "Region", region }, + { "ZoneTier", 0 } + }; + + var content = new StringContent( + JsonSerializer.Serialize(payload), + Encoding.UTF8, + "application/json"); + + var response = await _client.PostAsync( + "https://api.bunny.net/storagezone", + content); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new AbpException( + $"Failed to create storage zone '{containerName}'. " + + $"Status: {response.StatusCode}, Error: {errorContent}"); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var createdZone = JsonSerializer.Deserialize(responseContent); + + if (createdZone == null) + { + throw new AbpException( + $"Failed to deserialize the created storage zone response for '{containerName}'"); + } + + return createdZone; + } + + private async Task GetStorageZoneAsync(string accessKey, string containerName) + { + using var _client = _httpClientFactory.CreateClient("BunnyApiClient"); + _client.DefaultRequestHeaders.Add("AccessKey", accessKey); + var response = await _client.GetAsync("https://api.bunny.net/storagezone"); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + var zones = JsonSerializer.Deserialize(content); + + return Array.Find(zones!, z => + z.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !z.Deleted); + } + + [Serializable] + public class StorageZone + { + public int Id { get; set; } + public string Password { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Region { get; set; } + public bool Deleted { get; set; } + } +} +public class BunnyApiException : Exception +{ + public BunnyApiException(string message) : base(message) { } + public BunnyApiException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs new file mode 100644 index 0000000000..dce2c5b6f0 --- /dev/null +++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using BunnyCDN.Net.Storage; + +namespace Volo.Abp.BlobStoring.Bunny; + +public interface IBunnyClientFactory +{ + Task CreateAsync(string accessKey, string containerName, string region = "de"); + Task EnsureStorageZoneExistsAsync(string accessKey, string containerName, string region = "de", bool createIfNotExists = false); +}