From f0bf9ff6623ca4d829b8b350ca1203a1068d6d9a Mon Sep 17 00:00:00 2001
From: Suhaib <93185683+suhaib-mousa@users.noreply.github.com>
Date: Sat, 25 Jan 2025 18:44:54 +0300
Subject: [PATCH 1/5] implement Bunny.net blob storage provider
---
Directory.Packages.props | 1 +
.../infrastructure/blob-storing/bunny.md | 63 +++++
.../infrastructure/blob-storing/index.md | 1 +
framework/Volo.Abp.sln | 15 ++
.../FodyWeavers.xml | 3 +
.../FodyWeavers.xsd | 30 +++
.../Volo.Abp.BlobStoring.Bunny.abppkg | 3 +
....Abp.BlobStoring.Bunny.abppkg.analyze.json | 68 ++++++
.../Volo.Abp.BlobStoring.Bunny.csproj | 24 ++
.../Bunny/AbpBunnyBlobStoringModule .cs | 11 +
...nnyBlobContainerConfigurationExtensions.cs | 24 ++
.../Bunny/BunnyBlobNamingNormalizer.cs | 51 ++++
.../BlobStoring/Bunny/BunnyBlobProvider.cs | 168 +++++++++++++
.../Bunny/BunnyBlobProviderConfiguration.cs | 40 +++
.../BunnyBlobProviderConfigurationNames.cs | 15 ++
.../Volo/Abp/BlobStoring/Bunny/BunnyClient.cs | 229 ++++++++++++++++++
.../Bunny/DefaultBunnyBlobNameCalculator.cs | 21 ++
.../Bunny/IBunnyBlobNameCalculator.cs | 6 +
.../Volo.Abp.BlobStoring.Bunny.Tests.abppkg | 3 +
.../Volo.Abp.BlobStoring.Bunny.Tests.csproj | 19 ++
.../Bunny/AbpBlobStoringBunnyTestBase.cs | 19 ++
.../Bunny/AbpBlobStoringBunnyTestModule.cs | 73 ++++++
.../Bunny/BunnyBlobContainer_Tests.cs | 1 +
.../Bunny/BunnyBlobNameCalculator_Tests.cs | 56 +++++
...BunnyBlobNamingNormalizerProvider_Tests.cs | 57 +++++
25 files changed, 1001 insertions(+)
create mode 100644 docs/en/framework/infrastructure/blob-storing/bunny.md
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs
create 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/DefaultBunnyBlobNameCalculator.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs
create mode 100644 framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8cd134d1d8..eeee7a7ff3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -15,6 +15,7 @@
+
diff --git a/docs/en/framework/infrastructure/blob-storing/bunny.md b/docs/en/framework/infrastructure/blob-storing/bunny.md
new file mode 100644
index 0000000000..59aa1eb60c
--- /dev/null
+++ b/docs/en/framework/infrastructure/blob-storing/bunny.md
@@ -0,0 +1,63 @@
+# BLOB Storing Bunny Provider
+
+BLOB Storing Bunny Provider can store BLOBs in [bunny.net Storage](https://bunny.net/storage/).
+
+> Read the [BLOB Storing document](../blob-storing) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Bunny BLOB as the storage provider.
+
+## Installation
+
+Use the ABP CLI to add [Volo.Abp.BlobStoring.Bunny](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Bunny) NuGet package to your project:
+
+* Install the [ABP CLI](../../../cli) if you haven't installed before.
+* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Bunny` package.
+* Run `abp add-package Volo.Abp.BlobStoring.Bunny` command.
+
+If you want to do it manually, install the [Volo.Abp.BlobStoring.Bunny](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Bunny) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringBunnyModule))]` to the [ABP module](../../architecture/modularity/basics.md) class inside your project.
+
+## Configuration
+
+Configuration is done in the `ConfigureServices` method of your [module](../../architecture/modularity/basics.md) class, as explained in the [BLOB Storing document](../blob-storing).
+
+**Example: Configure to use the Bunny storage provider by default**
+
+````csharp
+Configure(options =>
+{
+ options.Containers.ConfigureDefault(container =>
+ {
+ container.UseBunny(Bunny =>
+ {
+ Bunny.AccessKey = "your Bunny account access key";
+ Bunny.Region = "the code of the main storage zone region"; // "de" is the default value
+ Bunny.ContainerName = "your bunny storage zone name";
+ Bunny.CreateContainerIfNotExists = true;
+ });
+ });
+});
+
+````
+
+> See the [BLOB Storing document](../blob-storing) to learn how to configure this provider for a specific container.
+
+### Options
+
+* **AccessKey** (string): Bunny Account Access Key. [Where do I find my Access key?](https://support.bunny.net/hc/en-us/articles/360012168840-Where-do-I-find-my-API-key)
+* **Region** (string?): The code of the main storage zone region (Possible values: DE, NY, LA, SG).
+* **ContainerName** (string): You can specify the container name in Bunny. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](../blob-storing)). Please note that Bunny has some **rules for naming containers**:
+ * Storage Zone names must be a globaly unique.
+ * Storage Zone names must be between **4** and **64** characters long.
+ * Storage Zone names can consist only of **lowercase** letters, numbers, and hyphens (-).
+* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Bunny, `BunnyBlobProvider` will try to create it.
+
+## Bunny Blob Name Calculator
+
+Bunny Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default:
+
+* Appends `host` string if [current tenant](../../architecture/multi-tenancy) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](../blob-storing) to learn how to disable multi-tenancy for a container).
+* Appends `tenants/` string if current tenant is not `null`.
+* Appends the BLOB name.
+
+## Other Services
+
+* `BunnyBlobProvider` is the main service that implements the Bunny BLOB storage provider, if you want to override/replace it via [dependency injection](../../fundamentals/dependency-injection.md) (don't replace `IBlobProvider` interface, but replace `BunnyBlobProvider` class).
+* `IBunnyBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultBunnyBlobNameCalculator` by default.
diff --git a/docs/en/framework/infrastructure/blob-storing/index.md b/docs/en/framework/infrastructure/blob-storing/index.md
index f95a90e410..676757ad80 100644
--- a/docs/en/framework/infrastructure/blob-storing/index.md
+++ b/docs/en/framework/infrastructure/blob-storing/index.md
@@ -23,6 +23,7 @@ The ABP has already the following storage provider implementations:
* [Minio](./minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/).
* [Aws](./aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/).
* [Google](./google.md): Stores BLOBs on the [Google Cloud Storage](https://cloud.google.com/storage).
+* [Bunny](./bunny.md): Stores BLOBs on the [Bunny.net Storage](https://bunny.net/storage/).
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](./custom-provider.md) and [contribute](../../../contribution) to the ABP.
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index c3ca5b63b2..ca471bb304 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -470,6 +470,7 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj", "{2F9BA650-395C-4BE0-8CCB-9978E753562A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj", "{7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google", "src\Volo.Abp.BlobStoring.Google\Volo.Abp.BlobStoring.Google.csproj", "{DEEB5200-BBF9-464D-9B7E-8FC035A27E94}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google.Tests", "test\Volo.Abp.BlobStoring.Google.Tests\Volo.Abp.BlobStoring.Google.Tests.csproj", "{40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}"
@@ -480,6 +481,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud.Tests", "test\Volo.Abp.Sms.TencenCloud.Tests\Volo.Abp.Sms.TencentCloud.Tests.csproj", "{C753DDD6-5699-45F8-8669-08CE0BB816DE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny", "src\Volo.Abp.BlobStoring.Bunny\Volo.Abp.BlobStoring.Bunny.csproj", "{1BBCBA72-CDB6-4882-96EE-D4CD149433A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny.Tests", "test\Volo.Abp.BlobStoring.Bunny.Tests\Volo.Abp.BlobStoring.Bunny.Tests.csproj", "{BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1434,6 +1439,14 @@ Global
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1676,6 +1689,8 @@ Global
{E50739A7-5E2F-4EB5-AEA9-554115CB9613} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{BE7109C5-7368-4688-8557-4A15D3F4776A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{C753DDD6-5699-45F8-8669-08CE0BB816DE} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml
new file mode 100644
index 0000000000..1715698ccd
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd
new file mode 100644
index 0000000000..ffa6fc4b78
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg
new file mode 100644
index 0000000000..f4bad072d2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg
@@ -0,0 +1,3 @@
+{
+ "role": "lib.framework"
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json
new file mode 100644
index 0000000000..b9e8bbba1b
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json
@@ -0,0 +1,68 @@
+{
+ "name": "Volo.Abp.BlobStoring.Bunny",
+ "hash": "",
+ "contents": [
+ {
+ "namespace": "Volo.Abp.BlobStoring.Bunny",
+ "dependsOnModules": [
+ {
+ "declaringAssemblyName": "Volo.Abp.BlobStoring",
+ "namespace": "Volo.Abp.BlobStoring",
+ "name": "AbpBlobStoringModule"
+ },
+ {
+ "declaringAssemblyName": "Volo.Abp.Caching",
+ "namespace": "Volo.Abp.Caching",
+ "name": "AbpCachingModule"
+ }
+ ],
+ "implementingInterfaces": [
+ {
+ "name": "IAbpModule",
+ "namespace": "Volo.Abp.Modularity",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.Modularity.IAbpModule"
+ },
+ {
+ "name": "IOnPreApplicationInitialization",
+ "namespace": "Volo.Abp.Modularity",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization"
+ },
+ {
+ "name": "IOnApplicationInitialization",
+ "namespace": "Volo.Abp",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.IOnApplicationInitialization"
+ },
+ {
+ "name": "IOnPostApplicationInitialization",
+ "namespace": "Volo.Abp.Modularity",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization"
+ },
+ {
+ "name": "IOnApplicationShutdown",
+ "namespace": "Volo.Abp",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.IOnApplicationShutdown"
+ },
+ {
+ "name": "IPreConfigureServices",
+ "namespace": "Volo.Abp.Modularity",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.Modularity.IPreConfigureServices"
+ },
+ {
+ "name": "IPostConfigureServices",
+ "namespace": "Volo.Abp.Modularity",
+ "declaringAssemblyName": "Volo.Abp.Core",
+ "fullName": "Volo.Abp.Modularity.IPostConfigureServices"
+ }
+ ],
+ "contentType": "abpModule",
+ "name": "AbpBlobStoringBunnyModule",
+ "summary": null
+ }
+ ]
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..bd16c59713
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ netstandard2.0;netstandard2.1;net8.0;net9.0
+ enable
+ Nullable
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000..5c8e1c79b7
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs
@@ -0,0 +1,11 @@
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+[DependsOn(typeof(AbpBlobStoringModule))]
+public class AbpBlobStoringBunnyModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..03afe1c36d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public static class BunnyBlobContainerConfigurationExtensions
+{
+ public static BunnyBlobProviderConfiguration GetBunnyConfiguration(
+ this BlobContainerConfiguration containerConfiguration)
+ {
+ return new BunnyBlobProviderConfiguration(containerConfiguration);
+ }
+
+ public static BlobContainerConfiguration UseBunny(
+ this BlobContainerConfiguration containerConfiguration,
+ Action bunnyConfigureAction)
+ {
+ containerConfiguration.ProviderType = typeof(BunnyBlobProvider);
+ containerConfiguration.NamingNormalizers.TryAdd();
+
+ bunnyConfigureAction(new BunnyBlobProviderConfiguration(containerConfiguration));
+
+ return containerConfiguration;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
new file mode 100644
index 0000000000..b478bac23e
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
@@ -0,0 +1,51 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Localization;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class BunnyBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
+{
+ private static readonly Regex ValidCharactersRegex =
+ new Regex(@"^[a-z0-9-]*$", RegexOptions.Compiled);
+
+ private const int MinLength = 4;
+ private const int MaxLength = 64;
+
+ public virtual string NormalizeBlobName(string blobName) => blobName;
+
+ public virtual string NormalizeContainerName(string containerName)
+ {
+ Check.NotNullOrWhiteSpace(containerName, nameof(containerName));
+
+ using (CultureHelper.Use(CultureInfo.InvariantCulture))
+ {
+ // Trim whitespace and convert to lowercase
+ var normalizedName = containerName
+ .Trim()
+ .ToLowerInvariant();
+
+ // Remove any invalid characters
+ normalizedName = Regex.Replace(normalizedName, "[^a-z0-9-]", string.Empty);
+
+ // Validate structure
+ if (!ValidCharactersRegex.IsMatch(normalizedName))
+ {
+ throw new AbpException(
+ $"Container name contains invalid characters: {containerName}. " +
+ "Only lowercase letters, numbers, and hyphens are allowed.");
+ }
+
+ // Validate length
+ if (normalizedName.Length < MinLength || normalizedName.Length > MaxLength)
+ {
+ throw new AbpException(
+ $"Container name must be between {MinLength} and {MaxLength} characters. " +
+ $"Current length: {normalizedName.Length}");
+ }
+
+ return normalizedName;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..596f977874
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs
@@ -0,0 +1,168 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using BunnyCDN.Net.Storage;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
+{
+ protected IBunnyBlobNameCalculator BunnyBlobNameCalculator { get; }
+ protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; }
+
+ public BunnyBlobProvider(
+ IBunnyBlobNameCalculator bunnyBlobNameCalculator,
+ IBlobNormalizeNamingService blobNormalizeNamingService)
+ {
+ BunnyBlobNameCalculator = bunnyBlobNameCalculator;
+ BlobNormalizeNamingService = blobNormalizeNamingService;
+ }
+
+ public override async Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ var configuration = args.Configuration.GetBunnyConfiguration();
+ var containerName = GetContainerName(args);
+ var blobName = BunnyBlobNameCalculator.Calculate(args);
+
+ using var bunnyClient = GetBunnyClient(args);
+
+ await ValidateContainerExistsAsync(bunnyClient, containerName, configuration);
+
+ if (!args.OverrideExisting && await BlobExistsAsync(bunnyClient, containerName, blobName))
+ {
+ throw new BlobAlreadyExistsException(
+ $"BLOB '{args.BlobName}' already exists in container '{containerName}'. " +
+ $"Set {nameof(args.OverrideExisting)} to true to overwrite.");
+ }
+
+ using var memoryStream = new MemoryStream();
+ await args.BlobStream.CopyToAsync(memoryStream);
+ memoryStream.Position = 0;
+
+ await bunnyClient.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))
+ {
+ return false;
+ }
+
+ try
+ {
+ await bunnyClient.DeleteObjectAsync($"{containerName}/{blobName}");
+ return true;
+ }
+ 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);
+
+ return await BlobExistsAsync(bunnyClient, 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);
+
+ if (!await BlobExistsAsync(bunnyClient, containerName, blobName))
+ {
+ return null;
+ }
+
+ try
+ {
+ return await bunnyClient.DownloadObjectAsStreamAsync($"{containerName}/{blobName}");
+ }
+ catch (WebException ex) when ((HttpStatusCode)ex.Status == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
+ protected virtual async Task BlobExistsAsync(BunnyClient bunnyClient, 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))
+ {
+ 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
+ return objects?.Any(o => o.FullPath == fullBlobPath) == true;
+ }
+ catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404"))
+ {
+ return false;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Error while checking blob existence: {ex.Message}", ex);
+ }
+ }
+ protected virtual BunnyClient GetBunnyClient(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);
+ }
+ protected virtual string GetContainerName(BlobProviderArgs args)
+ {
+ var configuration = args.Configuration.GetBunnyConfiguration();
+ return configuration.ContainerName.IsNullOrWhiteSpace()
+ ? args.ContainerName
+ : BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName!);
+ }
+ protected virtual async Task ValidateContainerExistsAsync(
+ BunnyClient client,
+ string containerName,
+ BunnyBlobProviderConfiguration configuration)
+ {
+ if (configuration.CreateContainerIfNotExists)
+ {
+ try
+ {
+ await client.EnsureStorageZoneExistsAsync(containerName);
+ }
+ catch (BunnyApiException ex)
+ {
+ throw new AbpException(
+ $"Failed to ensure Bunny storage zone '{containerName}' exists: {ex.Message}",
+ ex);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs
new file mode 100644
index 0000000000..0897c7f2b8
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs
@@ -0,0 +1,40 @@
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class BunnyBlobProviderConfiguration
+{
+ public string? Region {
+ get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.Region, "de");
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.Region, value);
+ }
+
+ ///
+ /// This name may only contain lowercase letters, numbers, and hyphens. (no spaces)
+ /// The name must also be between 4 and 64 characters long.
+ /// The name must be globaly unique
+ /// If this parameter is not specified, the ContainerName of the will be used.
+ ///
+ public string? ContainerName {
+ get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.ContainerName);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.ContainerName, value);
+ }
+
+ ///
+ /// Default value: false.
+ ///
+ public bool CreateContainerIfNotExists {
+ get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, false);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, value);
+ }
+
+ public string AccessKey {
+ get => _containerConfiguration.GetConfiguration(BunnyBlobProviderConfigurationNames.AccessKey);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.AccessKey, value);
+ }
+
+ private readonly BlobContainerConfiguration _containerConfiguration;
+
+ public BunnyBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
+ {
+ _containerConfiguration = containerConfiguration;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs
new file mode 100644
index 0000000000..09d79076a6
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs
@@ -0,0 +1,15 @@
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public static class BunnyBlobProviderConfigurationNames
+{
+ // The primary region for the storage zone (e.g., DE, NY, etc.)
+ public const string Region = "Bunny.Region";
+
+ // The name of the storage zone
+ public const string ContainerName = "Bunny.ContainerName";
+
+ // The API access key for the bunny.net account
+ public const string AccessKey = "Bunny.AccessKey";
+
+ public const string CreateContainerIfNotExists = "Bunny.CreateContainerIfNotExists";
+}
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
new file mode 100644
index 0000000000..cac007225b
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs
@@ -0,0 +1,229 @@
+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/DefaultBunnyBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNameCalculator.cs
new file mode 100644
index 0000000000..f06acb8c14
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNameCalculator.cs
@@ -0,0 +1,21 @@
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class DefaultBunnyBlobNameCalculator : IBunnyBlobNameCalculator, ITransientDependency
+{
+ protected ICurrentTenant CurrentTenant { get; }
+
+ public DefaultBunnyBlobNameCalculator(ICurrentTenant currentTenant)
+ {
+ CurrentTenant = currentTenant;
+ }
+
+ public virtual string Calculate(BlobProviderArgs args)
+ {
+ return CurrentTenant.Id == null
+ ? $"host/{args.BlobName}"
+ : $"tenants/{CurrentTenant.Id.Value.ToString("D")}/{args.BlobName}";
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs
new file mode 100644
index 0000000000..34a18aca46
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public interface IBunnyBlobNameCalculator
+{
+ string Calculate(BlobProviderArgs args);
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg
new file mode 100644
index 0000000000..a686451fbc
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg
@@ -0,0 +1,3 @@
+{
+ "role": "lib.test"
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj
new file mode 100644
index 0000000000..408f630fe2
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ net9.0
+
+ 9f0d2c00-80c1-435b-bfab-2c39c8249091
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs
new file mode 100644
index 0000000000..4f37cfca91
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs
@@ -0,0 +1,19 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class AbpBlobStoringBunnyTestCommonBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
+
+public class AbpBlobStoringBunnyTestBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
new file mode 100644
index 0000000000..3e79ad0faa
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+///
+/// This module will not try to connect to Bunny.
+///
+[DependsOn(
+ typeof(AbpBlobStoringBunnyModule),
+ typeof(AbpBlobStoringTestModule)
+)]
+public class AbpBlobStoringBunnyTestCommonModule : AbpModule
+{
+}
+
+[DependsOn(
+ typeof(AbpBlobStoringBunnyTestCommonModule)
+)]
+public class AbpBlobStoringBunnyTestModule : AbpModule
+{
+ private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091";
+
+ private readonly string _randomContainerName = "abp-bunny-test-container-" + Guid.NewGuid().ToString("N");
+
+ private BunnyBlobProviderConfiguration _configuration;
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
+ {
+ builder.AddUserSecrets(UserSecretsId);
+ }));
+
+ var configuration = context.Services.GetConfiguration();
+ var accessKey = configuration["Bunny:AccessKey"];
+ var region = configuration["Bunny:Region"];
+
+ Configure(options =>
+ {
+ options.Containers.ConfigureAll((containerName, containerConfiguration) =>
+ {
+ containerConfiguration.UseBunny(bunny =>
+ {
+ bunny.AccessKey = accessKey;
+ bunny.Region = region;
+ bunny.CreateContainerIfNotExists = true;
+ bunny.ContainerName = _randomContainerName;
+
+ _configuration = bunny;
+ });
+ });
+ });
+ }
+
+ public override void OnApplicationShutdown(ApplicationShutdownContext context)
+ {
+ AsyncHelper.RunSync(() => DeleteStorageZoneAsync(context));
+ }
+
+ private async Task DeleteStorageZoneAsync(ApplicationShutdownContext context)
+ {
+ var bunnyClient = new BunnyClient(_configuration.AccessKey, _configuration.ContainerName, _configuration.Region);
+ if (await bunnyClient.DoesStorageZoneExistAsync(_randomContainerName))
+ {
+ await bunnyClient.DeleteStorageZoneAsync(_randomContainerName);
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
new file mode 100644
index 0000000000..3d4592cc97
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
@@ -0,0 +1 @@
+namespace Volo.Abp.BlobStoring.Bunny;
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs
new file mode 100644
index 0000000000..fc3b2d8365
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs
@@ -0,0 +1,56 @@
+using System;
+using Shouldly;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class BunnyBlobNameCalculatorTests : AbpBlobStoringBunnyTestCommonBase
+{
+ private readonly IBunnyBlobNameCalculator _calculator;
+ private readonly ICurrentTenant _currentTenant;
+
+ private const string BunnyContainerName = "/";
+ private const string BunnySeparator = "/";
+
+ public BunnyBlobNameCalculatorTests()
+ {
+ _calculator = GetRequiredService();
+ _currentTenant = GetRequiredService();
+ }
+
+ [Fact]
+ public void Default_Settings()
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"host{BunnySeparator}my-blob");
+ }
+
+ [Fact]
+ public void Default_Settings_With_TenantId()
+ {
+ var tenantId = Guid.NewGuid();
+
+ using (_currentTenant.Change(tenantId))
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"tenants{BunnySeparator}{tenantId:D}{BunnySeparator}my-blob");
+ }
+ }
+
+ private static BlobProviderArgs GetArgs(
+ string containerName,
+ string blobName)
+ {
+ return new BlobProviderGetArgs(
+ containerName,
+ new BlobContainerConfiguration().UseBunny(x =>
+ {
+ x.ContainerName = containerName;
+ }),
+ blobName
+ );
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs
new file mode 100644
index 0000000000..c4d3cd6878
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs
@@ -0,0 +1,57 @@
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class DefaultBunnyBlobNamingNormalizerProviderTests : AbpBlobStoringBunnyTestCommonBase
+{
+ private readonly IBlobNamingNormalizer _blobNamingNormalizer;
+
+ public DefaultBunnyBlobNamingNormalizerProviderTests()
+ {
+ _blobNamingNormalizer = GetRequiredService();
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Lowercase()
+ {
+ var filename = "ThisIsMyContainerName";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("thisismycontainername");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Only_Letters_Numbers_Dash_Dots()
+ {
+ var filename = ",./this-i,/s-my-c,/ont,/ai+*/=!@#$n^&*er.name+/";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this-is-my-containername");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Min_Length()
+ {
+ var filename = "a";
+ Assert.Throws(()=>
+ {
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ });
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Max_Length()
+ {
+ var longName = new string('a', 65); // 65 characters
+ var exception = Assert.Throws(() =>
+ _blobNamingNormalizer.NormalizeContainerName(longName)
+ );
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Dots()
+ {
+ var filename = ".this..is.-.my.container....name.";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("thisis-mycontainername");
+ }
+}
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 2/5] 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);
+}
From 14fda53d069ddbbed4a163dff03fd80b089e1966 Mon Sep 17 00:00:00 2001
From: Suhaib <93185683+suhaib-mousa@users.noreply.github.com>
Date: Mon, 27 Jan 2025 20:24:46 +0300
Subject: [PATCH 3/5] encrypt the sensitive password before caching
---
.../infrastructure/blob-storing/bunny.md | 1 +
...actory.cs => DefaultBunnyClientFactory.cs} | 27 +++++++++++++------
2 files changed, 20 insertions(+), 8 deletions(-)
rename framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/{BunnyClientFactory.cs => DefaultBunnyClientFactory.cs} (80%)
diff --git a/docs/en/framework/infrastructure/blob-storing/bunny.md b/docs/en/framework/infrastructure/blob-storing/bunny.md
index 59aa1eb60c..4c5fb5ef0f 100644
--- a/docs/en/framework/infrastructure/blob-storing/bunny.md
+++ b/docs/en/framework/infrastructure/blob-storing/bunny.md
@@ -61,3 +61,4 @@ Bunny Blob Provider organizes BLOB name and implements some conventions. The ful
* `BunnyBlobProvider` is the main service that implements the Bunny BLOB storage provider, if you want to override/replace it via [dependency injection](../../fundamentals/dependency-injection.md) (don't replace `IBlobProvider` interface, but replace `BunnyBlobProvider` class).
* `IBunnyBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultBunnyBlobNameCalculator` by default.
+* `IBunnyClientFactory` is implemented by `DefaultBunnyClientFactory` by default. You can override/replace it,if you want customize.
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/DefaultBunnyClientFactory.cs
similarity index 80%
rename from framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs
rename to framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
index e4fb3d8fd5..ae60a25e01 100644
--- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
@@ -8,25 +8,30 @@ using BunnyCDN.Net.Storage;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Security.Encryption;
namespace Volo.Abp.BlobStoring.Bunny;
-public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
+public class DefaultBunnyClientFactory : IBunnyClientFactory, ITransientDependency
{
private readonly IDistributedCache _cache;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IStringEncryptionService _stringEncryptionService;
+
private const string CacheKeyPrefix = "BunnyStorageZone:";
private static TimeSpan CacheDuration = TimeSpan.FromHours(12);
- public BunnyClientFactory(
+ public DefaultBunnyClientFactory(
IHttpClientFactory httpClient,
- IDistributedCache cache)
+ IDistributedCache cache,
+ IStringEncryptionService stringEncryptionService)
{
_cache = cache;
_httpClientFactory = httpClient;
+ _stringEncryptionService = stringEncryptionService;
}
- public async Task CreateAsync(string accessKey, string containerName, string region = "de")
+ public virtual async Task CreateAsync(string accessKey, string containerName, string region = "de")
{
var cacheKey = $"{CacheKeyPrefix}{containerName}";
var storageZoneInfo = await _cache.GetOrAddAsync(
@@ -37,6 +42,9 @@ public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
{
throw new AbpException($"Storage zone '{containerName}' not found");
}
+
+ // Encrypt the sensitive password before caching
+ result.Password = _stringEncryptionService.Encrypt(result.Password!)!;
return result;
},
() => new DistributedCacheEntryOptions
@@ -50,10 +58,13 @@ public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
throw new AbpException($"Could not retrieve storage zone information for container '{containerName}'");
}
- return new BunnyCDNStorage(containerName, storageZoneInfo.Password, region);
+ // Decrypt the password before using it
+ var decryptedPassword = _stringEncryptionService.Decrypt(storageZoneInfo.Password);
+
+ return new BunnyCDNStorage(containerName, decryptedPassword, region);
}
- public async Task EnsureStorageZoneExistsAsync(
+ public virtual async Task EnsureStorageZoneExistsAsync(
string accessKey,
string containerName,
string region = "de",
@@ -78,7 +89,7 @@ public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
}
}
- private async Task CreateStorageZoneAsync(
+ protected virtual async Task CreateStorageZoneAsync(
string accessKey,
string containerName,
string region)
@@ -122,7 +133,7 @@ public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
return createdZone;
}
- private async Task GetStorageZoneAsync(string accessKey, string containerName)
+ protected virtual async Task GetStorageZoneAsync(string accessKey, string containerName)
{
using var _client = _httpClientFactory.CreateClient("BunnyApiClient");
_client.DefaultRequestHeaders.Add("AccessKey", accessKey);
From 5f38b486c625c61abd3aa5a5ccbca313bb2b1a49 Mon Sep 17 00:00:00 2001
From: Suhaib <93185683+suhaib-mousa@users.noreply.github.com>
Date: Mon, 27 Jan 2025 20:46:21 +0300
Subject: [PATCH 4/5] fix unit test
---
.../Bunny/AbpBlobStoringBunnyTestModule.cs | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
index 3e79ad0faa..73066291ed 100644
--- a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using NSubstitute.Extensions;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
@@ -56,18 +57,4 @@ public class AbpBlobStoringBunnyTestModule : AbpModule
});
});
}
-
- public override void OnApplicationShutdown(ApplicationShutdownContext context)
- {
- AsyncHelper.RunSync(() => DeleteStorageZoneAsync(context));
- }
-
- private async Task DeleteStorageZoneAsync(ApplicationShutdownContext context)
- {
- var bunnyClient = new BunnyClient(_configuration.AccessKey, _configuration.ContainerName, _configuration.Region);
- if (await bunnyClient.DoesStorageZoneExistAsync(_randomContainerName))
- {
- await bunnyClient.DeleteStorageZoneAsync(_randomContainerName);
- }
- }
}
From 41e3d350318faf3b073c821e73f72e12efcf6548 Mon Sep 17 00:00:00 2001
From: maliming
Date: Tue, 4 Feb 2025 11:00:28 +0800
Subject: [PATCH 5/5] Refactor code.
---
.../Bunny/AbpBunnyBlobStoringModule .cs | 7 +-
.../BlobStoring/Bunny/BunnyApiException.cs | 18 +++
.../Bunny/BunnyBlobNamingNormalizer.cs | 2 +-
.../BlobStoring/Bunny/BunnyBlobProvider.cs | 8 +-
.../Bunny/BunnyStorageZoneModel.cs | 17 +++
.../Bunny/DefaultBunnyClientFactory.cs | 104 ++++++++----------
.../BlobStoring/Bunny/IBunnyClientFactory.cs | 1 +
.../Bunny/BunnyBlobContainer_Tests.cs | 11 ++
nupkg/common.ps1 | 1 +
9 files changed, 102 insertions(+), 67 deletions(-)
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs
create mode 100644 framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs
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 49e97e861b..3fe814f2f0 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,12 +1,11 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
namespace Volo.Abp.BlobStoring.Bunny;
[DependsOn(
- typeof(AbpBlobStoringModule),
+ typeof(AbpBlobStoringModule),
typeof(AbpCachingModule))]
public class AbpBlobStoringBunnyModule : AbpModule
{
@@ -14,4 +13,4 @@ public class AbpBlobStoringBunnyModule : AbpModule
{
context.Services.AddHttpClient();
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs
new file mode 100644
index 0000000000..1e10d4caa2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+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/BunnyBlobNamingNormalizer.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
index b478bac23e..74c436e2e9 100644
--- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
@@ -7,7 +7,7 @@ namespace Volo.Abp.BlobStoring.Bunny;
public class BunnyBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
- private static readonly Regex ValidCharactersRegex =
+ private readonly static Regex ValidCharactersRegex =
new Regex(@"^[a-z0-9-]*$", RegexOptions.Compiled);
private const int MinLength = 4;
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 b154ab2160..6c06bc3f95 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
@@ -24,7 +24,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
BunnyClientFactory = bunnyClientFactory;
}
- public override async Task SaveAsync(BlobProviderSaveArgs args)
+ public async override Task SaveAsync(BlobProviderSaveArgs args)
{
var configuration = args.Configuration.GetBunnyConfiguration();
var containerName = GetContainerName(args);
@@ -48,7 +48,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
await bunnyStorage.UploadAsync(memoryStream, $"{containerName}/{blobName}");
}
- public override async Task DeleteAsync(BlobProviderDeleteArgs args)
+ public async override Task DeleteAsync(BlobProviderDeleteArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var containerName = GetContainerName(args);
@@ -69,7 +69,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
}
}
- public override async Task ExistsAsync(BlobProviderExistsArgs args)
+ public async override Task ExistsAsync(BlobProviderExistsArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var containerName = GetContainerName(args);
@@ -78,7 +78,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
return await BlobExistsAsync(bunnyStorage, containerName, blobName);
}
- public override async Task GetOrNullAsync(BlobProviderGetArgs args)
+ public async override Task GetOrNullAsync(BlobProviderGetArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var containerName = GetContainerName(args);
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs
new file mode 100644
index 0000000000..e718531927
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+[Serializable]
+public class BunnyStorageZoneModel
+{
+ 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; }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
index ae60a25e01..d22cff0e0a 100644
--- a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
@@ -14,16 +15,16 @@ namespace Volo.Abp.BlobStoring.Bunny;
public class DefaultBunnyClientFactory : IBunnyClientFactory, ITransientDependency
{
- private readonly IDistributedCache _cache;
+ private readonly IDistributedCache _cache;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IStringEncryptionService _stringEncryptionService;
private const string CacheKeyPrefix = "BunnyStorageZone:";
- private static TimeSpan CacheDuration = TimeSpan.FromHours(12);
+ private readonly static TimeSpan CacheDuration = TimeSpan.FromHours(12);
public DefaultBunnyClientFactory(
IHttpClientFactory httpClient,
- IDistributedCache cache,
+ IDistributedCache cache,
IStringEncryptionService stringEncryptionService)
{
_cache = cache;
@@ -89,76 +90,63 @@ public class DefaultBunnyClientFactory : IBunnyClientFactory, ITransientDependen
}
}
- protected virtual async Task CreateStorageZoneAsync(
+ protected virtual async Task CreateStorageZoneAsync(
string accessKey,
string containerName,
string region)
{
- using var _client = _httpClientFactory.CreateClient("BunnyApiClient");
- _client.DefaultRequestHeaders.Add("AccessKey", accessKey);
-
- var payload = new Dictionary
+ using (var client = _httpClientFactory.CreateClient("BunnyApiClient"))
{
- { "Name", containerName },
- { "Region", region },
- { "ZoneTier", 0 }
- };
+ client.DefaultRequestHeaders.Add("AccessKey", accessKey);
- var content = new StringContent(
- JsonSerializer.Serialize(payload),
- Encoding.UTF8,
- "application/json");
+ var payload = new Dictionary
+ {
+ { "Name", containerName },
+ { "Region", region },
+ { "ZoneTier", 0 }
+ };
- var response = await _client.PostAsync(
- "https://api.bunny.net/storagezone",
- content);
+ var content = new StringContent(
+ JsonSerializer.Serialize(payload),
+ Encoding.UTF8,
+ "application/json");
- if (!response.IsSuccessStatusCode)
- {
- var errorContent = await response.Content.ReadAsStringAsync();
- throw new AbpException(
- $"Failed to create storage zone '{containerName}'. " +
- $"Status: {response.StatusCode}, Error: {errorContent}");
- }
+ 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);
+ 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}'");
- }
+ if (createdZone == null)
+ {
+ throw new AbpException($"Failed to deserialize the created storage zone response for '{containerName}'");
+ }
- return createdZone;
+ return createdZone;
+ }
}
- protected virtual async Task GetStorageZoneAsync(string accessKey, string containerName)
+ protected virtual 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);
+ using (var client = _httpClientFactory.CreateClient("BunnyApiClient"))
+ {
+ client.DefaultRequestHeaders.Add("AccessKey", accessKey);
+ var response = await client.GetAsync("https://api.bunny.net/storagezone");
+ response.EnsureSuccessStatusCode();
- return Array.Find(zones!, z =>
- z.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !z.Deleted);
- }
+ var content = await response.Content.ReadAsStringAsync();
+ var zones = JsonSerializer.Deserialize(content);
- [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; }
+ return zones?.FirstOrDefault(x => x.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !x.Deleted);
+ }
}
}
-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
index dce2c5b6f0..7e5db5ed41 100644
--- 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
@@ -6,5 +6,6 @@ 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);
}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
index 3d4592cc97..84b255e4c5 100644
--- a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
@@ -1 +1,12 @@
namespace Volo.Abp.BlobStoring.Bunny;
+
+/*
+//Please set the correct connection string in secrets.json and continue the test.
+public class BunnyBlobContainer_Tests : BlobContainer_Tests
+{
+ public BunnyBlobContainer_Tests()
+ {
+
+ }
+}
+*/
diff --git a/nupkg/common.ps1 b/nupkg/common.ps1
index 4233ce7b59..e60f374566 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -155,6 +155,7 @@ $projects = (
"framework/src/Volo.Abp.BlobStoring.Minio",
"framework/src/Volo.Abp.BlobStoring.Aws",
"framework/src/Volo.Abp.BlobStoring.Google",
+ "framework/src/Volo.Abp.BlobStoring.Bunny",
"framework/src/Volo.Abp.Caching",
"framework/src/Volo.Abp.Caching.StackExchangeRedis",
"framework/src/Volo.Abp.Castle.Core",