mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-12 02:20:28 +08:00
262 lines
9.2 KiB
C#
262 lines
9.2 KiB
C#
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<CustomQCloudCredentialProvider> _logger;
|
||
|
||
public CustomQCloudCredentialProvider(CosStorageConfig config, ILogger<CustomQCloudCredentialProvider> 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<CosStorageProvider> _logger;
|
||
|
||
public CosStorageProvider(CosStorageConfig cosConfig, IConfigService configService, ILogger<CosStorageProvider> 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<CustomQCloudCredentialProvider>() :
|
||
Microsoft.Extensions.Logging.Abstractions.NullLogger<CustomQCloudCredentialProvider>.Instance);
|
||
|
||
return new CosXmlServer(config, cosCredentialProvider);
|
||
}
|
||
|
||
public async Task<string> 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<string> 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;
|
||
}
|
||
}
|
||
}
|