242 lines
8.3 KiB
C#
242 lines
8.3 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|