163 lines
5.4 KiB
C#
163 lines
5.4 KiB
C#
using Aliyun.OSS;
|
|
using Microsoft.Extensions.Options;
|
|
using TakeoutSaaS.Module.Storage.Abstractions;
|
|
using TakeoutSaaS.Module.Storage.Models;
|
|
using TakeoutSaaS.Module.Storage.Options;
|
|
|
|
namespace TakeoutSaaS.Module.Storage.Providers;
|
|
|
|
/// <summary>
|
|
/// 阿里云 OSS 存储提供商实现。
|
|
/// </summary>
|
|
public sealed class AliyunOssStorageProvider(IOptionsMonitor<StorageOptions> optionsMonitor) : IObjectStorageProvider, IDisposable
|
|
{
|
|
private OssClient? _client;
|
|
private bool _disposed;
|
|
|
|
private StorageOptions CurrentOptions => optionsMonitor.CurrentValue;
|
|
|
|
/// <inheritdoc />
|
|
public StorageProviderKind Kind => StorageProviderKind.AliyunOss;
|
|
|
|
/// <inheritdoc />
|
|
public async Task<StorageUploadResult> UploadAsync(StorageUploadRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 准备元数据
|
|
var options = CurrentOptions;
|
|
var metadata = new ObjectMetadata
|
|
{
|
|
ContentLength = request.ContentLength,
|
|
ContentType = request.ContentType
|
|
};
|
|
|
|
foreach (var kv in request.Metadata)
|
|
{
|
|
metadata.UserMetadata[kv.Key] = kv.Value;
|
|
}
|
|
|
|
// Aliyun OSS SDK 支持异步方法,如未支持将同步封装为任务。
|
|
// 2. 上传对象
|
|
await PutObjectAsync(options.AliyunOss.Bucket, request.ObjectKey, request.Content, metadata, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// 3. 生成签名或公有 URL
|
|
var signedUrl = request.GenerateSignedUrl
|
|
? await GenerateDownloadUrlAsync(request.ObjectKey, request.SignedUrlExpires, cancellationToken).ConfigureAwait(false)
|
|
: null;
|
|
|
|
// 4. 返回上传结果
|
|
return new StorageUploadResult
|
|
{
|
|
ObjectKey = request.ObjectKey,
|
|
Url = signedUrl ?? BuildPublicUrl(request.ObjectKey),
|
|
SignedUrl = signedUrl,
|
|
FileSize = request.ContentLength,
|
|
ContentType = request.ContentType
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<StorageDirectUploadResult> CreateDirectUploadAsync(StorageDirectUploadRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 计算过期时间并生成直传/下载链接
|
|
var expiresAt = DateTimeOffset.UtcNow.Add(request.Expires);
|
|
var uploadUrl = GeneratePresignedUrl(request.ObjectKey, request.Expires, SignHttpMethod.Put, request.ContentType);
|
|
var downloadUrl = GeneratePresignedUrl(request.ObjectKey, request.Expires, SignHttpMethod.Get, null);
|
|
|
|
// 2. 返回直传参数
|
|
var result = new StorageDirectUploadResult
|
|
{
|
|
UploadUrl = uploadUrl,
|
|
FormFields = new Dictionary<string, string>(),
|
|
ExpiresAt = expiresAt,
|
|
ObjectKey = request.ObjectKey,
|
|
SignedDownloadUrl = downloadUrl
|
|
};
|
|
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GenerateDownloadUrlAsync(string objectKey, TimeSpan expires, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 生成预签名下载 URL
|
|
var url = GeneratePresignedUrl(objectKey, expires, SignHttpMethod.Get, null);
|
|
return Task.FromResult(url);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string BuildPublicUrl(string objectKey)
|
|
{
|
|
var cdn = CurrentOptions.AliyunOss.CdnBaseUrl ?? CurrentOptions.CdnBaseUrl;
|
|
if (!string.IsNullOrWhiteSpace(cdn))
|
|
{
|
|
return $"{cdn!.TrimEnd('/')}/{objectKey}";
|
|
}
|
|
|
|
var endpoint = CurrentOptions.AliyunOss.Endpoint.TrimEnd('/');
|
|
var scheme = CurrentOptions.AliyunOss.UseHttps ? "https" : "http";
|
|
// Endpoint 可能已包含协议,若没有则补充。
|
|
if (!endpoint.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
endpoint = $"{scheme}://{endpoint}";
|
|
}
|
|
|
|
return $"{endpoint}/{CurrentOptions.AliyunOss.Bucket}/{objectKey}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 上传对象到 OSS。
|
|
/// </summary>
|
|
private async Task PutObjectAsync(string bucket, string key, Stream content, ObjectMetadata metadata, CancellationToken cancellationToken)
|
|
{
|
|
var client = EnsureClient();
|
|
// SDK 无异步则封装为 Task
|
|
await Task.Run(() => client.PutObject(bucket, key, content, metadata), cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成预签名 URL。
|
|
/// </summary>
|
|
private string GeneratePresignedUrl(string objectKey, TimeSpan expires, SignHttpMethod method, string? contentType)
|
|
{
|
|
var request = new GeneratePresignedUriRequest(CurrentOptions.AliyunOss.Bucket, objectKey, method)
|
|
{
|
|
Expiration = DateTime.Now.Add(expires)
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(contentType))
|
|
{
|
|
request.ContentType = contentType;
|
|
}
|
|
|
|
var uri = EnsureClient().GeneratePresignedUri(request);
|
|
return uri.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建或复用 OSS 客户端。
|
|
/// </summary>
|
|
private OssClient EnsureClient()
|
|
{
|
|
if (_client != null)
|
|
{
|
|
return _client;
|
|
}
|
|
|
|
var options = CurrentOptions.AliyunOss;
|
|
_client = new OssClient(options.Endpoint, options.AccessKeyId, options.AccessKeySecret);
|
|
return _client;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|