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