mirror of https://github.com/abpframework/abp.git
committed by
GitHub
22 changed files with 972 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="AWSSDK.S3" Version="3.3.111.27" /> |
||||
|
<PackageReference Include="AWSSDK.SecurityToken" Version="3.3.105.30" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,14 @@ |
|||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
[DependsOn(typeof(AbpBlobStoringModule), |
||||
|
typeof(AbpCachingModule))] |
||||
|
public class AbpBlobStoringAwsModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public static class AwsBlobContainerConfigurationExtensions |
||||
|
{ |
||||
|
public static AwsBlobProviderConfiguration GetAwsConfiguration( |
||||
|
this BlobContainerConfiguration containerConfiguration) |
||||
|
{ |
||||
|
return new AwsBlobProviderConfiguration(containerConfiguration); |
||||
|
} |
||||
|
|
||||
|
public static BlobContainerConfiguration UseAws( |
||||
|
this BlobContainerConfiguration containerConfiguration, |
||||
|
Action<AwsBlobProviderConfiguration> awsConfigureAction) |
||||
|
{ |
||||
|
containerConfiguration.ProviderType = typeof(AwsBlobProvider); |
||||
|
containerConfiguration.NamingNormalizers.TryAdd<AwsBlobNamingNormalizer>(); |
||||
|
|
||||
|
awsConfigureAction(new AwsBlobProviderConfiguration(containerConfiguration)); |
||||
|
|
||||
|
return containerConfiguration; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System.Text.RegularExpressions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class AwsBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
///https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
||||
|
/// </summary>
|
||||
|
public virtual string NormalizeContainerName(string containerName) |
||||
|
{ |
||||
|
// All letters in a container name must be lowercase.
|
||||
|
containerName = containerName.ToLower(); |
||||
|
|
||||
|
// Container names can contain only letters, numbers, and the dash (-) character.
|
||||
|
containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty); |
||||
|
|
||||
|
// Every dash (-) character must be immediately preceded and followed by a letter or number;
|
||||
|
// consecutive dashes are not permitted in container names.
|
||||
|
// Container names must start or end with a letter or number
|
||||
|
containerName = Regex.Replace(containerName, "-{2,}", "-"); |
||||
|
containerName = Regex.Replace(containerName, "^-", string.Empty); |
||||
|
containerName = Regex.Replace(containerName, "-$", string.Empty); |
||||
|
|
||||
|
// Container names must be from 3 through 63 characters long.
|
||||
|
if (containerName.Length < 3) |
||||
|
{ |
||||
|
var length = containerName.Length; |
||||
|
for (var i = 0; i < 3 - length; i++) |
||||
|
{ |
||||
|
containerName += "0"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (containerName.Length > 63) |
||||
|
{ |
||||
|
containerName = containerName.Substring(0, 63); |
||||
|
} |
||||
|
|
||||
|
return containerName; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
|
||||
|
/// </summary>
|
||||
|
public virtual string NormalizeBlobName(string blobName) |
||||
|
{ |
||||
|
return blobName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Amazon.S3; |
||||
|
using Amazon.S3.Model; |
||||
|
using Amazon.S3.Util; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class AwsBlobProvider : BlobProviderBase, ITransientDependency |
||||
|
{ |
||||
|
protected IAwsBlobNameCalculator AwsBlobNameCalculator { get; } |
||||
|
protected IAmazonS3ClientFactory AmazonS3ClientFactory { get; } |
||||
|
|
||||
|
public AwsBlobProvider(IAwsBlobNameCalculator awsBlobNameCalculator, |
||||
|
IAmazonS3ClientFactory amazonS3ClientFactory) |
||||
|
{ |
||||
|
AwsBlobNameCalculator = awsBlobNameCalculator; |
||||
|
AmazonS3ClientFactory = amazonS3ClientFactory; |
||||
|
} |
||||
|
|
||||
|
public override async Task SaveAsync(BlobProviderSaveArgs args) |
||||
|
{ |
||||
|
var blobName = AwsBlobNameCalculator.Calculate(args); |
||||
|
var configuration = args.Configuration.GetAwsConfiguration(); |
||||
|
var containerName = GetContainerName(args); |
||||
|
|
||||
|
using (var amazonS3Client = await GetAmazonS3Client(args)) |
||||
|
{ |
||||
|
if (!args.OverrideExisting && await BlobExistsAsync(amazonS3Client, containerName, blobName)) |
||||
|
{ |
||||
|
throw new BlobAlreadyExistsException( |
||||
|
$"Saving BLOB '{args.BlobName}' does already exists in the container '{containerName}'! Set {nameof(args.OverrideExisting)} if it should be overwritten."); |
||||
|
} |
||||
|
|
||||
|
if (configuration.CreateContainerIfNotExists) |
||||
|
{ |
||||
|
await CreateContainerIfNotExists(amazonS3Client, containerName); |
||||
|
} |
||||
|
|
||||
|
await amazonS3Client.PutObjectAsync(new PutObjectRequest |
||||
|
{ |
||||
|
BucketName = containerName, |
||||
|
Key = blobName, |
||||
|
InputStream = args.BlobStream |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override async Task<bool> DeleteAsync(BlobProviderDeleteArgs args) |
||||
|
{ |
||||
|
var blobName = AwsBlobNameCalculator.Calculate(args); |
||||
|
var containerName = GetContainerName(args); |
||||
|
|
||||
|
using (var amazonS3Client = await GetAmazonS3Client(args)) |
||||
|
{ |
||||
|
if (!await BlobExistsAsync(amazonS3Client, containerName, blobName)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
await amazonS3Client.DeleteObjectAsync(new DeleteObjectRequest |
||||
|
{ |
||||
|
BucketName = containerName, |
||||
|
Key = blobName |
||||
|
}); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override async Task<bool> ExistsAsync(BlobProviderExistsArgs args) |
||||
|
{ |
||||
|
var blobName = AwsBlobNameCalculator.Calculate(args); |
||||
|
var containerName = GetContainerName(args); |
||||
|
|
||||
|
using (var amazonS3Client = await GetAmazonS3Client(args)) |
||||
|
{ |
||||
|
return await BlobExistsAsync(amazonS3Client, containerName, blobName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override async Task<Stream> GetOrNullAsync(BlobProviderGetArgs args) |
||||
|
{ |
||||
|
var blobName = AwsBlobNameCalculator.Calculate(args); |
||||
|
var containerName = GetContainerName(args); |
||||
|
|
||||
|
using (var amazonS3Client = await GetAmazonS3Client(args)) |
||||
|
{ |
||||
|
if (!await BlobExistsAsync(amazonS3Client, containerName, blobName)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var response = await amazonS3Client.GetObjectAsync(new GetObjectRequest |
||||
|
{ |
||||
|
BucketName = containerName, |
||||
|
Key = blobName |
||||
|
}); |
||||
|
|
||||
|
var memoryStream = new MemoryStream(); |
||||
|
await response.ResponseStream.CopyToAsync(memoryStream); |
||||
|
return memoryStream; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<AmazonS3Client> GetAmazonS3Client(BlobProviderArgs args) |
||||
|
{ |
||||
|
var configuration = args.Configuration.GetAwsConfiguration(); |
||||
|
return await AmazonS3ClientFactory.GetAmazonS3Client(configuration); |
||||
|
} |
||||
|
|
||||
|
private async Task<bool> BlobExistsAsync(AmazonS3Client amazonS3Client, string containerName, string blobName) |
||||
|
{ |
||||
|
// Make sure Blob Container exists.
|
||||
|
if (!await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, containerName)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await amazonS3Client.GetObjectMetadataAsync(containerName, blobName); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
if (ex is AmazonS3Exception) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task CreateContainerIfNotExists(AmazonS3Client amazonS3Client, string containerName) |
||||
|
{ |
||||
|
if (!await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, containerName)) |
||||
|
{ |
||||
|
await amazonS3Client.PutBucketAsync(new PutBucketRequest |
||||
|
{ |
||||
|
BucketName = containerName |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string GetContainerName(BlobProviderArgs args) |
||||
|
{ |
||||
|
var configuration = args.Configuration.GetAwsConfiguration(); |
||||
|
return configuration.ContainerName.IsNullOrWhiteSpace() |
||||
|
? args.ContainerName |
||||
|
: configuration.ContainerName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class AwsBlobProviderConfiguration |
||||
|
{ |
||||
|
public string AccessKeyId |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.AccessKeyId); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.AccessKeyId, value); |
||||
|
} |
||||
|
|
||||
|
public string SecretAccessKey |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.SecretAccessKey); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.SecretAccessKey, value); |
||||
|
} |
||||
|
|
||||
|
public bool UseCredentials |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.UseCredentials,false); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.UseCredentials, value); |
||||
|
} |
||||
|
|
||||
|
public bool UseTemporaryCredentials |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.UseTemporaryCredentials,false); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.UseTemporaryCredentials, value); |
||||
|
} |
||||
|
|
||||
|
public bool UseTemporaryFederatedCredentials |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.UseTemporaryFederatedCredentials,false); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.UseTemporaryFederatedCredentials, value); |
||||
|
} |
||||
|
|
||||
|
public string ProfileName |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.ProfileName); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.ProfileName, value); |
||||
|
} |
||||
|
|
||||
|
public string ProfilesLocation |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.ProfilesLocation); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.ProfilesLocation, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set the validity period of the temporary access credential, the unit is s, the minimum is 900, and the maximum is 129600.
|
||||
|
/// </summary>
|
||||
|
public int DurationSeconds |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.DurationSeconds, 0); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.DurationSeconds, value); |
||||
|
} |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfiguration<string>(AwsBlobProviderConfigurationNames.Name); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Name, value); |
||||
|
} |
||||
|
|
||||
|
public string Policy |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.Policy); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Policy, value); |
||||
|
} |
||||
|
|
||||
|
public string Region |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfiguration<string>(AwsBlobProviderConfigurationNames.Region); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, Check.NotNull(value, nameof(value))); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// This name may only contain lowercase letters, numbers, and hyphens, and must begin with a letter or a number.
|
||||
|
/// Each hyphen must be preceded and followed by a non-hyphen character.
|
||||
|
/// The name must also be between 3 and 63 characters long.
|
||||
|
/// If this parameter is not specified, the ContainerName of the <see cref="BlobProviderArgs"/> will be used.
|
||||
|
/// </summary>
|
||||
|
public string ContainerName |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfiguration<string>(AwsBlobProviderConfigurationNames.ContainerName); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.ContainerName, Check.NotNullOrWhiteSpace(value, nameof(value))); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Default value: false.
|
||||
|
/// </summary>
|
||||
|
public bool CreateContainerIfNotExists |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.CreateContainerIfNotExists, false); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.CreateContainerIfNotExists, value); |
||||
|
} |
||||
|
|
||||
|
private readonly string _temporaryCredentialsCacheKey; |
||||
|
public string TemporaryCredentialsCacheKey |
||||
|
{ |
||||
|
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.TemporaryCredentialsCacheKey, _temporaryCredentialsCacheKey); |
||||
|
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.TemporaryCredentialsCacheKey, value); |
||||
|
} |
||||
|
|
||||
|
private readonly BlobContainerConfiguration _containerConfiguration; |
||||
|
|
||||
|
public AwsBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) |
||||
|
{ |
||||
|
_containerConfiguration = containerConfiguration; |
||||
|
_temporaryCredentialsCacheKey = Guid.NewGuid().ToString("N"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public static class AwsBlobProviderConfigurationNames |
||||
|
{ |
||||
|
public const string AccessKeyId = "Aws.AccessKeyId"; |
||||
|
public const string SecretAccessKey = "Aws.SecretAccessKey"; |
||||
|
public const string UseCredentials = "Aws.UseCredentials"; |
||||
|
public const string UseTemporaryCredentials = "Aws.UseTemporaryCredentials"; |
||||
|
public const string UseTemporaryFederatedCredentials = "Aws.UseTemporaryFederatedCredentials"; |
||||
|
public const string ProfileName = "Aws.ProfileName"; |
||||
|
public const string ProfilesLocation = "Aws.ProfilesLocation"; |
||||
|
public const string DurationSeconds = "Aws.DurationSeconds"; |
||||
|
public const string TemporaryCredentialsCacheKey = "Aws.TemporaryCredentialsCacheKey"; |
||||
|
public const string Name = "Aws.Name"; |
||||
|
public const string Policy = "Aws.Policy"; |
||||
|
public const string Region = "Aws.Region"; |
||||
|
public const string ContainerName = "Aws.ContainerName"; |
||||
|
public const string CreateContainerIfNotExists = "Aws.CreateContainerIfNotExists"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
[Serializable] |
||||
|
public class AwsTemporaryCredentialsCacheItem |
||||
|
{ |
||||
|
public string AccessKeyId { get; set; } |
||||
|
|
||||
|
public string SecretAccessKey { get; set; } |
||||
|
|
||||
|
public string SessionToken { get; set; } |
||||
|
|
||||
|
public AwsTemporaryCredentialsCacheItem() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public AwsTemporaryCredentialsCacheItem(string accessKeyId,string secretAccessKey,string sessionToken) |
||||
|
{ |
||||
|
AccessKeyId = accessKeyId; |
||||
|
SecretAccessKey = secretAccessKey; |
||||
|
SessionToken = sessionToken; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,193 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Amazon; |
||||
|
using Amazon.Runtime; |
||||
|
using Amazon.Runtime.CredentialManagement; |
||||
|
using Amazon.S3; |
||||
|
using Amazon.SecurityToken; |
||||
|
using Amazon.SecurityToken.Model; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Security.Encryption; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class DefaultAmazonS3ClientFactory : IAmazonS3ClientFactory, ITransientDependency |
||||
|
{ |
||||
|
protected IDistributedCache<AwsTemporaryCredentialsCacheItem> Cache { get; } |
||||
|
|
||||
|
protected IStringEncryptionService StringEncryptionService { get; } |
||||
|
|
||||
|
public DefaultAmazonS3ClientFactory( |
||||
|
IDistributedCache<AwsTemporaryCredentialsCacheItem> cache, |
||||
|
IStringEncryptionService stringEncryptionService) |
||||
|
{ |
||||
|
Cache = cache; |
||||
|
StringEncryptionService = stringEncryptionService; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<AmazonS3Client> GetAmazonS3Client( |
||||
|
AwsBlobProviderConfiguration configuration) |
||||
|
{ |
||||
|
var region = RegionEndpoint.GetBySystemName(configuration.Region); |
||||
|
|
||||
|
if (configuration.UseCredentials) |
||||
|
{ |
||||
|
var awsCredentials = GetAwsCredentials(configuration); |
||||
|
return awsCredentials == null |
||||
|
? new AmazonS3Client(region) |
||||
|
: new AmazonS3Client(GetAwsCredentials(configuration), region); |
||||
|
} |
||||
|
|
||||
|
if (configuration.UseTemporaryCredentials) |
||||
|
{ |
||||
|
return new AmazonS3Client(await GetTemporaryCredentialsAsync(configuration), region); |
||||
|
} |
||||
|
|
||||
|
if (configuration.UseTemporaryFederatedCredentials) |
||||
|
{ |
||||
|
return new AmazonS3Client(await GetTemporaryFederatedCredentialsAsync(configuration), |
||||
|
region); |
||||
|
} |
||||
|
|
||||
|
Check.NotNullOrWhiteSpace(configuration.AccessKeyId, nameof(configuration.AccessKeyId)); |
||||
|
Check.NotNullOrWhiteSpace(configuration.SecretAccessKey, nameof(configuration.SecretAccessKey)); |
||||
|
|
||||
|
return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, configuration.Region); |
||||
|
} |
||||
|
|
||||
|
protected virtual AWSCredentials GetAwsCredentials( |
||||
|
AwsBlobProviderConfiguration configuration) |
||||
|
{ |
||||
|
if (configuration.ProfileName.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var chain = new CredentialProfileStoreChain(configuration.ProfilesLocation); |
||||
|
|
||||
|
if (chain.TryGetAWSCredentials(configuration.ProfileName, out var awsCredentials)) |
||||
|
{ |
||||
|
return awsCredentials; |
||||
|
} |
||||
|
|
||||
|
throw new AmazonS3Exception("Not found aws credentials"); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<SessionAWSCredentials> GetTemporaryCredentialsAsync( |
||||
|
AwsBlobProviderConfiguration configuration) |
||||
|
{ |
||||
|
var temporaryCredentialsCache = await Cache.GetAsync(configuration.TemporaryCredentialsCacheKey); |
||||
|
|
||||
|
if (temporaryCredentialsCache == null) |
||||
|
{ |
||||
|
AmazonSecurityTokenServiceClient stsClient; |
||||
|
|
||||
|
if (!configuration.AccessKeyId.IsNullOrEmpty() && !configuration.SecretAccessKey.IsNullOrEmpty()) |
||||
|
{ |
||||
|
stsClient = new AmazonSecurityTokenServiceClient(configuration.AccessKeyId, |
||||
|
configuration.SecretAccessKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var awsCredentials = GetAwsCredentials(configuration); |
||||
|
stsClient = awsCredentials == null |
||||
|
? new AmazonSecurityTokenServiceClient() |
||||
|
: new AmazonSecurityTokenServiceClient(awsCredentials); |
||||
|
} |
||||
|
|
||||
|
using (stsClient) |
||||
|
{ |
||||
|
var getSessionTokenRequest = new GetSessionTokenRequest |
||||
|
{ |
||||
|
DurationSeconds = configuration.DurationSeconds |
||||
|
}; |
||||
|
|
||||
|
var sessionTokenResponse = |
||||
|
await stsClient.GetSessionTokenAsync(getSessionTokenRequest); |
||||
|
|
||||
|
var credentials = sessionTokenResponse.Credentials; |
||||
|
|
||||
|
temporaryCredentialsCache = |
||||
|
await SetTemporaryCredentialsCache(configuration, credentials); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var sessionCredentials = new SessionAWSCredentials( |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.AccessKeyId), |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.SecretAccessKey), |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.SessionToken)); |
||||
|
return sessionCredentials; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<SessionAWSCredentials> GetTemporaryFederatedCredentialsAsync( |
||||
|
AwsBlobProviderConfiguration configuration) |
||||
|
{ |
||||
|
Check.NotNullOrWhiteSpace(configuration.Name, nameof(configuration.Name)); |
||||
|
Check.NotNullOrWhiteSpace(configuration.Policy, nameof(configuration.Policy)); |
||||
|
|
||||
|
var temporaryCredentialsCache = await Cache.GetAsync(configuration.TemporaryCredentialsCacheKey); |
||||
|
|
||||
|
if (temporaryCredentialsCache == null) |
||||
|
{ |
||||
|
AmazonSecurityTokenServiceClient stsClient; |
||||
|
|
||||
|
if (!configuration.AccessKeyId.IsNullOrEmpty() && !configuration.SecretAccessKey.IsNullOrEmpty()) |
||||
|
{ |
||||
|
stsClient = new AmazonSecurityTokenServiceClient(configuration.AccessKeyId, |
||||
|
configuration.SecretAccessKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var awsCredentials = GetAwsCredentials(configuration); |
||||
|
stsClient = awsCredentials == null |
||||
|
? new AmazonSecurityTokenServiceClient() |
||||
|
: new AmazonSecurityTokenServiceClient(awsCredentials); |
||||
|
} |
||||
|
|
||||
|
using (stsClient) |
||||
|
{ |
||||
|
var federationTokenRequest = |
||||
|
new GetFederationTokenRequest |
||||
|
{ |
||||
|
DurationSeconds = configuration.DurationSeconds, |
||||
|
Name = configuration.Name, |
||||
|
Policy = configuration.Policy |
||||
|
}; |
||||
|
|
||||
|
var federationTokenResponse = |
||||
|
await stsClient.GetFederationTokenAsync(federationTokenRequest); |
||||
|
var credentials = federationTokenResponse.Credentials; |
||||
|
|
||||
|
temporaryCredentialsCache = |
||||
|
await SetTemporaryCredentialsCache(configuration, credentials); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var sessionCredentials = new SessionAWSCredentials( |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.AccessKeyId), |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.SecretAccessKey), |
||||
|
StringEncryptionService.Decrypt(temporaryCredentialsCache.SessionToken)); |
||||
|
return sessionCredentials; |
||||
|
} |
||||
|
|
||||
|
private async Task<AwsTemporaryCredentialsCacheItem> SetTemporaryCredentialsCache( |
||||
|
AwsBlobProviderConfiguration configuration, |
||||
|
Credentials credentials) |
||||
|
{ |
||||
|
var temporaryCredentialsCache = new AwsTemporaryCredentialsCacheItem( |
||||
|
StringEncryptionService.Encrypt(credentials.AccessKeyId), |
||||
|
StringEncryptionService.Encrypt(credentials.SecretAccessKey), |
||||
|
StringEncryptionService.Encrypt(credentials.SessionToken)); |
||||
|
|
||||
|
await Cache.SetAsync(configuration.TemporaryCredentialsCacheKey, temporaryCredentialsCache, |
||||
|
new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(configuration.DurationSeconds - 10) |
||||
|
}); |
||||
|
|
||||
|
return temporaryCredentialsCache; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class DefaultAwsBlobNameCalculator : IAwsBlobNameCalculator, ITransientDependency |
||||
|
{ |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
|
||||
|
public DefaultAwsBlobNameCalculator(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,10 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Amazon.S3; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public interface IAmazonS3ClientFactory |
||||
|
{ |
||||
|
Task<AmazonS3Client> GetAmazonS3Client(AwsBlobProviderConfiguration configuration); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public interface IAwsBlobNameCalculator |
||||
|
{ |
||||
|
string Calculate(BlobProviderArgs args); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.test.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
<UserSecretsId>9f0d2c00-80c1-435b-bfab-2c39c8249091</UserSecretsId> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.BlobStoring.Aws\Volo.Abp.BlobStoring.Aws.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
||||
|
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.BlobStoring.Tests\Volo.Abp.BlobStoring.Tests.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Volo.Abp.Testing; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class AbpBlobStoringAwsTestCommonBase : AbpIntegratedTest<AbpBlobStoringAwsTestCommonModule> |
||||
|
{ |
||||
|
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
||||
|
{ |
||||
|
options.UseAutofac(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class AbpBlobStoringAwsTestBase : AbpIntegratedTest<AbpBlobStoringAwsTestModule> |
||||
|
{ |
||||
|
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
||||
|
{ |
||||
|
options.UseAutofac(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Amazon.S3.Model; |
||||
|
using Amazon.S3.Util; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// This module will not try to connect to aws.
|
||||
|
/// </summary>
|
||||
|
[DependsOn( |
||||
|
typeof(AbpBlobStoringAwsModule), |
||||
|
typeof(AbpBlobStoringTestModule) |
||||
|
)] |
||||
|
public class AbpBlobStoringAwsTestCommonModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpBlobStoringAwsTestCommonModule) |
||||
|
)] |
||||
|
public class AbpBlobStoringAwsTestModule : AbpModule |
||||
|
{ |
||||
|
private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091"; |
||||
|
|
||||
|
private readonly string _randomContainerName = "abp-aws-test-container-" + Guid.NewGuid().ToString("N"); |
||||
|
|
||||
|
private AwsBlobProviderConfiguration _configuration; |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder => |
||||
|
{ |
||||
|
builder.AddUserSecrets(UserSecretsId); |
||||
|
})); |
||||
|
|
||||
|
var configuration = context.Services.GetConfiguration(); |
||||
|
var accessKeyId = configuration["Aws:AccessKeyId"]; |
||||
|
var secretAccessKey = configuration["Aws:SecretAccessKey"]; |
||||
|
var region = configuration["Aws:Region"]; |
||||
|
|
||||
|
Configure<AbpBlobStoringOptions>(options => |
||||
|
{ |
||||
|
options.Containers.ConfigureAll((containerName, containerConfiguration) => |
||||
|
{ |
||||
|
containerConfiguration.UseAws(aws => |
||||
|
{ |
||||
|
aws.AccessKeyId = accessKeyId; |
||||
|
aws.SecretAccessKey = secretAccessKey; |
||||
|
aws.Region = region; |
||||
|
aws.CreateContainerIfNotExists = true; |
||||
|
aws.ContainerName = _randomContainerName; |
||||
|
|
||||
|
_configuration = aws; |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void OnApplicationShutdown(ApplicationShutdownContext context) |
||||
|
{ |
||||
|
AsyncHelper.RunSync(() => DeleteBucketAsync(context)); |
||||
|
} |
||||
|
|
||||
|
private async Task DeleteBucketAsync(ApplicationShutdownContext context) |
||||
|
{ |
||||
|
var amazonS3Client = await context.ServiceProvider.GetService<IAmazonS3ClientFactory>() |
||||
|
.GetAmazonS3Client(_configuration); |
||||
|
|
||||
|
if (await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, _randomContainerName)) |
||||
|
{ |
||||
|
var blobs = await amazonS3Client.ListObjectsAsync(_randomContainerName); |
||||
|
|
||||
|
if (blobs.S3Objects.Any()) |
||||
|
{ |
||||
|
await amazonS3Client.DeleteObjectsAsync(new DeleteObjectsRequest |
||||
|
{ |
||||
|
BucketName = _randomContainerName, |
||||
|
Objects = blobs.S3Objects.Select(o => new KeyVersion {Key = o.Key}).ToList() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
await amazonS3Client.DeleteBucketAsync(_randomContainerName); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
/* |
||||
|
//Please set the correct connection string in secrets.json and continue the test.
|
||||
|
|
||||
|
public class AwsBlobContainer_Tests : BlobContainer_Tests<AbpBlobStoringAwsTestModule> |
||||
|
{ |
||||
|
public AwsBlobContainer_Tests() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
*/ |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
using System; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class AwsBlobNameCalculatorTests : AbpBlobStoringAwsTestCommonBase |
||||
|
{ |
||||
|
private readonly IAwsBlobNameCalculator _calculator; |
||||
|
private readonly ICurrentTenant _currentTenant; |
||||
|
|
||||
|
private const string AwsContainerName = "/"; |
||||
|
private const string AwsSeparator = "/"; |
||||
|
|
||||
|
public AwsBlobNameCalculatorTests() |
||||
|
{ |
||||
|
_calculator = GetRequiredService<IAwsBlobNameCalculator>(); |
||||
|
_currentTenant = GetRequiredService<ICurrentTenant>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Default_Settings() |
||||
|
{ |
||||
|
_calculator.Calculate( |
||||
|
GetArgs("my-container", "my-blob") |
||||
|
).ShouldBe($"host{AwsSeparator}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{AwsSeparator}{tenantId:D}{AwsSeparator}my-blob"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static BlobProviderArgs GetArgs( |
||||
|
string containerName, |
||||
|
string blobName) |
||||
|
{ |
||||
|
return new BlobProviderGetArgs( |
||||
|
containerName, |
||||
|
new BlobContainerConfiguration().UseAws(x => |
||||
|
{ |
||||
|
x.ContainerName = containerName; |
||||
|
}), |
||||
|
blobName |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.BlobStoring.Aws |
||||
|
{ |
||||
|
public class DefaultAwsBlobNamingNormalizerProviderTests : AbpBlobStoringAwsTestCommonBase |
||||
|
{ |
||||
|
private readonly IBlobNamingNormalizer _blobNamingNormalizer; |
||||
|
|
||||
|
public DefaultAwsBlobNamingNormalizerProviderTests() |
||||
|
{ |
||||
|
_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() |
||||
|
{ |
||||
|
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_Dash() |
||||
|
{ |
||||
|
var filename = "-this--is----my-container----name-"; |
||||
|
filename = _blobNamingNormalizer.NormalizeContainerName(filename); |
||||
|
filename.ShouldBe("this-is-my-container-name"); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue