@ -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 |
|||
@ -0,0 +1,64 @@ |
|||
|
|||
|
|||
 |
|||
|
|||
Ahead-of-Time (AOT) compilation and Just-in-Time (JIT) compilation are two different methods for compiling Angular applications. Here's a breakdown of the differences between them: |
|||
|
|||
### **Ahead-of-Time (AOT) Compilation** |
|||
|
|||
**What is AOT?** |
|||
|
|||
- AOT compilation refers to the process of compiling your Angular application during the build phase, before the application is served to the browser. |
|||
- The Angular compiler converts your TypeScript and HTML templates into efficient JavaScript code ahead of time. |
|||
|
|||
**Advantages:** |
|||
|
|||
1. **Faster Rendering:** Since the compilation is done beforehand, the browser receives precompiled code, leading to faster rendering and better performance when the app loads. |
|||
2. **Smaller Bundle Size:** AOT eliminates the need for the Angular compiler in the client-side bundle, which reduces the overall bundle size. |
|||
3. **Improved Security:** AOT compilation checks your templates and bindings during the build process, catching errors early and reducing the risk of injection attacks. |
|||
4. **Early Error Detection:** Errors related to templates and bindings are caught during the build time rather than at runtime, leading to more robust and error-free applications. |
|||
|
|||
**When to Use:** |
|||
|
|||
- AOT is typically used in production builds because it provides better performance, smaller bundles, and more secure applications. |
|||
|
|||
**How to Use:** |
|||
|
|||
- AOT is the default when you run `ng build --prod` in an Angular project. |
|||
|
|||
### **Just-in-Time (JIT) Compilation** |
|||
|
|||
**What is JIT?** |
|||
|
|||
- JIT compilation occurs in the browser at runtime. The Angular compiler translates the TypeScript and HTML templates into JavaScript code just before the application runs in the browser. |
|||
- The application is compiled on the fly as the user interacts with it. |
|||
|
|||
**Advantages:** |
|||
|
|||
1. **Faster Build Time:** Since there’s no pre-compilation step, the build process is faster during development. |
|||
2. **More Flexible Development:** JIT allows for rapid iteration during development, as changes can be quickly reflected without needing to rebuild the entire application. |
|||
3. **Dynamic Components:** JIT allows for more flexibility in scenarios where components need to be dynamically created or compiled at runtime. |
|||
|
|||
**When to Use:** |
|||
|
|||
- JIT is typically used in development environments because it allows for quicker build times and easier debugging. |
|||
|
|||
**How to Use:** |
|||
|
|||
- JIT is the default compilation method when you run `ng serve` for development builds in Angular. |
|||
|
|||
### **Comparison Summary:** |
|||
|
|||
| Feature | AOT (Ahead-of-Time) | JIT (Just-in-Time) | |
|||
| ---------------------- | ----------------------------- | ---------------------------------- | |
|||
| **Compilation Timing** | At build time | At runtime | |
|||
| **Performance** | Faster application startup | Slower application startup | |
|||
| **Bundle Size** | Smaller (no Angular compiler) | Larger (includes Angular compiler) | |
|||
| **Error Detection** | Catches template errors early | Errors caught at runtime | |
|||
| **Use Case** | Production | Development | |
|||
| **Dynamic Components** | Less flexible | More flexible | |
|||
|
|||
### **Best Practices:** |
|||
|
|||
- **Use AOT** for production builds to ensure faster load times, smaller bundle sizes, and more secure applications. |
|||
- **Use JIT** during development to take advantage of quicker builds and easier debugging. |
|||
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 139 KiB |
@ -0,0 +1,85 @@ |
|||
# What is Angular Schematics? |
|||
|
|||
 |
