27 changed files with 1140 additions and 28 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>net8.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.OssManagement.Minio</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.OssManagement.Minio</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.BlobStoring.Minio" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.OssManagement.Domain\LINGYUN.Abp.OssManagement.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,23 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using Volo.Abp.BlobStoring.Minio; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpBlobStoringMinioModule), |
|||
typeof(AbpOssManagementDomainModule))] |
|||
public class AbpOssManagementMinioModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddTransient<IOssContainerFactory, MinioOssContainerFactory>(); |
|||
|
|||
context.Services.AddTransient<IOssObjectExpireor>(provider => |
|||
provider |
|||
.GetRequiredService<IOssContainerFactory>() |
|||
.Create() |
|||
.As<MinioOssContainer>()); |
|||
} |
|||
} |
|||
@ -0,0 +1,558 @@ |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.Logging; |
|||
using Minio; |
|||
using Minio.DataModel.Args; |
|||
using Minio.DataModel.ILM; |
|||
using Minio.Exceptions; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Linq.Dynamic.Core; |
|||
using System.Reactive.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.BlobStoring.Minio; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
|
|||
/// <summary>
|
|||
/// Oss容器的Minio实现
|
|||
/// </summary>
|
|||
public class MinioOssContainer : IOssContainer, IOssObjectExpireor |
|||
{ |
|||
protected IMinioBlobNameCalculator MinioBlobNameCalculator { get; } |
|||
protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; } |
|||
protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } |
|||
|
|||
protected IClock Clock { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected ILogger<MinioOssContainer> Logger { get; } |
|||
|
|||
public MinioOssContainer( |
|||
IClock clock, |
|||
ICurrentTenant currentTenant, |
|||
ILogger<MinioOssContainer> logger, |
|||
IMinioBlobNameCalculator minioBlobNameCalculator, |
|||
IBlobNormalizeNamingService blobNormalizeNamingService, |
|||
IBlobContainerConfigurationProvider configurationProvider) |
|||
{ |
|||
Clock = clock; |
|||
Logger = logger; |
|||
CurrentTenant = currentTenant; |
|||
MinioBlobNameCalculator = minioBlobNameCalculator; |
|||
BlobNormalizeNamingService = blobNormalizeNamingService; |
|||
ConfigurationProvider = configurationProvider; |
|||
} |
|||
|
|||
public async virtual Task BulkDeleteObjectsAsync(BulkDeleteObjectRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.Bucket); |
|||
|
|||
var prefixPath = GetPrefixPath(); |
|||
var path = GetBlobPath(prefixPath, request.Path); |
|||
|
|||
var args = new RemoveObjectsArgs() |
|||
.WithBucket(bucket) |
|||
.WithObjects(request.Objects.Select((x) => path + x.RemovePreFix("/")).ToList()); |
|||
|
|||
var response = await client.RemoveObjectsAsync(args); |
|||
|
|||
var tcs = new TaskCompletionSource<bool>(); |
|||
|
|||
using var _ = response.Subscribe( |
|||
onNext: (error) => |
|||
{ |
|||
Logger.LogWarning("Batch deletion of objects failed, error details {code}: {message}", error.Code, error.Message); |
|||
}, |
|||
onError: tcs.SetException, |
|||
onCompleted: () => tcs.SetResult(true)); |
|||
|
|||
await tcs.Task; |
|||
} |
|||
|
|||
public async virtual Task<OssContainer> CreateAsync(string name) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(name); |
|||
|
|||
if (await BucketExists(client, bucket)) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ContainerAlreadyExists); |
|||
} |
|||
|
|||
await client.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucket)); |
|||
|
|||
return new OssContainer( |
|||
name, |
|||
Clock.Now, |
|||
0L, |
|||
Clock.Now, |
|||
new Dictionary<string, string>()); |
|||
} |
|||
|
|||
public async virtual Task<OssObject> CreateObjectAsync(CreateOssObjectRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.Bucket); |
|||
var prefixPath = GetPrefixPath(); |
|||
var objectPath = GetBlobPath(prefixPath, request.Path); |
|||
var objectName = objectPath.IsNullOrWhiteSpace() |
|||
? request.Object |
|||
: objectPath + request.Object; |
|||
|
|||
if (!request.Overwrite && await ObjectExists(client, bucket, objectName)) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ObjectAlreadyExists); |
|||
} |
|||
|
|||
// 没有bucket则创建
|
|||
if (!await BucketExists(client, bucket)) |
|||
{ |
|||
var configuration = GetMinioConfiguration(); |
|||
if (!configuration.CreateBucketIfNotExists) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ContainerNotFound); |
|||
} |
|||
await client.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucket)); |
|||
} |
|||
if (request.Content.IsNullOrEmpty()) |
|||
{ |
|||
var emptyContent = "This is an OSS object that simulates a directory.".GetBytes(); |
|||
request.SetContent(new MemoryStream(emptyContent)); |
|||
} |
|||
var putResponse = await client.PutObjectAsync(new PutObjectArgs() |
|||
.WithBucket(bucket) |
|||
.WithObject(objectName) |
|||
.WithStreamData(request.Content) |
|||
.WithObjectSize(request.Content.Length)); |
|||
|
|||
if (request.ExpirationTime.HasValue) |
|||
{ |
|||
var lifecycleRule = new LifecycleRule |
|||
{ |
|||
Status = "Enabled", |
|||
ID = putResponse.Etag, |
|||
Expiration = new Expiration(Clock.Now.Add(request.ExpirationTime.Value)) |
|||
}; |
|||
var lifecycleConfiguration = new LifecycleConfiguration(); |
|||
lifecycleConfiguration.Rules.Add(lifecycleRule); |
|||
|
|||
var lifecycleArgs = new SetBucketLifecycleArgs() |
|||
.WithBucket(bucket) |
|||
.WithLifecycleConfiguration(lifecycleConfiguration); |
|||
|
|||
await client.SetBucketLifecycleAsync(lifecycleArgs); |
|||
} |
|||
|
|||
var ossObject = new OssObject( |
|||
!objectPath.IsNullOrWhiteSpace() |
|||
? objectName.Replace(objectPath, "") |
|||
: objectName, |
|||
objectPath.Replace(prefixPath, ""), |
|||
putResponse.Etag, |
|||
Clock.Now, |
|||
putResponse.Size, |
|||
Clock.Now, |
|||
new Dictionary<string, string>(), |
|||
objectName.EndsWith('/')) |
|||
{ |
|||
FullName = objectName.Replace(prefixPath, "") |
|||
}; |
|||
|
|||
if (!Equals(request.Content, Stream.Null)) |
|||
{ |
|||
request.Content.Seek(0, SeekOrigin.Begin); |
|||
ossObject.SetContent(request.Content); |
|||
} |
|||
|
|||
return ossObject; |
|||
} |
|||
|
|||
public async virtual Task DeleteAsync(string name) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
var bucket = GetBucket(name); |
|||
|
|||
if (!await BucketExists(client, bucket)) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ContainerNotFound); |
|||
} |
|||
|
|||
// 非空目录无法删除
|
|||
var tcs = new TaskCompletionSource<bool>(); |
|||
var listObjectObs = client.ListObjectsAsync( |
|||
new ListObjectsArgs() |
|||
.WithBucket(bucket)); |
|||
|
|||
var listObjects = new List<string>(); |
|||
using var _ = listObjectObs.Subscribe( |
|||
(item) => |
|||
{ |
|||
listObjects.Add(item.Key); |
|||
tcs.TrySetResult(true); |
|||
}, |
|||
(ex) => tcs.TrySetException(ex), |
|||
() => tcs.TrySetResult(true)); |
|||
|
|||
await tcs.Task; |
|||
|
|||
if (listObjects.Count > 0) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ContainerDeleteWithNotEmpty); |
|||
} |
|||
|
|||
var deleteBucketArgs = new RemoveBucketArgs() |
|||
.WithBucket(bucket); |
|||
|
|||
await client.RemoveBucketAsync(deleteBucketArgs); |
|||
} |
|||
|
|||
public async virtual Task DeleteObjectAsync(GetOssObjectRequest request) |
|||
{ |
|||
if (request.Object.EndsWith('/')) |
|||
{ |
|||
// Minio系统设计并不支持目录的形式
|
|||
// 如果是目录的形式,那必定有文件存在,抛出目录不为空即可
|
|||
throw new BusinessException(code: OssManagementErrorCodes.ObjectDeleteWithNotEmpty); |
|||
} |
|||
|
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.Bucket); |
|||
|
|||
var prefixPath = GetPrefixPath(); |
|||
var objectPath = GetBlobPath(prefixPath, request.Path); |
|||
var objectName = objectPath.IsNullOrWhiteSpace() |
|||
? request.Object |
|||
: objectPath + request.Object; |
|||
|
|||
if (await BucketExists(client, bucket) && |
|||
await ObjectExists(client, bucket, objectName)) |
|||
{ |
|||
var removeObjectArgs = new RemoveObjectArgs() |
|||
.WithBucket(bucket) |
|||
.WithObject(objectName); |
|||
|
|||
await client.RemoveObjectAsync(removeObjectArgs); |
|||
} |
|||
} |
|||
|
|||
public async virtual Task<bool> ExistsAsync(string name) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(name); |
|||
|
|||
return await BucketExists(client, bucket); |
|||
} |
|||
|
|||
public async virtual Task ExpireAsync(ExprieOssObjectRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.Bucket); |
|||
|
|||
var bucketListResult = await client.ListBucketsAsync(); |
|||
var expiredBuckets = bucketListResult.Buckets.Take(request.Batch); |
|||
|
|||
foreach (var expiredBucket in expiredBuckets) |
|||
{ |
|||
var listObjectArgs = new ListObjectsArgs() |
|||
.WithBucket(expiredBucket.Name); |
|||
|
|||
var expiredObjectItem = client.ListObjectsAsync(listObjectArgs); |
|||
|
|||
var tcs = new TaskCompletionSource<bool>(); |
|||
using var _ = expiredObjectItem.Subscribe( |
|||
onNext: (item) => |
|||
{ |
|||
var lifecycleRule = new LifecycleRule |
|||
{ |
|||
Status = "Enabled", |
|||
ID = item.Key, |
|||
Expiration = new Expiration(Clock.Normalize(request.ExpirationTime.DateTime)) |
|||
}; |
|||
var lifecycleConfiguration = new LifecycleConfiguration(); |
|||
lifecycleConfiguration.Rules.Add(lifecycleRule); |
|||
|
|||
var lifecycleArgs = new SetBucketLifecycleArgs() |
|||
.WithBucket(bucket) |
|||
.WithLifecycleConfiguration(lifecycleConfiguration); |
|||
|
|||
var _ = client.SetBucketLifecycleAsync(lifecycleArgs); |
|||
}, |
|||
onError: tcs.SetException, |
|||
onCompleted: () => tcs.SetResult(true) |
|||
); |
|||
|
|||
await tcs.Task; |
|||
} |
|||
} |
|||
|
|||
public async virtual Task<OssContainer> GetAsync(string name) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(name); |
|||
|
|||
var bucketListResult = await client.ListBucketsAsync(); |
|||
|
|||
var bucketInfo = bucketListResult.Buckets.FirstOrDefault((x) => x.Name == bucket); |
|||
if (bucketInfo == null) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ContainerNotFound); |
|||
} |
|||
|
|||
return new OssContainer( |
|||
bucketInfo.Name, |
|||
bucketInfo.CreationDateDateTime, |
|||
0L, |
|||
bucketInfo.CreationDateDateTime, |
|||
new Dictionary<string, string>()); |
|||
} |
|||
|
|||
public async virtual Task<GetOssContainersResponse> GetListAsync(GetOssContainersRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucketListResult = await client.ListBucketsAsync(); |
|||
|
|||
var totalCount = bucketListResult.Buckets.Count; |
|||
|
|||
var resultObjects = bucketListResult.Buckets |
|||
.AsQueryable() |
|||
.OrderBy(x => x.Name) |
|||
.PageBy(request.Current, request.MaxKeys ?? 10) |
|||
.Select(x => new OssContainer( |
|||
x.Name, |
|||
x.CreationDateDateTime, |
|||
0L, |
|||
null, |
|||
new Dictionary<string, string>())) |
|||
.ToList(); |
|||
|
|||
return new GetOssContainersResponse( |
|||
request.Prefix, |
|||
request.Marker, |
|||
null, |
|||
totalCount, |
|||
resultObjects); |
|||
} |
|||
|
|||
public async virtual Task<OssObject> GetObjectAsync(GetOssObjectRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.Bucket); |
|||
|
|||
var prefixPath = GetPrefixPath(); |
|||
var objectPath = GetBlobPath(prefixPath, request.Path); |
|||
var objectName = objectPath.IsNullOrWhiteSpace() |
|||
? request.Object |
|||
: objectPath.EnsureEndsWith('/') + request.Object; |
|||
|
|||
if (!await ObjectExists(client, bucket, objectName)) |
|||
{ |
|||
throw new BusinessException(code: OssManagementErrorCodes.ObjectNotFound); |
|||
} |
|||
|
|||
var memoryStream = new MemoryStream(); |
|||
var getObjectArgs = new GetObjectArgs() |
|||
.WithBucket(bucket) |
|||
.WithObject(objectName) |
|||
.WithCallbackStream((stream) => |
|||
{ |
|||
if (stream != null) |
|||
{ |
|||
stream.CopyTo(memoryStream); |
|||
memoryStream.Seek(0, SeekOrigin.Begin); |
|||
} |
|||
else |
|||
{ |
|||
memoryStream = null; |
|||
} |
|||
}); |
|||
var getObjectResult = await client.GetObjectAsync(getObjectArgs); |
|||
|
|||
var ossObject = new OssObject( |
|||
!objectPath.IsNullOrWhiteSpace() |
|||
? getObjectResult.ObjectName.Replace(objectPath, "") |
|||
: getObjectResult.ObjectName, |
|||
request.Path, |
|||
getObjectResult.ETag, |
|||
getObjectResult.LastModified, |
|||
memoryStream.Length, |
|||
getObjectResult.LastModified, |
|||
getObjectResult.MetaData, |
|||
getObjectResult.ObjectName.EndsWith("/")) |
|||
{ |
|||
FullName = getObjectResult.ObjectName.Replace(prefixPath, "") |
|||
}; |
|||
|
|||
if (memoryStream.Length > 0) |
|||
{ |
|||
ossObject.SetContent(memoryStream); |
|||
} |
|||
|
|||
if (!request.Process.IsNullOrWhiteSpace()) |
|||
{ |
|||
// TODO: 文件流处理
|
|||
} |
|||
|
|||
return ossObject; |
|||
} |
|||
|
|||
public async virtual Task<GetOssObjectsResponse> GetObjectsAsync(GetOssObjectsRequest request) |
|||
{ |
|||
var client = GetMinioClient(); |
|||
|
|||
var bucket = GetBucket(request.BucketName); |
|||
|
|||
var prefixPath = GetPrefixPath(); |
|||
var objectPath = GetBlobPath(prefixPath, request.Prefix); |
|||
var marker = !objectPath.IsNullOrWhiteSpace() && !request.Marker.IsNullOrWhiteSpace() |
|||
? request.Marker.Replace(objectPath, "") |
|||
: request.Marker; |
|||
|
|||
var listObjectArgs = new ListObjectsArgs() |
|||
.WithBucket(bucket) |
|||
.WithPrefix(objectPath); |
|||
|
|||
var tcs = new TaskCompletionSource<bool>(); |
|||
|
|||
var listObjectResult = client.ListObjectsAsync(listObjectArgs); |
|||
|
|||
var resultObjects = new List<OssObject>(); |
|||
|
|||
using var _ = listObjectResult.Subscribe( |
|||
onNext: (item) => |
|||
{ |
|||
resultObjects.Add(new OssObject( |
|||
!objectPath.IsNullOrWhiteSpace() |
|||
? item.Key.Replace(objectPath, "") |
|||
: item.Key, |
|||
request.Prefix, |
|||
item.ETag, |
|||
item.LastModifiedDateTime, |
|||
item.Size.To<long>(), |
|||
item.LastModifiedDateTime, |
|||
new Dictionary<string, string>(), |
|||
item.IsDir)); |
|||
}, |
|||
onError: (ex) => |
|||
{ |
|||
tcs.TrySetException(ex); |
|||
}, |
|||
onCompleted: () => |
|||
{ |
|||
tcs.SetResult(true); |
|||
} |
|||
); |
|||
|
|||
await tcs.Task; |
|||
|
|||
var totalCount = resultObjects.Count; |
|||
resultObjects = resultObjects |
|||
.AsQueryable() |
|||
.OrderBy(x => x.Name) |
|||
.PageBy(request.Current, request.MaxKeys ?? 10) |
|||
.ToList(); |
|||
|
|||
return new GetOssObjectsResponse( |
|||
bucket, |
|||
request.Prefix, |
|||
request.Marker, |
|||
marker, |
|||
"/", |
|||
totalCount, |
|||
resultObjects); |
|||
} |
|||
|
|||
protected virtual IMinioClient GetMinioClient() |
|||
{ |
|||
var configuration = GetMinioConfiguration(); |
|||
|
|||
var client = new MinioClient() |
|||
.WithEndpoint(configuration.EndPoint) |
|||
.WithCredentials(configuration.AccessKey, configuration.SecretKey); |
|||
|
|||
if (configuration.WithSSL) |
|||
{ |
|||
client.WithSSL(); |
|||
} |
|||
|
|||
return client.Build(); |
|||
} |
|||
|
|||
protected async virtual Task<bool> BucketExists(IMinioClient client, string bucket) |
|||
{ |
|||
var args = new BucketExistsArgs().WithBucket(bucket); |
|||
|
|||
return await client.BucketExistsAsync(args); |
|||
} |
|||
|
|||
protected async virtual Task<bool> ObjectExists(IMinioClient client, string bucket, string @object) |
|||
{ |
|||
if (await client.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucket))) |
|||
{ |
|||
try |
|||
{ |
|||
await client.StatObjectAsync(new StatObjectArgs().WithBucket(bucket).WithObject(@object)); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
if (e is ObjectNotFoundException) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
protected virtual MinioBlobProviderConfiguration GetMinioConfiguration() |
|||
{ |
|||
var configuration = ConfigurationProvider.Get<AbpOssManagementContainer>(); |
|||
|
|||
return configuration.GetMinioConfiguration(); |
|||
} |
|||
|
|||
protected virtual string GetBucket(string bucket) |
|||
{ |
|||
var configuration = ConfigurationProvider.Get<AbpOssManagementContainer>(); |
|||
var minioConfiguration = configuration.GetMinioConfiguration(); |
|||
|
|||
return bucket.IsNullOrWhiteSpace() |
|||
? BlobNormalizeNamingService.NormalizeContainerName(configuration, minioConfiguration.BucketName!) |
|||
: BlobNormalizeNamingService.NormalizeContainerName(configuration, bucket); |
|||
} |
|||
|
|||
protected virtual string GetPrefixPath() |
|||
{ |
|||
return CurrentTenant.Id == null |
|||
? $"host/" |
|||
: $"tenants/{CurrentTenant.Id.Value.ToString("D")}/"; |
|||
} |
|||
|
|||
protected virtual string GetBlobPath(string basePath, string path) |
|||
{ |
|||
var resultPath = $"{basePath}{(
|
|||
path.IsNullOrWhiteSpace() ? "" : |
|||
path.Replace("./", "").RemovePreFix("/"))}";
|
|||
|
|||
return resultPath.Replace("//", ""); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.BlobStoring.Minio; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
public class MinioOssContainerFactory : IOssContainerFactory |
|||
{ |
|||
protected IMinioBlobNameCalculator MinioBlobNameCalculator { get; } |
|||
protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; } |
|||
protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } |
|||
|
|||
protected IClock Clock { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected ILogger<MinioOssContainer> Logger { get; } |
|||
|
|||
public MinioOssContainerFactory( |
|||
IClock clock, |
|||
ICurrentTenant currentTenant, |
|||
ILogger<MinioOssContainer> logger, |
|||
IMinioBlobNameCalculator minioBlobNameCalculator, |
|||
IBlobNormalizeNamingService blobNormalizeNamingService, |
|||
IBlobContainerConfigurationProvider configurationProvider) |
|||
{ |
|||
Clock = clock; |
|||
Logger = logger; |
|||
CurrentTenant = currentTenant; |
|||
MinioBlobNameCalculator = minioBlobNameCalculator; |
|||
BlobNormalizeNamingService = blobNormalizeNamingService; |
|||
ConfigurationProvider = configurationProvider; |
|||
} |
|||
|
|||
public IOssContainer Create() |
|||
{ |
|||
return new MinioOssContainer( |
|||
Clock, |
|||
CurrentTenant, |
|||
Logger, |
|||
MinioBlobNameCalculator, |
|||
BlobNormalizeNamingService, |
|||
ConfigurationProvider); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
# LINGYUN.Abp.OssManagement.Minio |
|||
|
|||
Oss容器管理接口的Minio实现 |
|||
|
|||
## 配置使用 |
|||
|
|||
模块按需引用 |
|||
|
|||
相关配置项请参考 [BlobStoring Minio](https://abp.io/docs/latest/framework/infrastructure/blob-storing/minio) |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpOssManagementMinioModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
|
|||
|
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net8.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="xunit" /> |
|||
<PackageReference Include="xunit.runner.visualstudio"> |
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
|||
<PrivateAssets>all</PrivateAssets> |
|||
</PackageReference> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\modules\oss-management\LINGYUN.Abp.OssManagement.Domain\LINGYUN.Abp.OssManagement.Domain.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,6 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement; |
|||
public abstract class AbpOssManagementDomainTestBase : AbpTestsBase<AbpOssManagementDomainTestsModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpOssManagementDomainModule), |
|||
typeof(AbpTestsBaseModule))] |
|||
public class AbpOssManagementDomainTestsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
using Shouldly; |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
using static System.Runtime.InteropServices.JavaScript.JSType; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement; |
|||
public abstract class OssContainer_Tests<TStartupModule> : AbpIntegratedTest<TStartupModule> |
|||
where TStartupModule : IAbpModule |
|||
{ |
|||
private readonly ICurrentTenant _currentTenant; |
|||
private readonly IOssContainerFactory _ossContainerFactory; |
|||
|
|||
protected OssContainer_Tests() |
|||
{ |
|||
_currentTenant = GetRequiredService<ICurrentTenant>(); |
|||
_ossContainerFactory = GetRequiredService<IOssContainerFactory>(); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket")] |
|||
public async virtual Task Should_Creat_And_Get_Bucket(string bucket) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
var ossContainer = await container.CreateAsync(bucket); |
|||
|
|||
ossContainer.ShouldNotBeNull(); |
|||
ossContainer.Name.ShouldBe(bucket); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket")] |
|||
public async virtual Task Should_Get_Exists_Bucket(string bucket) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
await container.CreateAsync(bucket); |
|||
|
|||
var getContainer = await container.GetAsync(bucket); |
|||
|
|||
getContainer.ShouldNotBeNull(); |
|||
getContainer.Name.ShouldBe(bucket); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket")] |
|||
public async virtual Task Should_Delete_Bucket(string bucket) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
await container.CreateAsync(bucket); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
|
|||
var getNotFoundException = await Assert.ThrowsAsync<BusinessException>(async () => |
|||
await container.GetAsync(bucket) |
|||
); |
|||
|
|||
getNotFoundException.Code.ShouldBe(OssManagementErrorCodes.ContainerNotFound); |
|||
|
|||
var deleteNotFoundException = await Assert.ThrowsAsync<BusinessException>(async () => |
|||
await container.DeleteAsync(bucket) |
|||
); |
|||
|
|||
deleteNotFoundException.Code.ShouldBe(OssManagementErrorCodes.ContainerNotFound); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket", "test-object-1")] |
|||
[InlineData("test-bucket", "test-folder/test-object-1")] |
|||
public async virtual Task Should_Save_And_Get_Object(string bucket, string @object) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
if (!await container.ExistsAsync(bucket)) |
|||
{ |
|||
await container.CreateAsync(bucket); |
|||
} |
|||
|
|||
var testContent = "test content".GetBytes(); |
|||
using var stream = new MemoryStream(testContent); |
|||
|
|||
var createObject = await container.CreateObjectAsync( |
|||
new CreateOssObjectRequest( |
|||
bucket, |
|||
@object, |
|||
stream)); |
|||
|
|||
var getOssObject = await container.GetObjectAsync( |
|||
new GetOssObjectRequest( |
|||
bucket, |
|||
@object)); |
|||
|
|||
var result = await getOssObject.Content.GetAllBytesAsync(); |
|||
result.SequenceEqual(testContent).ShouldBeTrue(); |
|||
|
|||
await container.DeleteObjectAsync( |
|||
new GetOssObjectRequest( |
|||
bucket, |
|||
@object)); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket", "test-object-1")] |
|||
[InlineData("test-bucket", "test-folder/test-object-1")] |
|||
public async virtual Task Should_Delete_Object(string bucket, string @object) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
if (!await container.ExistsAsync(bucket)) |
|||
{ |
|||
await container.CreateAsync(bucket); |
|||
} |
|||
|
|||
var testContent = "test content".GetBytes(); |
|||
using var stream = new MemoryStream(testContent); |
|||
|
|||
await container.CreateObjectAsync( |
|||
new CreateOssObjectRequest( |
|||
bucket, |
|||
@object, |
|||
stream)); |
|||
|
|||
var getOssObjectRequest = new GetOssObjectRequest( |
|||
bucket, |
|||
@object); |
|||
await container.DeleteObjectAsync(getOssObjectRequest); |
|||
|
|||
var getNotFoundException = await Assert.ThrowsAsync<BusinessException>(async () => |
|||
await container.GetObjectAsync(getOssObjectRequest) |
|||
); |
|||
|
|||
getNotFoundException.Code.ShouldBe(OssManagementErrorCodes.ObjectNotFound); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket")] |
|||
public async virtual Task Should_Bulk_Delete_Object(string bucket) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
if (!await container.ExistsAsync(bucket)) |
|||
{ |
|||
await container.CreateAsync(bucket); |
|||
} |
|||
|
|||
string[] testObjects = ["test-object-1", "test-object-2", "test-object-3"]; |
|||
var testContent = "test content".GetBytes(); |
|||
using var stream = new MemoryStream(testContent); |
|||
|
|||
foreach (var testObject in testObjects) |
|||
{ |
|||
await container.CreateObjectAsync( |
|||
new CreateOssObjectRequest( |
|||
bucket, |
|||
testObject, |
|||
stream)); |
|||
} |
|||
|
|||
await container.BulkDeleteObjectsAsync( |
|||
new BulkDeleteObjectRequest(bucket, testObjects, "/")); |
|||
|
|||
var getNotFoundException = await Assert.ThrowsAsync<BusinessException>(async () => |
|||
await container.GetObjectAsync( |
|||
new GetOssObjectRequest( |
|||
bucket, |
|||
testObjects[0]) |
|||
) |
|||
); |
|||
|
|||
getNotFoundException.Code.ShouldBe(OssManagementErrorCodes.ObjectNotFound); |
|||
|
|||
await container.BulkDeleteObjectsAsync( |
|||
new BulkDeleteObjectRequest( |
|||
bucket, testObjects, "/")); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("test-bucket")] |
|||
public async virtual Task Should_List_Object(string bucket) |
|||
{ |
|||
var container = _ossContainerFactory.Create(); |
|||
|
|||
if (!await container.ExistsAsync(bucket)) |
|||
{ |
|||
await container.CreateAsync(bucket); |
|||
} |
|||
|
|||
string[] testObjects = [ |
|||
"test-object-1", |
|||
"test-object-2", |
|||
"test-object-3", |
|||
"test-object-4", |
|||
"test-object-5"]; |
|||
var testContent = "test content".GetBytes(); |
|||
using var stream = new MemoryStream(testContent); |
|||
|
|||
foreach (var testObject in testObjects) |
|||
{ |
|||
await container.CreateObjectAsync( |
|||
new CreateOssObjectRequest( |
|||
bucket, |
|||
testObject, |
|||
stream)); |
|||
} |
|||
|
|||
var result1 = await container.GetObjectsAsync( |
|||
new GetOssObjectsRequest( |
|||
bucket, |
|||
"/", |
|||
maxKeys: 1)); |
|||
|
|||
result1.Objects.Count.ShouldBe(1); |
|||
result1.Objects[0].Name.ShouldBe(testObjects[0]); |
|||
|
|||
var result2 = await container.GetObjectsAsync( |
|||
new GetOssObjectsRequest( |
|||
bucket, |
|||
"/", |
|||
current: 2, |
|||
maxKeys: 2)); |
|||
result2.Objects.Count.ShouldBe(2); |
|||
result2.Objects[1].Name.ShouldBe(testObjects[3]); |
|||
|
|||
await container.BulkDeleteObjectsAsync( |
|||
new BulkDeleteObjectRequest( |
|||
bucket, testObjects, "/")); |
|||
|
|||
await container.DeleteAsync(bucket); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net8.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<IsPackable>false</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="xunit" /> |
|||
<PackageReference Include="xunit.runner.visualstudio"> |
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
|||
<PrivateAssets>all</PrivateAssets> |
|||
</PackageReference> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\modules\oss-management\LINGYUN.Abp.OssManagement.Minio\LINGYUN.Abp.OssManagement.Minio.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.OssManagement.Domain.Tests\LINGYUN.Abp.OssManagement.Domain.Tests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,6 @@ |
|||
using LINGYUN.Abp.Tests; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
public abstract class AbpOssManagementMinioTestBase : AbpTestsBase<AbpOssManagementMinioTestsModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.BlobStoring.Minio; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpOssManagementMinioModule), |
|||
typeof(AbpOssManagementDomainTestsModule))] |
|||
public class AbpOssManagementMinioTestsModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configurationOptions = new AbpConfigurationBuilderOptions |
|||
{ |
|||
BasePath = @"D:\Projects\Development\Abp\BlobStoring\Minio", |
|||
EnvironmentName = "Test" |
|||
}; |
|||
|
|||
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions)); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureAll((containerName, containerConfiguration) => |
|||
{ |
|||
containerConfiguration.UseMinio(minio => |
|||
{ |
|||
minio.BucketName = configuration[MinioBlobProviderConfigurationNames.BucketName]; |
|||
minio.AccessKey = configuration[MinioBlobProviderConfigurationNames.AccessKey]; |
|||
minio.SecretKey = configuration[MinioBlobProviderConfigurationNames.SecretKey]; |
|||
minio.EndPoint = configuration[MinioBlobProviderConfigurationNames.EndPoint]; |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
namespace LINGYUN.Abp.OssManagement.Minio; |
|||
public class MinioOssContainer_Tests : OssContainer_Tests<AbpOssManagementMinioTestsModule> |
|||
{ |
|||
} |
|||
Loading…
Reference in new issue