diff --git a/Directory.Packages.props b/Directory.Packages.props
index b43886f7cf..652b9a836e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -38,6 +38,7 @@
+
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 3373a239de..19fbf05df8 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -467,6 +467,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.RemoteServices.Tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Abstractions", "src\Volo.Abp.AspNetCore.Abstractions\Volo.Abp.AspNetCore.Abstractions.csproj", "{E1051CD0-9262-4869-832D-B951723F4DDE}"
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}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1393,6 +1397,14 @@ Global
{E1051CD0-9262-4869-832D-B951723F4DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Release|Any CPU.Build.0 = Release|Any CPU
+ {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1628,6 +1640,8 @@ Global
{DFAF8763-D1D6-4EB4-B459-20E31007FE2F} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{DACD4485-61BE-4DE5-ACAE-4FFABC122500} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{E1051CD0-9262-4869-832D-B951723F4DDE} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {DEEB5200-BBF9-464D-9B7E-8FC035A27E94} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo.Abp.BlobStoring.Google.csproj b/framework/src/Volo.Abp.BlobStoring.Google/Volo.Abp.BlobStoring.Google.csproj
new file mode 100644
index 0000000000..073c8c1891
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo.Abp.BlobStoring.Google.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ netstandard2.0;netstandard2.1;net8.0
+ enable
+ Nullable
+ Volo.Abp.BlobStoring.Google
+ Volo.Abp.BlobStoring.Google
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleModule.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleModule.cs
new file mode 100644
index 0000000000..f5243fda15
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleModule.cs
@@ -0,0 +1,9 @@
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+[DependsOn(typeof(AbpBlobStoringModule))]
+public class AbpBlobStoringGoogleModule : AbpModule
+{
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNameCalculator.cs
new file mode 100644
index 0000000000..fec52b44d1
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNameCalculator.cs
@@ -0,0 +1,21 @@
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class DefaultGoogleBlobNameCalculator : IGoogleBlobNameCalculator, ITransientDependency
+{
+ protected ICurrentTenant CurrentTenant { get; }
+
+ public DefaultGoogleBlobNameCalculator(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}";
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..3c0efd4c86
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobContainerConfigurationExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public static class GoogleBlobContainerConfigurationExtensions
+{
+ public static GoogleBlobProviderConfiguration GetGoogleConfiguration(
+ this BlobContainerConfiguration containerConfiguration)
+ {
+ return new GoogleBlobProviderConfiguration(containerConfiguration);
+ }
+
+ public static BlobContainerConfiguration UseGoogle(
+ this BlobContainerConfiguration containerConfiguration,
+ Action googleConfigureAction)
+ {
+ containerConfiguration.ProviderType = typeof(GoogleBlobProvider);
+ containerConfiguration.NamingNormalizers.TryAdd();
+
+ googleConfigureAction(new GoogleBlobProviderConfiguration(containerConfiguration));
+
+ return containerConfiguration;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobNamingNormalizer.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobNamingNormalizer.cs
new file mode 100644
index 0000000000..8dc840d805
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobNamingNormalizer.cs
@@ -0,0 +1,120 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Localization;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class GoogleBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
+{
+ ///
+ /// https://cloud.google.com/storage/docs/buckets#naming
+ ///
+ public string NormalizeContainerName(string containerName)
+ {
+ using (CultureHelper.Use(CultureInfo.InvariantCulture))
+ {
+ // All letters in a Bucket name must be lowercase.
+ containerName = containerName.ToLower();
+
+ // Bucket names must contain 3-63 characters. Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters.
+ if(containerName.Contains("."))
+ {
+ if (containerName.Length > 222)
+ {
+ containerName = containerName.Substring(0, 222);
+ }
+
+ var parts = containerName.Split('.');
+ for (var i = 0; i < parts.Length; i++)
+ {
+ if (parts[i].Length > 63)
+ {
+ parts[i] = parts[i].Substring(0, 63);
+ }
+ }
+
+ containerName = string.Join(".", parts);
+ }
+ else if (containerName.Length > 63)
+ {
+ containerName = containerName.Substring(0, 63);
+ }
+
+ //Bucket names can only contain lowercase letters, numeric characters, dashes (-), underscores (_), and dots (.). Spaces are not allowed. Names containing dots require verification.
+ containerName = Regex.Replace(containerName, "[^a-z0-9-_.]", string.Empty);
+
+ //Be a syntactically valid DNS name (for example, bucket..example.com is not valid because it contains two dots in a row).
+ containerName = Regex.Replace(containerName, "[.]{2,}", ".");
+
+ //Bucket names cannot be represented as an IP address in dotted-decimal notation (for example, 192.168.5.4).
+ containerName = Regex.Replace(containerName, "^(?:(?:^|\\.)(?:2(?:5[0-5]|[0-4]\\d)|1?\\d?\\d)){4}$", string.Empty);
+
+ //Bucket names cannot begin with the "goog" prefix.
+ containerName = Regex.Replace(containerName, "^goog", string.Empty);
+
+ //Bucket names cannot contain "google" or close misspellings, such as "g00gle".
+ containerName = Regex.Replace(containerName, "google", string.Empty);
+
+ //Bucket names must start and end with a number or letter.
+ containerName = RemoveInvalidStartEndCharacters(containerName);
+
+ // Bucket names must be from 3 through 63 characters long. Names containing dots can contain up to 222 characters.
+ if (containerName.Length < 3)
+ {
+ var length = containerName.Length;
+ for (var i = 0; i < 3 - length; i++)
+ {
+ containerName += "0";
+ }
+ }
+
+ return containerName;
+ }
+ }
+
+ protected virtual string RemoveInvalidStartEndCharacters(string containerName)
+ {
+ if (string.IsNullOrWhiteSpace(containerName))
+ {
+ return containerName;
+ }
+
+ if (!char.IsLetterOrDigit(containerName[0]))
+ {
+ containerName = containerName.Substring(1);
+ return RemoveInvalidStartEndCharacters(containerName);
+ }
+
+ if (!char.IsLetterOrDigit(containerName[containerName.Length - 1]))
+ {
+ containerName = containerName.Substring(0, containerName.Length - 1);
+ return RemoveInvalidStartEndCharacters(containerName);
+ }
+
+ return containerName;
+ }
+
+ ///
+ /// https://cloud.google.com/storage/docs/objects#naming
+ ///
+ public string NormalizeBlobName(string blobName)
+ {
+ //Object names can contain any sequence of valid Unicode characters, of length 1-1024 bytes when UTF-8 encoded
+ if (blobName.Length > 1024)
+ {
+ blobName = blobName.Substring(0, 1024);
+ }
+
+ //Object names cannot contain Carriage Return or Line Feed characters.
+ blobName = Regex.Replace(blobName, "[\r\n]", string.Empty);
+
+ //Object names cannot start with .well-known/acme-challenge/.
+ blobName = Regex.Replace(blobName, "^\\.well-known/acme-challenge/", string.Empty);
+
+ //Objects cannot be named . or ...
+ blobName = Regex.Replace(blobName, "^\\.\\.?$", string.Empty);
+
+ return blobName;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProvider.cs
new file mode 100644
index 0000000000..6f9b5c280e
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProvider.cs
@@ -0,0 +1,156 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using Google;
+using Google.Apis.Auth.OAuth2;
+using Google.Cloud.Storage.V1;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class GoogleBlobProvider : BlobProviderBase, ITransientDependency
+{
+ protected IGoogleBlobNameCalculator GoogleBlobNameCalculator { get; }
+ protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; }
+
+ public GoogleBlobProvider(IGoogleBlobNameCalculator googleBlobNameCalculator, IBlobNormalizeNamingService blobNormalizeNamingService)
+ {
+ GoogleBlobNameCalculator = googleBlobNameCalculator;
+ BlobNormalizeNamingService = blobNormalizeNamingService;
+ }
+
+ public async override Task SaveAsync(BlobProviderSaveArgs args)
+ {
+ var configuration = args.Configuration.GetGoogleConfiguration();
+ var storageClient = await GetStorageClientClientAsync(args);
+ var blobName = GoogleBlobNameCalculator.Calculate(args);
+ var containerName = GetContainerName(args);
+
+ if (await BlobExistsAsync(args, blobName) && !args.OverrideExisting)
+ {
+ throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{GetContainerName(args)}'! Set {nameof(args.OverrideExisting)} if it should be overwritten.");
+ }
+
+ if (configuration.CreateContainerIfNotExists)
+ {
+ await CreateContainerIfNotExists(args);
+ }
+
+ await storageClient.UploadObjectAsync(containerName, blobName, contentType: "application/octet-stream", args.BlobStream);
+ }
+
+ public async override Task DeleteAsync(BlobProviderDeleteArgs args)
+ {
+ var storageClient = await GetStorageClientClientAsync(args);
+ var blobName = GoogleBlobNameCalculator.Calculate(args);
+ var containerName = GetContainerName(args);
+
+ try
+ {
+ await storageClient.DeleteObjectAsync(containerName, blobName);
+ }
+ catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound)
+ {
+ return true;
+ }
+
+ return true;
+ }
+
+ public async override Task ExistsAsync(BlobProviderExistsArgs args)
+ {
+ var blobName = GoogleBlobNameCalculator.Calculate(args);
+ return await BlobExistsAsync(args, blobName);
+ }
+
+ public async override Task GetOrNullAsync(BlobProviderGetArgs args)
+ {
+ var storageClient = await GetStorageClientClientAsync(args);
+ var blobName = GoogleBlobNameCalculator.Calculate(args);
+ var containerName = GetContainerName(args);
+
+ if(!await BlobExistsAsync(args, blobName))
+ {
+ return null;
+ }
+
+ var stream = new MemoryStream();
+
+ await storageClient.DownloadObjectAsync(containerName, blobName, stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ return stream;
+ }
+
+ protected virtual string GetContainerName(BlobProviderArgs args)
+ {
+ var configuration = args.Configuration.GetGoogleConfiguration();
+ return configuration.ContainerName.IsNullOrWhiteSpace()
+ ? args.ContainerName
+ : BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName!);
+ }
+
+ protected virtual async Task BlobExistsAsync(BlobProviderArgs args, string blobName)
+ {
+ var storageClient = await GetStorageClientClientAsync(args);
+ if(!await ContainerExistsAsync(args, storageClient))
+ {
+ return false;
+ }
+
+ try
+ {
+ await storageClient.GetObjectAsync(GetContainerName(args), blobName);
+ }
+ catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected virtual async Task CreateContainerIfNotExists(BlobProviderArgs args)
+ {
+ var storageClient = await GetStorageClientClientAsync(args);
+ var configuration = args.Configuration.GetGoogleConfiguration();
+ if(await ContainerExistsAsync(args, storageClient))
+ {
+ return;
+ }
+
+ await storageClient.CreateBucketAsync(configuration.ProjectId, GetContainerName(args));
+ }
+
+ protected virtual async Task ContainerExistsAsync(BlobProviderArgs args, StorageClient client)
+ {
+ try
+ {
+ await client.GetBucketAsync(GetContainerName(args));
+ }
+ catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected virtual async Task GetStorageClientClientAsync(BlobProviderArgs args)
+ {
+ var configuration = args.Configuration.GetGoogleConfiguration();
+ var googleCredential = GoogleCredential.FromServiceAccountCredential(
+ new ServiceAccountCredential(
+ new ServiceAccountCredential.Initializer(configuration.ClientEmail)
+ {
+ ProjectId = configuration.ProjectId,
+ Scopes = configuration.Scopes
+ }
+ .FromPrivateKey(configuration.PrivateKey)
+ ));
+
+ return await StorageClient.CreateAsync(googleCredential);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfiguration.cs
new file mode 100644
index 0000000000..f00987b303
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfiguration.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class GoogleBlobProviderConfiguration
+{
+ private readonly BlobContainerConfiguration _containerConfiguration;
+
+ public GoogleBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
+ {
+ _containerConfiguration = containerConfiguration;
+ }
+
+ ///
+ /// Unique identifier for your project.
+ /// For more info see: https://cloud.google.com/resource-manager/docs/creating-managing-projects
+ ///
+ public string? ProjectId {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.ProjectId);
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ProjectId, value);
+ }
+
+ ///
+ /// Email address that generated by the Google Cloud.
+ ///
+ public string? ClientEmail {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.ClientEmail);
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ClientEmail, value);
+ }
+
+ ///
+ /// Private key that generated by Google Cloud.
+ /// Starts with '-----BEGIN PRIVATE KEY-----'
+ /// and ends with '-----END PRIVATE KEY-----'
+ ///
+ public string? PrivateKey {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.PrivateKey);
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.PrivateKey, value);
+ }
+
+ ///
+ /// Available OAuth 2.0 scopes.
+ ///
+ public List? Scopes {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.Scopes, new List());
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.Scopes, value);
+ }
+
+ ///
+ /// The name can only contain lowercase letters, numeric characters, dashes (-), underscores (_), and dots (.). Spaces are not allowed. Names containing dots require verification.
+ /// Must start and end with a number or letter.
+ /// Must contain 3-63 characters. Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters.
+ /// Cannot be represented as an IP address in dotted-decimal notation (for example, 192.168.5.4).
+ /// Cannot begin with the "goog" prefix.
+ /// Cannot contain "google" or close misspellings, such as "g00gle".
+ /// If this parameter is not specified, the ContainerName of the will be used.
+ ///
+ public string? ContainerName {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.ContainerName);
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ContainerName, value);
+ }
+
+ ///
+ /// Default value: false.
+ ///
+ public bool CreateContainerIfNotExists {
+ get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.CreateContainerIfNotExists, false);
+ set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.CreateContainerIfNotExists, value);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfigurationNames.cs
new file mode 100644
index 0000000000..0cfd10d129
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/GoogleBlobProviderConfigurationNames.cs
@@ -0,0 +1,11 @@
+namespace Volo.Abp.BlobStoring.Google;
+
+public static class GoogleBlobProviderConfigurationNames
+{
+ public const string ProjectId = "Google.ProjectId";
+ public const string ClientEmail = "Google.ClientEmail";
+ public const string PrivateKey = "Google.PrivateKey";
+ public const string Scopes = "Google.Scopes";
+ public const string ContainerName = "Google.ContainerName";
+ public const string CreateContainerIfNotExists = "Google.CreateContainerIfNotExists";
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/IGoogleBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/IGoogleBlobNameCalculator.cs
new file mode 100644
index 0000000000..8099c6fa8c
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Google/Volo/Abp/BlobStoring/Google/IGoogleBlobNameCalculator.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp.BlobStoring.Google;
+
+public interface IGoogleBlobNameCalculator
+{
+ string Calculate(BlobProviderArgs args);
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo.Abp.BlobStoring.Google.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo.Abp.BlobStoring.Google.Tests.csproj
new file mode 100644
index 0000000000..a36e26fade
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo.Abp.BlobStoring.Google.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ net8.0
+
+ 9f0d2c00-80c1-435b-bfab-2c39c8249091
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestBase.cs b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestBase.cs
new file mode 100644
index 0000000000..709590f6ed
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestBase.cs
@@ -0,0 +1,19 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class AbpBlobStoringGoogleTestCommonBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
+
+public class AbpBlobStoringGoogleTestBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestModule.cs
new file mode 100644
index 0000000000..be2de91a38
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/AbpBlobStoringGoogleTestModule.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Threading.Tasks;
+using Google.Apis.Auth.OAuth2;
+using Google.Cloud.Storage.V1;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+
+///
+/// This module will not try to connect to Google Cloud Storage.
+///
+[DependsOn(
+ typeof(AbpBlobStoringGoogleModule),
+ typeof(AbpBlobStoringTestModule)
+)]
+public class AbpBlobStoringGoogleTestCommonModule : AbpModule
+{
+
+}
+
+[DependsOn(
+ typeof(AbpBlobStoringGoogleTestCommonModule)
+)]
+public class AbpBlobStoringGoogleTestModule : AbpModule
+{
+ private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091";
+
+ private string _clientEmail;
+ private string _projectId;
+ private string _privateKey;
+
+ private readonly string _randomContainerName = "abp-test-container-" + Guid.NewGuid().ToString("N");
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
+ {
+ builder.AddUserSecrets(UserSecretsId);
+ }));
+
+ var configuration = context.Services.GetConfiguration();
+ _clientEmail = configuration["Google:ClientEmail"];
+ _projectId = configuration["Google:ProjectId"];
+ _privateKey = configuration["Google:PrivateKey"];
+
+ Configure(options =>
+ {
+ options.Containers.ConfigureAll((containerName, containerConfiguration) =>
+ {
+ containerConfiguration.UseGoogle(google =>
+ {
+ google.ClientEmail = _clientEmail;
+ google.ProjectId = _projectId = "wide-origin-296910";
+ google.PrivateKey = _privateKey;
+ google.ContainerName = _randomContainerName;
+ google.CreateContainerIfNotExists = true;
+ });
+ });
+ });
+ }
+
+ public override void OnApplicationShutdown(ApplicationShutdownContext context)
+ {
+ var googleCredential = GoogleCredential.FromServiceAccountCredential(
+ new ServiceAccountCredential(
+ new ServiceAccountCredential.Initializer(_clientEmail)
+ {
+ ProjectId = _projectId
+ }
+ .FromPrivateKey(_privateKey)
+ ));
+
+ var client = StorageClient.Create(googleCredential);
+
+ try
+ {
+ client.DeleteBucket(_randomContainerName, new DeleteBucketOptions
+ {
+ DeleteObjects = true
+ });
+ }
+ catch (Exception e)
+ {
+ // ignored
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNamingNormalizerProvider_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNamingNormalizerProvider_Tests.cs
new file mode 100644
index 0000000000..cfe43f8bcf
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/DefaultGoogleBlobNamingNormalizerProvider_Tests.cs
@@ -0,0 +1,94 @@
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class DefaultGoogleBlobNamingNormalizerProvider_Tests: AbpBlobStoringGoogleTestCommonBase
+{
+ private readonly IBlobNamingNormalizer _blobNamingNormalizer;
+
+ public DefaultGoogleBlobNamingNormalizerProvider_Tests()
+ {
+ _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_Underscores()
+ {
+ var filename = ",./this-i,/s-my-c,/ont,/ai+*/=!@#$n^&*er.name+/";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this-is-my-container.name");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Only_Start_With_Letters_Or_Numbers()
+ {
+ var filename = "-this.--is-.-.-my--_container---name-";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this.--is-.-.-my--_container---name");
+
+ filename = ".this.--is-.-.-my--container---name0._";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this.--is-.-.-my--container---name0");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Min_Length()
+ {
+ var filename = "a";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.Length.ShouldBeGreaterThanOrEqualTo(3);
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Max_Length()
+ {
+ var filename = "abpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabp";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.Length.ShouldBeLessThanOrEqualTo(63);
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Must_Not_Be_Ip_Address()
+ {
+ var filename = "192.168.5.4";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("000");
+
+ filename = "a.192.168.5.4";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("a.192.168.5.4");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Dots()
+ {
+ var filename = ".this..is.my.container....name.";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this.is.my.container.name");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_DNS()
+ {
+ var filename = "bucket...example..com";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("bucket.example.com");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Max_Length_Dash()
+ {
+ var filename = "-this-is-my-container-name-abpabpabpabpabpabpabpabp-a-b-p-a--b-p-";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this-is-my-container-name-abpabpabpabpabpabpabpabp-a-b-p-a--b");
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobContainer_Tests.cs
new file mode 100644
index 0000000000..6a4405f7cd
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobContainer_Tests.cs
@@ -0,0 +1,13 @@
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+/*
+//Please set the correct connection string in secrets.json and continue the test.
+public class GoogleBlobContainer_Tests : BlobContainer_Tests
+{
+ public GoogleBlobContainer_Tests()
+ {
+ }
+}
+*/
diff --git a/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobNameCalculator_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobNameCalculator_Tests.cs
new file mode 100644
index 0000000000..781900ad81
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Google.Tests/Volo/Abp/BlobStoring/Google/GoogleBlobNameCalculator_Tests.cs
@@ -0,0 +1,56 @@
+using System;
+using Shouldly;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Google;
+
+public class GoogleBlobNameCalculator_Tests : AbpBlobStoringGoogleTestCommonBase
+{
+ private readonly IGoogleBlobNameCalculator _calculator;
+ private readonly ICurrentTenant _currentTenant;
+
+ private const string GoogleContainerName = "/";
+ private const string GoogleSeparator = "/";
+
+ public GoogleBlobNameCalculator_Tests()
+ {
+ _calculator = GetRequiredService();
+ _currentTenant = GetRequiredService();
+ }
+
+ [Fact]
+ public void Default_Settings()
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"host{GoogleSeparator}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{GoogleSeparator}{tenantId:D}{GoogleSeparator}my-blob");
+ }
+ }
+
+ private static BlobProviderArgs GetArgs(
+ string containerName,
+ string blobName)
+ {
+ return new BlobProviderGetArgs(
+ containerName,
+ new BlobContainerConfiguration().UseGoogle(x =>
+ {
+ x.ContainerName = containerName;
+ }),
+ blobName
+ );
+ }
+}
\ No newline at end of file