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