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);
+}