using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Aliyun.OSS; using Aliyun.OSS.Util; using Microsoft.Extensions.Options; using TakeoutSaaS.Module.Storage.Abstractions; using TakeoutSaaS.Module.Storage.Models; using TakeoutSaaS.Module.Storage.Options; namespace TakeoutSaaS.Module.Storage.Providers; /// /// 阿里云 OSS 存储提供商实现。 /// public sealed class AliyunOssStorageProvider(IOptionsMonitor optionsMonitor) : IObjectStorageProvider, IDisposable { private OssClient? _client; private bool _disposed; private StorageOptions CurrentOptions => optionsMonitor.CurrentValue; /// public StorageProviderKind Kind => StorageProviderKind.AliyunOss; /// public async Task 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 }; } /// public Task 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(), ExpiresAt = expiresAt, ObjectKey = request.ObjectKey, SignedDownloadUrl = downloadUrl }; return Task.FromResult(result); } /// public Task GenerateDownloadUrlAsync(string objectKey, TimeSpan expires, CancellationToken cancellationToken = default) { // 1. 生成预签名下载 URL var url = GeneratePresignedUrl(objectKey, expires, SignHttpMethod.Get, null); return Task.FromResult(url); } /// 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}"; } /// /// 上传对象到 OSS。 /// 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); } /// /// 生成预签名 URL。 /// 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(); } /// /// 构建或复用 OSS 客户端。 /// private OssClient EnsureClient() { if (_client != null) { return _client; } var options = CurrentOptions.AliyunOss; _client = new OssClient(options.Endpoint, options.AccessKeyId, options.AccessKeySecret); return _client; } /// public void Dispose() { if (_disposed) { return; } _disposed = true; } }