|||
|
|||
**Angular Schematics** is a powerful tool which is part of the Angular CLI that allows developers to automate various development tasks by **generating and modifying code**. Schematics provides a way to create **templates and boilerplate code** for Angular applications or libraries, enabling consistency and reducing the amount of repetitive work. |
|||
|
|||
### Key Concepts of Angular Schematics: |
|||
|
|||
1. **Code Generation:** |
|||
- Schematics can generate boilerplate code for various Angular artifacts, such as components, services, modules, pipes... For example, when you run a command like `ng generate component my-component`, Angular uses a schematic to create the component files and update the necessary modules. |
|||
|
|||
2. **Code Modification:** |
|||
- Schematics can also modify existing code in your project. This includes tasks like updating configuration files, adding new routes, or modifying Angular modules. They can be very useful when upgrading projects to a new version of Angular or adding new features that require changes to the existing codebase. |
|||
|
|||
3. **Custom Schematics:** |
|||
- Developers can create their own custom schematics to automate repetitive tasks specific to their projects. For example, if your team frequently needs to set up a certain type of service or configure specific libraries, you can create a schematic that automates these steps. In [ABP Suite](https://abp.io/suite), we use the custom schematics templates to generate CRUD pages for Angular UI. |
|||
|
|||
4. **Collection of Schematics:** |
|||
- Schematics are often grouped into collections. The Angular CLI itself is a collection of schematics. You can create your own collection or use third-party schematic collections created by the Angular community. |
|||
|
|||
5. **Integration with Angular CLI:** |
|||
- Schematics is integrated with the Angular CLI. The `ng generate` and `ng add` commands are examples of how schematics are used in everyday Angular development. When you use these commands, the Angular CLI runs the corresponding schematic to perform the desired operation. |
|||
|
|||
6. **Upgrade Tasks:** |
|||
- Schematics can be used to automate the process of upgrading Angular applications. For example, the Angular Update command (`ng update`) uses schematics to automatically apply necessary changes to your project when upgrading to a new version of Angular. |
|||
|
|||
### Common Use Cases: |
|||
|
|||
- **Generating Components, Services, Modules.:** Easily create Angular building blocks with commands like `ng generate component`, `ng generate service`, or `ng generate module`. |
|||
|
|||
- **Adding Libraries:** Automatically set up and configure third-party libraries with `ng add`. For example, `ng add @angular/material` uses a schematic to install and configure Angular Material in your project. |
|||
|
|||
- **Automating Project Upgrades:** Simplify the process of upgrading Angular versions by running `ng update`, which uses schematics to make necessary code changes. |
|||
|
|||
- **Custom Project Scaffolding:** Create custom schematics to enforce your team's development standards and best practices by automating the creation of specific project structures. |
|||
|
|||
### How to Create a Custom Schematic: |
|||
|
|||
Creating a custom schematic involves several steps: |
|||
1. **Install the Schematics CLI:** |
|||
```bash |
|||
npm install -g @angular-devkit/schematics-cli |
|||
``` |
|||
|
|||
2. **Create a New Schematic Project:** |
|||
```bash |
|||
schematics blank --name=my-schematic |
|||
cd my-schematic |
|||
``` |
|||
|
|||
3. **Define Your Schematic:** Modify the files in the schematic project to define what your schematic will generate or modify. |
|||
|
|||
4. **Test Your Schematic:** You can run your schematic locally using the `schematics` command. |
|||
|
|||
5. **Publish Your Schematic (Optional):** Once your schematic is ready, you can publish it to npm or include it in your Angular projects. |
|||
|
|||
#### Example: |
|||
If you want to create a custom component with specific settings or styles, you can create a schematic to automate this. Every time a developer on your team needs to create this type of component, they can run the schematic, ensuring consistency across the project. |
|||
|
|||
|
|||
|
|||
### Helpful Links |
|||
|
|||
Here are the direct links for the Angular Schematics resources: |
|||
|
|||
- [Angular Official Documentation: Schematics Overview — angular.io](https://angular.io/guide/schematics) |
|||
|
|||
- [Creating Custom Schematics — angular.io](https://angular.io/guide/schematics-for-libraries) |
|||
|
|||
- [CLI Schematic Command — angular.io](https://angular.io/cli/schematic) |
|||
|
|||
- [Devkit Schematics— github.com](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/schematics) |
|||
|
|||
- [Schematics Examples — github.com](https://github.com/angular/angular-cli/tree/main/packages/schematics/angular) |
|||
|
|||
- [Debugging Angular Schematics — dev.to](https://dev.to/nikolasleblanc/debugging-angular-schematics-1ne0) |
|||
|
|||
- [Using Schematics with Nx — nx.dev](https://nx.dev/guides/using-angular-schematics) |
|||
|
|||
- [Schematics API — angular.io](https://angular.io/api/schematics) |
|||
|
|||
|
|||
|
|||
### Conclusion: |
|||
Angular Schematics is a powerful tool for automating repetitive tasks, generating consistent code, and managing project upgrades. By leveraging schematics, Angular developers can save time, reduce errors, and enforce best practices across their projects. |
|||
|
After Width: | Height: | Size: 35 KiB |
@ -0,0 +1,123 @@ |
|||
# Do You Really Need Multi-tenancy? |
|||
|
|||
This article discusses whether you need a multi-tenancy architecture for your next project. Answer my critical questions to decide if multi-tenancy suits your application or not! |
|||
|
|||
 |
|||
|
|||
## What’s Multi-tenancy? |
|||
|
|||
It’s an architectural approach to building SaaS solutions. In this model, the hardware and software resources are shared between tenants, and application data is virtually or physically isolated between tenants. Here, **the main goal is minimizing costs and maximizing customer count**. |
|||
|
|||
**An ideal multi-tenant application should be;** |
|||
|
|||
- A multi-tenancy system should be designed to **work seamlessly** and make your application code **multi-tenancy aware** as much as possible. |
|||
- When a customer wants to separate their database, it **should also be deployable to on-premise**. |
|||
|
|||
|
|||
 |
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Multi-tenant Development is Hard - Reconsider! |
|||
|
|||
**Developing a multi-tenant application is harder** compared to non-multi-tenant applications. You add `TenandId` to all your shared entities. And each time you make a query, you need to filter by `TenantId`. There is an increased risk of security breaches if one tenant's data is compromised. Multi-tenancy can limit the extent of customization available to each tenant since they all operate on the same core infrastructure. Also, in a microservice environment, things get two times more complicated. So you should carefully consider if you need multi-tenancy or not. Sometimes, I got questions like; |
|||
|
|||
 |
|||
|
|||
**No! These are not multi-tenant applications.** It just needs a simple filtering by your different branches/faculties/departments/holding sub-companies/ groups or whatever hierarchy is… |
|||
**You are confusing "grouping" with "resource sharing".** |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Do I Really Need Multi-tenancy? |
|||
|
|||
Ask yourself the following questions if you cannot decide whether your app needs multi-tenancy or not; |
|||
|
|||
1. Can a user be shared among other tenants? |
|||
2. Any tenant needs to see other tenant's data? |
|||
3. Does your application break when you physically move one of the tenants? |
|||
4. Do your customers need higher security and better GDPR enforcement? |
|||
5. Do you need cumulative queries over your tenants? |
|||
|
|||
|
|||
|
|||
Let's answer these questions; |
|||
|
|||
|
|||
|
|||
##### **1. Can a user be shared among other tenants?** |
|||
|
|||
If you need to *share a user among other tenants, or in other words, users can be members of different tenants,* then **your application is definitely not multi-tenant**! Multi-tenancy means a tenant’s data is always isolated, even if it’s logically separated. **You cannot share a user among your tenants.** The reason is simple: In the future, if you move a tenant to on-premise, then your application will break! |
|||
|
|||
 |
|||
|
|||
##### **2. Does any tenant need to see other tenants' data?** |
|||
|
|||
If your answer is **YES**, **then** **your application is not multi-tenant**. In multi-tenant apps, the tenant's data cannot be shared in any circumstances among the other tenants. Business decision-makers sometimes want to share some entities with other tenants. If there's this requirement, you shouldn't start a multi-tenant architecture. You can simply group these tenants manually. Again, reply to this question; **In the future, if I move a tenant physically to another environment, will my app still work properly?** In this case, it will not! |
|||
|
|||
 |
|||
|
|||
Let's say your app is *Amazon.com* and you have a product entity *iPhone* that you want to share with other sellers. This requirement violates the multi-tenant rule. While your *Amazon.com* is still SaaS, it shouldn't be multi-tenant. |
|||
|
|||
On the other hand, if you serve several online shopping websites dedicated to different brands. Let's say Nike and Adidas want to have their own e-commerce websites. And your ***Amazon.com* provides these companies with their own subdomains: *nike.amazon.com* and *adidas.amazon.com***. In this architecture, these companies will have their own user, roles, settings. Also these companies will have their own branding like custom login screen, custom logo, different theme layouts, menu items, language options, payment gateways etc... Hence, **this is %100 multi-tenant.** |
|||
|
|||
|
|||
|
|||
##### **3. Does your application break when you physically move one of the tenants?** |
|||
|
|||
If your answer is **YES**, you should **stop making it multi-tenant**. It is **not a multi-tenant app**! This means your tenants are tightly coupled with the application's infrastructure or database, and this requirement prevents you from making it multi-tenant because it disrupts the entire system when you take out a tenant. |
|||
|
|||
 |
|||
|
|||
|
|||
##### **4. Do your customers need higher security and better GDPR enforcement?** |
|||
|
|||
If your answer is **YES**, you **should not make it multi-tenancy.** When a **hacker** gets into your server, he can **steal all your client data**. Also, if you have a security hole, **a tenant can gain access** to other tenants' data. Especially your tenants' data is being shared in the same database... On the other hand, some customers, for example, government agencies or banks, may be required to locate the database in their own geo-location and make it unreachable from the main application. In this case, you should go with a single-tenant architecture. Another difficulty is that some tenants may have **different data retention policies**, so you must implement different strategies for each tenant. In this case, you should also consider making it a single-tenant. |
|||
|
|||
 |
|||
|
|||
##### **5. Do you need cumulative queries over your tenants?** |
|||
|
|||
This question is not a clear decision parameter for multi-tenancy. But it can be a supportive parameter for your decision. Actually, this is a common feature in multi-tenant systems where aggregated reporting or analytics across tenants is required. In a virtually isolated multi-tenant environment, you can easily do this by just disabling the multi-tenancy filter. However, when some of your tenants have separate databases or application instances, it becomes harder to get aggregate reports. |
|||
|
|||
Do you need cumulative queries over your tenants? If your answer is YES, then you should keep all your tenants' data in a shared database to easily query them. But if you still need to physically move some of the tenants, then you have the following strategies: |
|||
|
|||
* First, all your databases need to be on the same network so that you can gather data from different databases. |
|||
* Second, in some cases, according to the GDPR requirements or government agency regulations, you must locate the database in their geo-location. And you cannot connect to these databases. In this case, you can create a Web API in your application that gives you a report for this specific tenant. Later, you'll gather the reports from all your tenants like this and merge them. |
|||
* Third; When the tenant databases are in different geo-location (inaccessible), then each tenant can send their report to your central server. So you can generate cumulative reports. |
|||
|
|||
|
|||
|
|||
## Conclusion |
|||
|
|||
- It's important to decide on the first day whether your application needs to be multi-tenant. To decide this, consider these topics; |
|||
|
|||
- **Do my tenants really have a relationship with each other?** No, they have nothing to do in common; **OK, go with multi-tenancy**. |
|||
- **My tenants don't have a relationship** with each other, and the only thing they have in common is sharing my application. **OK, go with multi-tenancy**. |
|||
- **When a tenant leaves the system (by removing their data), the other tenants also stop working** This means that the tenants are coupled. **Do not do multi-tenancy!** |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
We’ve been working for a long time on multi-tenancy, microservices, modular development topics, and developing tools and frameworks for repetitive development tasks. Check out the amazing open-source [ABP Framework](https://github.com/abpframework/abp), which provides all the alternative solutions to implement a multi-tenant architecture. The multi-tenancy logic is seamlessly done at the core level, and you can simply focus on your business code. Keep it simple, nice and neat with ABP. |
|||
|
|||
|
|||
```bash |
|||
ABP.IO: The Modern Way of Developing Web Apps for .NET Developers! |
|||
``` |
|||
|
|||
|
|||
|
|||
Alper Ebicoglu / [x.com/alperebicoglu](https://x.com/alperebicoglu) |
|||
|
|||
— ABP Team |
|||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 989 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 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. |
|||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 34 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,15 @@ |
|||
# ABP Studio and ABP Solution Templates Version Compatibility |
|||
|
|||
This document provides an overview of the compatibility between various versions of ABP Studio and [ABP Solution Templates](../solution-templates/index.md). Use this reference to ensure that you are using the correct versions of ABP Studio and ABP for your projects. |
|||
|
|||
| **ABP Studio Version** | **ABP Version** | |
|||
|------------------------|---------------------------| |
|||
| 0.7.8 | 8.2.2 | |
|||
| 0.7.4 to 0.7.7 | 8.2.1 | |
|||
| 0.7.0 to 0.7.3 | 8.2.0 | |
|||
| 0.6.8 - 0.6.9 | 8.1.3 | |
|||
| 0.6.7 | 8.1.1 | |
|||
|
|||
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: |
|||
|
|||
 |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.Libs; |
|||
|
|||
public class AbpMvcLibsOptions |
|||
{ |
|||
public bool CheckLibs { get; set; } |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Hosting; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Libs; |
|||
|
|||
public class AbpMvcLibsService : IAbpMvcLibsService, ITransientDependency |
|||
{ |
|||
private Task<bool>? _checkLibsTask; |
|||
|
|||
public virtual void CheckLibs(ApplicationInitializationContext context) |
|||
{ |
|||
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpMvcLibsOptions>>().Value; |
|||
if (options.CheckLibs) |
|||
{ |
|||
var app = context.GetApplicationBuilderOrNull(); |
|||
if (app == null) |
|||
{ |
|||
var logger = context.ServiceProvider.GetRequiredService<ILogger<AbpMvcLibsService>>(); |
|||
logger.LogWarning($"The {nameof(IApplicationBuilder)} is not available. The 'CheckLibs' feature is disabled!"); |
|||
return; |
|||
} |
|||
|
|||
app.Use(async (httpContext, next) => |
|||
{ |
|||
if (!await CheckLibsAsyncOnceAsync(httpContext)) |
|||
{ |
|||
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; |
|||
httpContext.Response.ContentType = "text/html"; |
|||
await httpContext.Response.WriteAsync( |
|||
"<html>" + |
|||
" <head>" + |
|||
" <title>Error - The Libs folder is missing!</title>" + |
|||
" </head>" + |
|||
" <body>" + |
|||
" <h1> ⚠️ The Libs folder under the <code style='background-color: #e7e7e7;'>wwwroot/libs</code> directory is empty!</h1>" + |
|||
" <p>The Libs folder contains mandatory NPM Packages for running the project.</p>" + |
|||
" <p>Make sure you run the <code style='background-color: #e7e7e7;'>abp install-libs</code> CLI tool command.</p>" + |
|||
" <p>For more information, check out the <a href='https://abp.io/docs/latest/CLI#install-libs'>ABP CLI documentation</a></p>" + |
|||
" </body>" + |
|||
"</html>", |
|||
Encoding.UTF8 |
|||
); |
|||
return; |
|||
} |
|||
|
|||
await next(httpContext); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
protected virtual Task<bool> CheckLibsAsyncOnceAsync(HttpContext httpContext) |
|||
{ |
|||
if (_checkLibsTask == null) |
|||
{ |
|||
_checkLibsTask = CheckLibsAsync(httpContext); |
|||
} |
|||
|
|||
return _checkLibsTask; |
|||
} |
|||
|
|||
protected virtual Task<bool> CheckLibsAsync(HttpContext httpContext) |
|||
{ |
|||
var logger = httpContext.RequestServices.GetRequiredService<ILogger<AbpMvcLibsService>>(); |
|||
try |
|||
{ |
|||
var fileProvider = new PhysicalFileProvider(httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().WebRootPath); |
|||
var libsFolder = fileProvider.GetDirectoryContents("/libs"); |
|||
if (!libsFolder.Exists || !libsFolder.Any()) |
|||
{ |
|||
logger.LogError("The 'wwwroot/libs' folder does not exist or empty!"); |
|||
return Task.FromResult(false); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
// In case of any exception, log it and return true to prevent crashing the application.
|
|||
logger.LogError(e, "An error occurred while checking the libs folder!"); |
|||
} |
|||
|
|||
return Task.FromResult(true); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.Libs; |
|||
|
|||
public interface IAbpMvcLibsService |
|||
{ |
|||
void CheckLibs(ApplicationInitializationContext context); |
|||
} |
|||
@ -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,28 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Http; |
|||
|
|||
public static class UrlHelpers |
|||
{ |
|||
private const string WildcardSubdomain = "*."; |
|||
|
|||
public static bool IsSubdomainOf(string subdomain, string domain) |
|||
{ |
|||
if (Uri.TryCreate(subdomain, UriKind.Absolute, out var subdomainUri) && |
|||
Uri.TryCreate(domain.Replace(WildcardSubdomain, string.Empty), UriKind.Absolute, out var domainUri)) |
|||
{ |
|||
return domainUri == subdomainUri || IsSubdomainOf(subdomainUri, domainUri); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public static bool IsSubdomainOf(Uri subdomain, Uri domain) |
|||
{ |
|||
return subdomain.IsAbsoluteUri |
|||
&& domain.IsAbsoluteUri |
|||
&& subdomain.Scheme == domain.Scheme |
|||
&& subdomain.Port == domain.Port |
|||
&& subdomain.Host.EndsWith($".{domain.Host}", StringComparison.Ordinal); |
|||
} |
|||
} |
|||
@ -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 |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Http; |
|||
|
|||
public class UrlHelpers_Tests |
|||
{ |
|||
[Theory] |
|||
[InlineData(null)] |
|||
[InlineData("null")] |
|||
[InlineData("http://")] |
|||
[InlineData("http://*")] |
|||
[InlineData("http://.domain")] |
|||
[InlineData("http://.domain/hello")] |
|||
public void IsSubdomainOf_ReturnsFalseIfDomainIsMalformedUri(string domain) |
|||
{ |
|||
var actual = UrlHelpers.IsSubdomainOf("http://*.domain", domain); |
|||
actual.ShouldBeFalse(); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("http://sub.domain", "http://domain")] |
|||
[InlineData("http://sub.domain", "http://*.domain")] |
|||
[InlineData("http://sub.sub.domain", "http://*.domain")] |
|||
[InlineData("http://sub.sub.domain", "http://*.sub.domain")] |
|||
[InlineData("http://sub.domain:4567", "http://*.domain:4567")] |
|||
public void IsSubdomainOf_ReturnsTrue_WhenASubdomain(string subdomain, string domain) |
|||
{ |
|||
var actual = UrlHelpers.IsSubdomainOf(subdomain, domain); |
|||
actual.ShouldBeTrue(); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("http://sub.domain:1234", "http://*.domain:5678")] |
|||
[InlineData("http://sub.domain", "http://domain.*")] |
|||
[InlineData("http://sub.domain.hacker", "http://*.domain")] |
|||
[InlineData("https://sub.domain", "http://*.domain")] |
|||
public void IsSubdomainOf_ReturnsFalse_WhenNotASubdomain(string subdomain, string domain) |
|||
{ |
|||
var actual = UrlHelpers.IsSubdomainOf(subdomain, domain); |
|||
actual.ShouldBeFalse(); |
|||
} |
|||
} |
|||