HaniBlindBox/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Storage/TencentCosProvider.cs
2026-01-19 15:05:52 +08:00

242 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using COSXML;
using COSXML.Auth;
using COSXML.Model.Object;
using HoneyBox.Admin.Business.Models.Config;
using HoneyBox.Admin.Business.Models.Upload;
using HoneyBox.Admin.Business.Services.Interfaces;
using Microsoft.Extensions.Logging;
namespace HoneyBox.Admin.Business.Services.Storage;
/// <summary>
/// 腾讯云COS存储提供者
/// 将文件上传到腾讯云COS对象存储
/// </summary>
public class TencentCosProvider : IStorageProvider
{
private readonly ILogger<TencentCosProvider> _logger;
private readonly Func<UploadSetting?> _getUploadSetting;
private const string UploadBasePath = "uploads";
public TencentCosProvider(
ILogger<TencentCosProvider> logger,
Func<UploadSetting?> getUploadSetting)
{
_logger = logger;
_getUploadSetting = getUploadSetting;
}
/// <inheritdoc />
public string StorageType => "3";
/// <inheritdoc />
public async Task<UploadResult> UploadAsync(Stream fileStream, string fileName, string contentType)
{
try
{
var setting = _getUploadSetting();
if (setting == null)
{
return UploadResult.Fail("存储配置无效,请检查上传配置");
}
// 验证必要的配置参数
var validationError = ValidateConfig(setting);
if (validationError != null)
{
return UploadResult.Fail(validationError);
}
// 生成日期目录路径: uploads/2026/01/19/
var now = DateTime.Now;
var datePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}";
// 生成唯一文件名
var uniqueFileName = GenerateUniqueFileName(fileName);
// 构建COS对象路径
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
// 创建COS客户端
var cosXml = CreateCosXmlServer(setting);
// 将流转换为字节数组
byte[] fileBytes;
using (var memoryStream = new MemoryStream())
{
await fileStream.CopyToAsync(memoryStream);
fileBytes = memoryStream.ToArray();
}
// 上传文件
var putObjectRequest = new PutObjectRequest(setting.Bucket!, objectKey, fileBytes);
putObjectRequest.SetRequestHeader("Content-Type", contentType);
var result = cosXml.PutObject(putObjectRequest);
if (result.IsSuccessful())
{
// 生成访问URL
var url = GenerateAccessUrl(setting.Domain!, objectKey);
_logger.LogInformation("腾讯云COS上传成功: {FileName} -> {Url}", fileName, url);
return UploadResult.Ok(url);
}
else
{
var errorMessage = $"上传到云存储失败: {result.httpMessage}";
_logger.LogError("腾讯云COS上传失败: {FileName}, 错误: {Error}", fileName, errorMessage);
return UploadResult.Fail(errorMessage);
}
}
catch (COSXML.CosException.CosClientException clientEx)
{
var errorMessage = $"上传到云存储失败: 客户端错误 - {clientEx.Message}";
_logger.LogError(clientEx, "腾讯云COS客户端错误: {FileName}", fileName);
return UploadResult.Fail(errorMessage);
}
catch (COSXML.CosException.CosServerException serverEx)
{
var errorMessage = $"上传到云存储失败: 服务端错误 - {serverEx.GetInfo()}";
_logger.LogError(serverEx, "腾讯云COS服务端错误: {FileName}", fileName);
return UploadResult.Fail(errorMessage);
}
catch (Exception ex)
{
var errorMessage = $"上传到云存储失败: {ex.Message}";
_logger.LogError(ex, "腾讯云COS上传异常: {FileName}", fileName);
return UploadResult.Fail(errorMessage);
}
}
/// <inheritdoc />
public Task<bool> DeleteAsync(string fileUrl)
{
try
{
if (string.IsNullOrWhiteSpace(fileUrl))
{
return Task.FromResult(false);
}
var setting = _getUploadSetting();
if (setting == null)
{
_logger.LogWarning("腾讯云COS删除失败配置无效");
return Task.FromResult(false);
}
// 从URL中提取对象路径
var objectKey = ExtractObjectKeyFromUrl(fileUrl, setting.Domain);
if (string.IsNullOrEmpty(objectKey))
{
_logger.LogWarning("腾讯云COS删除失败无法解析对象路径: {Url}", fileUrl);
return Task.FromResult(false);
}
var cosXml = CreateCosXmlServer(setting);
var deleteObjectRequest = new DeleteObjectRequest(setting.Bucket!, objectKey);
var result = cosXml.DeleteObject(deleteObjectRequest);
if (result.IsSuccessful())
{
_logger.LogInformation("腾讯云COS删除成功: {Url}", fileUrl);
return Task.FromResult(true);
}
_logger.LogWarning("腾讯云COS删除失败: {Url}", fileUrl);
return Task.FromResult(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "腾讯云COS删除异常: {Url}", fileUrl);
return Task.FromResult(false);
}
}
/// <summary>
/// 验证配置参数
/// </summary>
private static string? ValidateConfig(UploadSetting setting)
{
if (string.IsNullOrWhiteSpace(setting.Bucket))
return "存储配置无效: Bucket不能为空";
if (string.IsNullOrWhiteSpace(setting.Region))
return "存储配置无效: Region不能为空";
if (string.IsNullOrWhiteSpace(setting.AccessKeyId))
return "存储配置无效: AccessKeyId不能为空";
if (string.IsNullOrWhiteSpace(setting.AccessKeySecret))
return "存储配置无效: AccessKeySecret不能为空";
if (string.IsNullOrWhiteSpace(setting.Domain))
return "存储配置无效: Domain不能为空";
return null;
}
/// <summary>
/// 创建COS客户端
/// </summary>
private static CosXml CreateCosXmlServer(UploadSetting setting)
{
var config = new CosXmlConfig.Builder()
.IsHttps(true)
.SetRegion(setting.Region!)
.Build();
var credentialProvider = new DefaultQCloudCredentialProvider(
setting.AccessKeyId!,
setting.AccessKeySecret!,
600); // 临时密钥有效期600秒
return new CosXmlServer(config, credentialProvider);
}
/// <summary>
/// 生成唯一文件名
/// 格式: {timestamp}_{guid}{extension}
/// </summary>
public static string GenerateUniqueFileName(string originalFileName)
{
var extension = Path.GetExtension(originalFileName).ToLowerInvariant();
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff");
var guid = Guid.NewGuid().ToString("N")[..8]; // 取GUID前8位
return $"{timestamp}_{guid}{extension}";
}
/// <summary>
/// 生成访问URL
/// </summary>
public static string GenerateAccessUrl(string domain, string objectKey)
{
// 确保domain以https://开头,不以/结尾
var normalizedDomain = domain.TrimEnd('/');
if (!normalizedDomain.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!normalizedDomain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
normalizedDomain = $"https://{normalizedDomain}";
}
// 确保objectKey以/开头
var normalizedKey = objectKey.StartsWith('/') ? objectKey : $"/{objectKey}";
return $"{normalizedDomain}{normalizedKey}";
}
/// <summary>
/// 从URL中提取对象路径
/// </summary>
private static string? ExtractObjectKeyFromUrl(string fileUrl, string? domain)
{
if (string.IsNullOrWhiteSpace(domain))
return null;
try
{
var uri = new Uri(fileUrl);
return uri.AbsolutePath.TrimStart('/');
}
catch
{
return null;
}
}
}