@ -0,0 +1,94 @@ |
|||
name: 🤠 ABP Studio |
|||
description: Create a report to help us improve the ABP Studio |
|||
labels: [studio] |
|||
body: |
|||
- type: markdown |
|||
attributes: |
|||
value: | |
|||
We welcome bug reports! This template will help us gather the information we need to start the triage process. |
|||
|
|||
Please keep in mind that the GitHub issue tracker is not intended as a general support forum, but for reporting **non-security** bugs and feature requests. |
|||
If you believe you have an issue that affects the SECURITY of the platform, please do NOT create an issue and instead email your issue details to info@abp.io. |
|||
For other types of questions, consider using [StackOverflow](https://stackoverflow.com/questions/tagged/abp). |
|||
- type: checkboxes |
|||
id: searched |
|||
attributes: |
|||
label: Is there an existing issue for this? |
|||
description: Please search to see if an issue already exists for the bug you encountered or feature request ([abp/issues](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+label%3Astudio)). |
|||
options: |
|||
- label: I have searched the existing issues |
|||
required: true |
|||
- type: textarea |
|||
id: background |
|||
attributes: |
|||
label: Description |
|||
description: Please share a clear and concise description of the problem. |
|||
placeholder: Description |
|||
validations: |
|||
required: true |
|||
- type: markdown |
|||
attributes: |
|||
value: | |
|||
## Setup |
|||
Please provide more information on your ABP Studio setup. |
|||
- type: input |
|||
id: version |
|||
attributes: |
|||
label: Version |
|||
description: Which version of ABP Studio are you using? |
|||
placeholder: Version |
|||
validations: |
|||
required: true |
|||
- type: dropdown |
|||
id: Operation-System |
|||
attributes: |
|||
label: Operation System |
|||
description: What is the operation system of the computer? |
|||
options: |
|||
- Windows (Default) |
|||
- Linux |
|||
- macOS |
|||
- Others |
|||
validations: |
|||
required: true |
|||
- type: textarea |
|||
id: solution-config |
|||
attributes: |
|||
label: Solution Configuration |
|||
description: | |
|||
If there is an open solution, what are the configurations of the solution? |
|||
🧐 Hint: You can see all the information about your solution from the configuration window, which opens when you right-click on the [solution](https://abp.io/docs/latest/studio/solution-explorer#solution) and click on the `Solution Configuration` button. |
|||
placeholder: | |
|||
- **Template**: app |
|||
- **Created ABP Studio Version**: 0.7.9 |
|||
- **Tiered**: No |
|||
- **UI Framework**: mvc |
|||
- **Theme**: leptonx |
|||
- **Theme Style**: system |
|||
- **Database Provider**: ef |
|||
- **Database Management System**: sqlserver |
|||
- **Separate Tenant Schema**: No |
|||
- **Mobile Framework**: none |
|||
- **Public Website**: No |
|||
- **Optional Modules**: |
|||
* GDPR |
|||
* TextTemplateManagement |
|||
* LanguageManagement |
|||
* AuditLogging |
|||
* SaaS |
|||
* OpenIddictAdmin |
|||
validations: |
|||
required: false |
|||
- type: markdown |
|||
attributes: |
|||
value: | |
|||
--- |
|||
- type: textarea |
|||
id: other-info |
|||
attributes: |
|||
label: Other information |
|||
description: | |
|||
If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. |
|||
placeholder: Other information |
|||
validations: |
|||
required: false |
|||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,70 @@ |
|||
# BLOB Storing Google Provider |
|||
|
|||
BLOB Storing Google Provider can store BLOBs in [Google Cloud Storage](https://cloud.google.com/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 Google Cloud Storage as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.Google](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Google) 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.Google` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.Google` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.Google](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Google) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringGoogleModule))]` 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 Google storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseGoogle(google => |
|||
{ |
|||
google.ClientEmail = "your coogle client email"; |
|||
google.ProjectId = "your coogle project id"; |
|||
google.PrivateKey = "your coogle private key"; |
|||
google.Scopes = "your coogle scopes"; |
|||
google.ContainerName = "your coogle container name"; |
|||
google.CreateContainerIfNotExists = true; |
|||
//google.UseApplicationDefaultCredentials = true; // If you want to use application default credentials |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](../blob-storing) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **ClientEmail** (string): The client email of the Google service account. You can create a service account and get the client email. Please refer to Google documentation: https://cloud.google.com/iam/docs/service-account-overview |
|||
* **ProjectId** (string): The project ID of the Google service account. |
|||
* **PrivateKey** (string): The private key of the Google service account. You can create a service account and get the private key. Please refer to Google documentation: https://cloud.google.com/iam/docs/keys-create-delete |
|||
* **Scopes** (string): The scopes of the Google service account. |
|||
* **UseApplicationDefaultCredentials** (bool): If `true`, it uses the application default credentials(ADC). Default value is `false`. Please refer to Google documentation: https://cloud.google.com/docs/authentication/provide-credentials-adc |
|||
* **ContainerName** (string): You can specify the container name in Google. 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 Google has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): |
|||
* Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. |
|||
* Every dash (-) character must be immediately preceded and followed by a letter or number; consecutive dashes are not permitted in container names. |
|||
* All letters in a container name must be **lowercase**. |
|||
* Container names must be from **3** through **63** characters long. |
|||
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Google, `GoogleBlobProvider` will try to create it. |
|||
|
|||
|
|||
## Google Blob Name Calculator |
|||
|
|||
Google 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/<tenant-id>` string if current tenant is not `null`. |
|||
* Appends the BLOB name. |
|||
|
|||
## Other Services |
|||
|
|||
* `GoogleBlobProvider` is the main service that implements the Google BLOB storage provider, if you want to override/replace it via [dependency injection](../../fundamentals/dependency-injection.md) (don't replace `IBlobProvider` interface, but replace `GoogleBlobProvider` class). |
|||
* `IGoogleBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultGoogleBlobNameCalculator` by default. |
|||
|
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,91 @@ |
|||
# ABP Studio Release Notes |
|||
|
|||
This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. |
|||
|
|||
## 0.7.9 (2024-08-29) |
|||
|
|||
* Opened Readme.md file after module creation or opening a solution |
|||
* Refactor & made enhancements to microservice template |
|||
* Fixed some bugs that occur while creating a new module |
|||
* Synchronized new templates with the old templates |
|||
* Angular: Fixed logo problem in templates |
|||
|
|||
## 0.7.8 (2024-08-23) |
|||
|
|||
* Revised the new solution creation wizard and improved for UX |
|||
* Fixed bugs on the Blazor WebApp template |
|||
* Reduced logs count per application |
|||
|
|||
## 0.7.7 (2024-08-14) |
|||
|
|||
* Updated LeptonX logos in new templates |
|||
* ABP Suite: Opened modules with .sln path instead of directory (fixes some problems in Suite integration) |
|||
* Enhancements on Solution Runner |
|||
|
|||
## 0.7.6 (2024-08-12) |
|||
|
|||
* Removed redundant helm repo installation |
|||
* Made enhancements to the Welcome Page area |
|||
* Microservice Template: Enabled `DynamicPermissionStore` in AuthServer |
|||
* Added Blazor WebApp option for Application template |
|||
* Added **Solution Creation Info** action for identifying the solution |
|||
|
|||
## 0.7.5 (2024-08-06) |
|||
|
|||
* Fixed ABP Suite integration for macOS |
|||
* Fixed build errors after upgrading to ABP 8.2.1 |
|||
* ABP Suite: Detected version inconsistencies and notified users to fix them automatically |
|||
|
|||
## 0.7.4 (2024-07-31) |
|||
|
|||
* Allowed creating open-source templates for active license owners |
|||
* Fixed basic theme problems in angular templates |
|||
* Handled docker related errors in solution runner |
|||
* Fixed versions of Chat Module packages |
|||
|
|||
## 0.7.3 (2024-07-27) |
|||
|
|||
* Added administrator mode indicator on the application title |
|||
* Made enhancements for aligning application title on macOS |
|||
|
|||
## 0.7.2 (2024-07-26) |
|||
|
|||
* Added `Blazor.Client` packages for alignment with new hosting logic |
|||
* Updated Microsoft packages to v8.0.4 |
|||
* Revised application upgrade process |
|||
* Fixed profile photo problem in tiered projects |
|||
* Removed **LinkedAccounts** and **AuthorityDelegation** menu from templates |
|||
|
|||
## 0.7.1 (2024-07-24) |
|||
|
|||
* Fixed restart problem after installing extensions on macOS |
|||
* Provided a model for sharing the same Kubernetes cluster between developers |
|||
* Fixed the **Text Template Management** Module build problem in the app template |
|||
* Fixed login problem for the community edition |
|||
* Fixed all reported bugs & made enhancements |
|||
|
|||
## 0.7.0 (2024-07-17) |
|||
|
|||
* Added an option for building & starting C# applications directly |
|||
* Disabled redis on migrator when the solution is tiered |
|||
* Allowed Generating Static Proxies on ABP Studio UI |
|||
|
|||
## 0.6.9 (2024-06-27) |
|||
|
|||
* Allowed to open ABP Studio by clicking on a `*.abpsln` file |
|||
* Fixed solution runner multiple instance problem |
|||
* Allowed to install an NPM package to a package |
|||
|
|||
## 0.6.8 (2024-06-13) |
|||
|
|||
* Added free to pro upgrade option |
|||
* Fixed microservice template helm chart bugs |
|||
* Made enhancements to the new CLI |
|||
|
|||
## 0.6.7 (2024-05-31) |
|||
|
|||
* Added free option to app templates |
|||
* Made social login optional for the app template |
|||
* Added open-source (free) templates |
|||
* Started showing angular projects on the browser tab |
|||
* Introduced the Community Edition |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks> |
|||
<Nullable>enable</Nullable> |
|||
<WarningsAsErrors>Nullable</WarningsAsErrors> |
|||
<AssemblyName>Volo.Abp.BlobStoring.Google</AssemblyName> |
|||
<PackageId>Volo.Abp.BlobStoring.Google</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" /> |
|||
<PackageReference Include="Google.Cloud.Storage.V1" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Google; |
|||
|
|||
[DependsOn(typeof(AbpBlobStoringModule))] |
|||
public class AbpBlobStoringGoogleModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
@ -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<GoogleBlobProviderConfiguration> googleConfigureAction) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(GoogleBlobProvider); |
|||
containerConfiguration.NamingNormalizers.TryAdd<GoogleBlobNamingNormalizer>(); |
|||
|
|||
googleConfigureAction(new GoogleBlobProviderConfiguration(containerConfiguration)); |
|||
|
|||
return containerConfiguration; |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// https://cloud.google.com/storage/docs/buckets#naming
|
|||
/// </summary>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// https://cloud.google.com/storage/docs/objects#naming
|
|||
/// </summary>
|
|||
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; |
|||
} |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
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<bool> 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<bool> ExistsAsync(BlobProviderExistsArgs args) |
|||
{ |
|||
var blobName = GoogleBlobNameCalculator.Calculate(args); |
|||
return await BlobExistsAsync(args, blobName); |
|||
} |
|||
|
|||
public async override Task<Stream?> 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<bool> 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<bool> 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<StorageClient> GetStorageClientClientAsync(BlobProviderArgs args) |
|||
{ |
|||
var configuration = args.Configuration.GetGoogleConfiguration(); |
|||
if (configuration.UseApplicationDefaultCredentials) |
|||
{ |
|||
return await StorageClient.CreateAsync(); |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Google; |
|||
|
|||
public class GoogleBlobProviderConfiguration |
|||
{ |
|||
private readonly BlobContainerConfiguration _containerConfiguration; |
|||
|
|||
public GoogleBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
_containerConfiguration = containerConfiguration; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Unique identifier for your project.
|
|||
/// For more info see: https://cloud.google.com/resource-manager/docs/creating-managing-projects
|
|||
/// </summary>
|
|||
public string? ProjectId { |
|||
get => _containerConfiguration.GetConfigurationOrDefault<string>(GoogleBlobProviderConfigurationNames.ProjectId); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ProjectId, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Email address that generated by the Google Cloud.
|
|||
/// </summary>
|
|||
public string? ClientEmail { |
|||
get => _containerConfiguration.GetConfigurationOrDefault<string>(GoogleBlobProviderConfigurationNames.ClientEmail); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ClientEmail, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Private key that generated by Google Cloud.
|
|||
/// Starts with '-----BEGIN PRIVATE KEY-----'
|
|||
/// and ends with '-----END PRIVATE KEY-----'
|
|||
/// </summary>
|
|||
public string? PrivateKey { |
|||
get => _containerConfiguration.GetConfigurationOrDefault<string>(GoogleBlobProviderConfigurationNames.PrivateKey); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.PrivateKey, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Available OAuth 2.0 scopes.
|
|||
/// </summary>
|
|||
public List<string>? Scopes { |
|||
get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.Scopes, new List<string>()); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.Scopes, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Use the application default credentials. https://cloud.google.com/docs/authentication/provide-credentials-adc
|
|||
/// Default value: false.
|
|||
/// </summary>
|
|||
public bool UseApplicationDefaultCredentials { |
|||
get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.UseApplicationDefaultCredentials, false); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.UseApplicationDefaultCredentials, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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 <see cref="BlobProviderArgs"/> will be used.
|
|||
/// </summary>
|
|||
public string? ContainerName { |
|||
get => _containerConfiguration.GetConfigurationOrDefault<string>(GoogleBlobProviderConfigurationNames.ContainerName); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.ContainerName, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Default value: false.
|
|||
/// </summary>
|
|||
public bool CreateContainerIfNotExists { |
|||
get => _containerConfiguration.GetConfigurationOrDefault(GoogleBlobProviderConfigurationNames.CreateContainerIfNotExists, false); |
|||
set => _containerConfiguration.SetConfiguration(GoogleBlobProviderConfigurationNames.CreateContainerIfNotExists, value); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
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 UseApplicationDefaultCredentials = "Google.ApplicationDefaultCredentials"; |
|||
public const string ContainerName = "Google.ContainerName"; |
|||
public const string CreateContainerIfNotExists = "Google.CreateContainerIfNotExists"; |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.BlobStoring.Google; |
|||
|
|||
public interface IGoogleBlobNameCalculator |
|||
{ |
|||
string Calculate(BlobProviderArgs args); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net8.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<UserSecretsId>9f0d2c00-80c1-435b-bfab-2c39c8249091</UserSecretsId> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.BlobStoring.Google\Volo.Abp.BlobStoring.Google.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<ProjectReference Include="..\Volo.Abp.BlobStoring.Tests\Volo.Abp.BlobStoring.Tests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,19 @@ |
|||
using Volo.Abp.Testing; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Google; |
|||
|
|||
public class AbpBlobStoringGoogleTestCommonBase : AbpIntegratedTest<AbpBlobStoringGoogleTestCommonModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
} |
|||
|
|||
public class AbpBlobStoringGoogleTestBase : AbpIntegratedTest<AbpBlobStoringGoogleTestModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// This module will not try to connect to Google Cloud Storage.
|
|||
/// </summary>
|
|||
[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<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureAll((containerName, containerConfiguration) => |
|||
{ |
|||
containerConfiguration.UseGoogle(google => |
|||
{ |
|||
google.ClientEmail = _clientEmail; |
|||
google.ProjectId = _projectId; |
|||
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
|
|||
} |
|||
} |
|||
} |
|||
@ -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<IBlobNamingNormalizer>(); |
|||
} |
|||
|
|||
[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"); |
|||
} |
|||
} |
|||
@ -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<AbpBlobStoringGoogleTestModule> |
|||
{ |
|||
public GoogleBlobContainer_Tests() |
|||
{ |
|||
} |
|||
} |
|||
*/ |
|||
@ -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<IGoogleBlobNameCalculator>(); |
|||
_currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
} |
|||
|
|||
[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 |
|||
); |
|||
} |
|||
} |
|||