using COSXML; using COSXML.Auth; using COSXML.CosException; using COSXML.Model.Object; using COSXML.Model.Tag; using COSXML.Transfer; using Foxel.Services.Attributes; using Foxel.Services.Configuration; using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; public class CosStorageConfig { public string Region { get; set; } = string.Empty; public string SecretId { get; set; } = string.Empty; public string SecretKey { get; set; } = string.Empty; public string? Token { get; set; } // Token 可能为空 public string BucketName { get; set; } = string.Empty; public string? CdnUrl { get; set; } public bool PublicRead { get; set; } = false; } public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider { private readonly CosStorageConfig _config; private readonly ILogger _logger; public CustomQCloudCredentialProvider(CosStorageConfig config, ILogger logger) : base(null, null, 0L, null) // Base constructor parameters are set in Refresh { _config = config; _logger = logger; Refresh(); } public sealed override void Refresh() { try { string tmpSecretId = _config.SecretId; string tmpSecretKey = _config.SecretKey; string? tmpToken = _config.Token; long tmpStartTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // 腾讯云建议临时密钥有效期最长2小时(7200秒) long tmpExpiredTime = tmpStartTime + 7200; SetQCloudCredential(tmpSecretId, tmpSecretKey, $"{tmpStartTime};{tmpExpiredTime}", tmpToken); } catch (Exception ex) { _logger.LogError(ex, "刷新临时密钥时出错"); throw; } } } [StorageProvider(StorageType.Cos)] public class CosStorageProvider : IStorageProvider { private readonly CosStorageConfig _cosConfig; private readonly IConfigService _configService; // 保留用于可能的应用级配置 private readonly ILogger _logger; public CosStorageProvider(CosStorageConfig cosConfig, IConfigService configService, ILogger logger) { _cosConfig = cosConfig; _configService = configService; // 存储起来以备后用 _logger = logger; if (string.IsNullOrEmpty(_cosConfig.Region) || string.IsNullOrEmpty(_cosConfig.SecretId) || string.IsNullOrEmpty(_cosConfig.SecretKey) || string.IsNullOrEmpty(_cosConfig.BucketName)) { _logger.LogError("COS Storage配置不完整 (Region, SecretId, SecretKey, BucketName 都是必需的)."); throw new InvalidOperationException("COS Storage配置不完整。"); } } private CosXml CreateClient() { var config = new CosXmlConfig.Builder() .IsHttps(true) .SetRegion(_cosConfig.Region) .SetDebugLog(true) .Build(); var cosCredentialProvider = new CustomQCloudCredentialProvider(_cosConfig, _logger.IsEnabled(LogLevel.Debug) ? Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger() : Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); return new CosXmlServer(config, cosCredentialProvider); } public async Task SaveAsync(Stream fileStream, string fileName, string contentType) { try { // 创建唯一的文件存储路径 string currentDate = DateTime.Now.ToString("yyyy/MM"); // fileName 参数现在是期望的最终文件名部分,例如 "guid.ext" // string ext = Path.GetExtension(fileName); // 旧的 fileName 是原始文件名,现在 fileName 是目标文件名 // string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}"; // 旧逻辑 string objectKey = $"{currentDate}/{fileName}"; // 新逻辑 // 创建临时文件 string tempPath = Path.GetTempFileName(); try { await using (var fileStream2 = new FileStream(tempPath, FileMode.Create)) { await fileStream.CopyToAsync(fileStream2); } var cosXmlClient = CreateClient(); var transferConfig = new TransferConfig(); var transferManager = new TransferManager(cosXmlClient, transferConfig); var uploadTask = new COSXMLUploadTask(_cosConfig.BucketName, objectKey); uploadTask.SetSrcPath(tempPath); await transferManager.UploadAsync(uploadTask); return objectKey; } finally { // 确保临时文件被删除 if (File.Exists(tempPath)) { File.Delete(tempPath); } } } catch (CosClientException clientEx) { _logger.LogError(clientEx, "COS客户端异常"); throw; } catch (CosServerException serverEx) { _logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); throw; } catch (Exception ex) { _logger.LogError(ex, "上传文件到腾讯云COS时出错"); throw; } } public async Task DeleteAsync(string storagePath) { try { if (string.IsNullOrEmpty(storagePath)) return; var cosXmlClient = CreateClient(); var request = new DeleteObjectRequest(_cosConfig.BucketName, storagePath); await Task.Run(() => cosXmlClient.DeleteObject(request)); } catch (CosClientException clientEx) { _logger.LogWarning(clientEx, "COS客户端异常"); } catch (CosServerException serverEx) { _logger.LogWarning(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); } catch (Exception ex) { _logger.LogWarning(ex, "从腾讯云COS删除文件时出错"); } } public string GetUrl(int pictureId,string storagePath) { try { if (string.IsNullOrEmpty(storagePath)) return "/images/unavailable.gif"; string? cdnUrl = _cosConfig.CdnUrl; string bucketName = _cosConfig.BucketName; string region = _cosConfig.Region; bool isPublicRead = _cosConfig.PublicRead; // 优先使用CDN if (!string.IsNullOrEmpty(cdnUrl)) return $"{cdnUrl.TrimEnd('/')}/{storagePath}"; // 公开读取的桶可直接访问 if (isPublicRead) return $"https://{bucketName}.cos.{region}.myqcloud.com/{storagePath}"; var cosXmlClient = CreateClient(); var bucketParts = bucketName.Split('-'); var request = new PreSignatureStruct { bucket = bucketParts[0], appid = bucketParts[1], region = region, key = storagePath, httpMethod = "GET", isHttps = true, signDurationSecond = 3600 * 24 }; var url = cosXmlClient.GenerateSignURL(request); return url; } catch (Exception ex) { _logger.LogError(ex, "生成腾讯云COS文件URL时出错"); return "/images/unavailable.gif"; } } public async Task DownloadFileAsync(string storagePath) { try { if (string.IsNullOrEmpty(storagePath)) { throw new ArgumentException("存储路径不能为空"); } // 创建临时目录 var tempDir = Path.Combine(Path.GetTempPath(), "FoxelCosTemp"); if (!Directory.Exists(tempDir)) { Directory.CreateDirectory(tempDir); } string bucketName = _cosConfig.BucketName; string fileName = Path.GetFileName(storagePath); var cosXmlClient = CreateClient(); var transferConfig = new TransferConfig(); var transferManager = new TransferManager(cosXmlClient, transferConfig); var downloadTask = new COSXMLDownloadTask(bucketName, storagePath, tempDir, fileName); await transferManager.DownloadAsync(downloadTask); return Path.Combine(tempDir, fileName); } catch (CosClientException clientEx) { _logger.LogError(clientEx, "COS客户端异常"); throw; } catch (CosServerException serverEx) { _logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); throw; } catch (Exception ex) { _logger.LogError(ex, "从腾讯云COS下载文件时出错"); throw; } } }