mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-02 22:20:01 +08:00
feat(storage): implement storage management API and enhance storage mode handling
This commit is contained in:
@@ -15,7 +15,7 @@ public interface IStorageProvider
|
||||
/// <summary>
|
||||
/// 获取文件URL
|
||||
/// </summary>
|
||||
string GetUrl(string storagePath);
|
||||
string GetUrl(int pictureId,string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件到本地临时目录
|
||||
|
||||
@@ -8,18 +8,18 @@ namespace Foxel.Services.Storage;
|
||||
public interface IStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// 在指定存储类型上执行操作
|
||||
/// 在指定存储模式上执行操作
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">操作结果类型</typeparam>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="storageModeId">存储模式的ID</param>
|
||||
/// <param name="operation">要执行的操作</param>
|
||||
/// <returns>操作结果</returns>
|
||||
Task<TResult> ExecuteAsync<TResult>(StorageType storageType, Func<IStorageProvider, Task<TResult>> operation);
|
||||
Task<TResult> ExecuteAsync<TResult>(int storageModeId, Func<IStorageProvider, Task<TResult>> operation);
|
||||
|
||||
/// <summary>
|
||||
/// 在指定存储类型上执行无返回值的操作
|
||||
/// 在指定存储模式上执行无返回值的操作
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="storageModeId">存储模式的ID</param>
|
||||
/// <param name="operation">要执行的操作</param>
|
||||
Task ExecuteAsync(StorageType storageType, Func<IStorageProvider, Task> operation);
|
||||
Task ExecuteAsync(int storageModeId, Func<IStorageProvider, Task> operation);
|
||||
}
|
||||
|
||||
@@ -10,15 +10,26 @@ 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 IConfigService _configService;
|
||||
private readonly CosStorageConfig _config;
|
||||
private readonly ILogger<CustomQCloudCredentialProvider> _logger;
|
||||
|
||||
public CustomQCloudCredentialProvider(IConfigService configService, ILogger<CustomQCloudCredentialProvider> logger)
|
||||
: base(null, null, 0L, null)
|
||||
public CustomQCloudCredentialProvider(CosStorageConfig config, ILogger<CustomQCloudCredentialProvider> logger)
|
||||
: base(null, null, 0L, null) // Base constructor parameters are set in Refresh
|
||||
{
|
||||
_configService = configService;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
Refresh();
|
||||
}
|
||||
@@ -27,11 +38,12 @@ public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProv
|
||||
{
|
||||
try
|
||||
{
|
||||
string tmpSecretId = _configService["Storage:CosStorageSecretId"];
|
||||
string tmpSecretKey = _configService["Storage:CosStorageSecretKey"];
|
||||
string tmpToken = _configService["Storage:CosStorageToken"];
|
||||
string tmpSecretId = _config.SecretId;
|
||||
string tmpSecretKey = _config.SecretKey;
|
||||
string? tmpToken = _config.Token;
|
||||
long tmpStartTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
long tmpExpiredTime = tmpStartTime + 7200;
|
||||
// 腾讯云建议临时密钥有效期最长2小时(7200秒)
|
||||
long tmpExpiredTime = tmpStartTime + 7200;
|
||||
SetQCloudCredential(tmpSecretId, tmpSecretKey,
|
||||
$"{tmpStartTime};{tmpExpiredTime}", tmpToken);
|
||||
}
|
||||
@@ -44,18 +56,38 @@ public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProv
|
||||
}
|
||||
|
||||
[StorageProvider(StorageType.Cos)]
|
||||
public class CosStorageProvider(IConfigService configService, ILogger<CosStorageProvider> logger) : IStorageProvider
|
||||
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(configService["Storage:CosStorageRegion"])
|
||||
.SetRegion(_cosConfig.Region)
|
||||
.SetDebugLog(true)
|
||||
.Build();
|
||||
|
||||
var cosCredentialProvider = new CustomQCloudCredentialProvider(configService,
|
||||
logger.IsEnabled(LogLevel.Debug) ?
|
||||
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);
|
||||
|
||||
@@ -85,7 +117,7 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
var cosXmlClient = CreateClient();
|
||||
var transferConfig = new TransferConfig();
|
||||
var transferManager = new TransferManager(cosXmlClient, transferConfig);
|
||||
var uploadTask = new COSXMLUploadTask(configService["Storage:CosStorageBucketName"], objectKey);
|
||||
var uploadTask = new COSXMLUploadTask(_cosConfig.BucketName, objectKey);
|
||||
uploadTask.SetSrcPath(tempPath);
|
||||
await transferManager.UploadAsync(uploadTask);
|
||||
return objectKey;
|
||||
@@ -101,17 +133,17 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
logger.LogError(clientEx, "COS客户端异常");
|
||||
_logger.LogError(clientEx, "COS客户端异常");
|
||||
throw;
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
_logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "上传文件到腾讯云COS时出错");
|
||||
_logger.LogError(ex, "上传文件到腾讯云COS时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -124,38 +156,38 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
return;
|
||||
|
||||
var cosXmlClient = CreateClient();
|
||||
var request = new DeleteObjectRequest(configService["Storage:CosStorageBucketName"], storagePath);
|
||||
var request = new DeleteObjectRequest(_cosConfig.BucketName, storagePath);
|
||||
await Task.Run(() => cosXmlClient.DeleteObject(request));
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
logger.LogWarning(clientEx, "COS客户端异常");
|
||||
_logger.LogWarning(clientEx, "COS客户端异常");
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
logger.LogWarning(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
_logger.LogWarning(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "从腾讯云COS删除文件时出错");
|
||||
_logger.LogWarning(ex, "从腾讯云COS删除文件时出错");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
public string GetUrl(int pictureId,string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return "/images/unavailable.gif";
|
||||
|
||||
string cdnUrl = configService["Storage:CosStorageCdnUrl"];
|
||||
string bucketName = configService["Storage:CosStorageBucketName"];
|
||||
string region = configService["Storage:CosStorageRegion"];
|
||||
bool isPublicRead = bool.TryParse(configService["Storage:CosStoragePublicRead"], out var publicRead) && publicRead;
|
||||
string? cdnUrl = _cosConfig.CdnUrl;
|
||||
string bucketName = _cosConfig.BucketName;
|
||||
string region = _cosConfig.Region;
|
||||
bool isPublicRead = _cosConfig.PublicRead;
|
||||
|
||||
// 优先使用CDN
|
||||
if (!string.IsNullOrEmpty(cdnUrl))
|
||||
return $"{cdnUrl}/{storagePath}";
|
||||
return $"{cdnUrl.TrimEnd('/')}/{storagePath}";
|
||||
|
||||
// 公开读取的桶可直接访问
|
||||
if (isPublicRead)
|
||||
@@ -179,7 +211,7 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "生成腾讯云COS文件URL时出错");
|
||||
_logger.LogError(ex, "生成腾讯云COS文件URL时出错");
|
||||
return "/images/unavailable.gif";
|
||||
}
|
||||
}
|
||||
@@ -200,7 +232,7 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
Directory.CreateDirectory(tempDir);
|
||||
}
|
||||
|
||||
string bucketName = configService["Storage:CosStorageBucketName"];
|
||||
string bucketName = _cosConfig.BucketName;
|
||||
string fileName = Path.GetFileName(storagePath);
|
||||
|
||||
var cosXmlClient = CreateClient();
|
||||
@@ -212,17 +244,17 @@ public class CosStorageProvider(IConfigService configService, ILogger<CosStorage
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
logger.LogError(clientEx, "COS客户端异常");
|
||||
_logger.LogError(clientEx, "COS客户端异常");
|
||||
throw;
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
_logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "从腾讯云COS下载文件时出错");
|
||||
_logger.LogError(ex, "从腾讯云COS下载文件时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,49 +4,134 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.Local)]
|
||||
public class LocalStorageProvider(IConfigService configService) : IStorageProvider
|
||||
public class LocalStorageConfig
|
||||
{
|
||||
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
||||
public string BasePath { get; set; } = string.Empty;
|
||||
|
||||
public string ServerUrl { get; set; } = string.Empty;
|
||||
|
||||
public string PublicBasePath { get; set; } = "/Uploads";
|
||||
}
|
||||
|
||||
[StorageProvider(StorageType.Local)]
|
||||
public class LocalStorageProvider : IStorageProvider
|
||||
{
|
||||
private readonly LocalStorageConfig _config;
|
||||
private readonly ILogger<LocalStorageProvider> _logger;
|
||||
|
||||
public LocalStorageProvider(LocalStorageConfig config, ILogger<LocalStorageProvider> logger)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
if (string.IsNullOrWhiteSpace(_config.BasePath))
|
||||
{
|
||||
var defaultPath = Path.Combine(Directory.GetCurrentDirectory(), "DefaultUploads");
|
||||
_logger.LogWarning("LocalStorageConfig.BasePath 未配置,将使用默认路径: {DefaultPath}", defaultPath);
|
||||
throw new InvalidOperationException("LocalStorageConfig.BasePath 必须在配置中提供。");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_config.BasePath);
|
||||
}
|
||||
|
||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
string currentDate = DateTime.Now.ToString("yyyy/MM");
|
||||
string folder = Path.Combine(_baseDirectory, currentDate);
|
||||
Directory.CreateDirectory(folder);
|
||||
try
|
||||
{
|
||||
string currentDate = DateTime.Now.ToString("yyyy/MM");
|
||||
string folder = Path.Combine(_config.BasePath, currentDate);
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
string newFileName = fileName;
|
||||
string filePath = Path.Combine(folder, newFileName);
|
||||
string newFileName = fileName;
|
||||
string filePath = Path.Combine(folder, newFileName);
|
||||
|
||||
await using var output = new FileStream(filePath, FileMode.Create);
|
||||
await fileStream.CopyToAsync(output);
|
||||
return $"/Uploads/{currentDate}/{newFileName}";
|
||||
await using var output = new FileStream(filePath, FileMode.Create);
|
||||
await fileStream.CopyToAsync(output);
|
||||
return $"{_config.PublicBasePath.TrimEnd('/')}/{currentDate}/{newFileName}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "保存文件到本地存储时出错。BasePath: {BasePath}, FileName: {FileName}", _config.BasePath, fileName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task DeleteAsync(string storagePath)
|
||||
{
|
||||
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), storagePath.TrimStart('/'));
|
||||
if (File.Exists(fullPath))
|
||||
File.Delete(fullPath);
|
||||
try
|
||||
{
|
||||
string relativePath = storagePath;
|
||||
if (!string.IsNullOrEmpty(_config.PublicBasePath) && storagePath.StartsWith(_config.PublicBasePath))
|
||||
{
|
||||
relativePath = storagePath.Substring(_config.PublicBasePath.Length);
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(_config.BasePath, relativePath.TrimStart('/'));
|
||||
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
_logger.LogInformation("已删除本地文件: {FullPath}", fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("尝试删除本地文件但文件未找到: {FullPath}", fullPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除本地文件时出错。StoragePath: {StoragePath}, BasePath: {BasePath}", storagePath,
|
||||
_config.BasePath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetUrl(string? storagePath)
|
||||
public string GetUrl(int pictureId,string? storagePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return $"/images/unavailable.gif";
|
||||
|
||||
string serverUrl = configService["AppSettings:ServerUrl"];
|
||||
return $"{serverUrl}{storagePath}";
|
||||
string serverUrl = _config.ServerUrl.TrimEnd('/');
|
||||
return string.IsNullOrEmpty(serverUrl) ? storagePath : $"{serverUrl}{storagePath}";
|
||||
}
|
||||
|
||||
public Task<string> DownloadFileAsync(string storagePath)
|
||||
{
|
||||
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), storagePath.TrimStart('/'));
|
||||
if (!File.Exists(fullPath))
|
||||
try
|
||||
{
|
||||
throw new FileNotFoundException($"找不到文件: {fullPath}");
|
||||
string relativePath = storagePath;
|
||||
if (!string.IsNullOrEmpty(_config.PublicBasePath) && storagePath.StartsWith(_config.PublicBasePath))
|
||||
{
|
||||
relativePath = storagePath.Substring(_config.PublicBasePath.Length);
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(_config.BasePath, relativePath.TrimStart('/'));
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogError("尝试下载但文件未找到: {FullPath}", fullPath);
|
||||
throw new FileNotFoundException($"本地存储中找不到文件: {fullPath}", fullPath);
|
||||
}
|
||||
|
||||
string tempFileName = Path.GetRandomFileName();
|
||||
if (Path.HasExtension(fullPath))
|
||||
{
|
||||
tempFileName = Path.ChangeExtension(tempFileName, Path.GetExtension(fullPath));
|
||||
}
|
||||
|
||||
string tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName);
|
||||
|
||||
File.Copy(fullPath, tempFilePath, true);
|
||||
_logger.LogInformation("已将文件 {FullPath} 复制到临时位置 {TempFilePath} 以供下载/处理", fullPath, tempFilePath);
|
||||
|
||||
return Task.FromResult(tempFilePath); // 返回临时文件的路径
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "下载本地文件时出错。StoragePath: {StoragePath}, BasePath: {BasePath}", storagePath,
|
||||
_config.BasePath);
|
||||
throw;
|
||||
}
|
||||
return Task.FromResult(fullPath);
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,47 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.S3)]
|
||||
public class S3StorageProvider(IConfigService configService, ILogger<S3StorageProvider> logger) : IStorageProvider
|
||||
public class S3StorageConfig
|
||||
{
|
||||
public string AccessKey { get; set; } = string.Empty;
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
public string? Region { get; set; } // Region 可能为空,特别是对于非AWS S3兼容存储
|
||||
public bool UsePathStyleUrls { get; set; } = false;
|
||||
public string BucketName { get; set; } = string.Empty;
|
||||
public string? CdnUrl { get; set; }
|
||||
}
|
||||
|
||||
[StorageProvider(StorageType.S3)]
|
||||
public class S3StorageProvider : IStorageProvider
|
||||
{
|
||||
private readonly S3StorageConfig _s3Config;
|
||||
private readonly IConfigService _configService; // 保留用于可能的应用级配置
|
||||
private readonly ILogger<S3StorageProvider> _logger;
|
||||
|
||||
public S3StorageProvider(S3StorageConfig s3Config, IConfigService configService, ILogger<S3StorageProvider> logger)
|
||||
{
|
||||
_s3Config = s3Config;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
|
||||
if (string.IsNullOrEmpty(_s3Config.AccessKey) ||
|
||||
string.IsNullOrEmpty(_s3Config.SecretKey) ||
|
||||
string.IsNullOrEmpty(_s3Config.Endpoint) ||
|
||||
string.IsNullOrEmpty(_s3Config.BucketName))
|
||||
{
|
||||
_logger.LogError("S3 Storage配置不完整 (AccessKey, SecretKey, Endpoint, BucketName 都是必需的).");
|
||||
throw new InvalidOperationException("S3 Storage配置不完整。");
|
||||
}
|
||||
}
|
||||
|
||||
private AmazonS3Client CreateClient()
|
||||
{
|
||||
string accessKey = configService["Storage:S3StorageAccessKey"];
|
||||
string secretKey = configService["Storage:S3StorageSecretKey"];
|
||||
string endpoint = configService["Storage:S3StorageEndpoint"];
|
||||
string region = configService["Storage:S3StorageRegion"];
|
||||
bool usePathStyleUrls = bool.TryParse(configService["Storage:S3StorageUsePathStyleUrls"], out var usePathStyle) && usePathStyle;
|
||||
string accessKey = _s3Config.AccessKey;
|
||||
string secretKey = _s3Config.SecretKey;
|
||||
string endpoint = _s3Config.Endpoint;
|
||||
string? region = _s3Config.Region;
|
||||
bool usePathStyleUrls = _s3Config.UsePathStyleUrls;
|
||||
|
||||
var config = new AmazonS3Config
|
||||
{
|
||||
@@ -25,7 +56,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
ForcePathStyle = usePathStyleUrls
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(region) && endpoint.Contains("amazonaws.com"))
|
||||
if (!string.IsNullOrEmpty(region) && endpoint.Contains("amazonaws.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
config.RegionEndpoint = Amazon.RegionEndpoint.GetBySystemName(region);
|
||||
}
|
||||
@@ -55,7 +86,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
{
|
||||
InputStream = fileStream,
|
||||
Key = objectKey,
|
||||
BucketName = configService["Storage:S3StorageBucketName"],
|
||||
BucketName = _s3Config.BucketName,
|
||||
ContentType = contentType
|
||||
};
|
||||
|
||||
@@ -66,7 +97,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "上传文件到S3时出错");
|
||||
_logger.LogError(ex, "上传文件到S3时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +112,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
using var client = CreateClient();
|
||||
var deleteRequest = new DeleteObjectRequest
|
||||
{
|
||||
BucketName = configService["Storage:S3StorageBucketName"],
|
||||
BucketName = _s3Config.BucketName,
|
||||
Key = storagePath
|
||||
};
|
||||
|
||||
@@ -89,30 +120,30 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "从S3删除文件时出错");
|
||||
_logger.LogWarning(ex, "从S3删除文件时出错");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
public string GetUrl(int pictureId,string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return "/images/unavailable.gif";
|
||||
|
||||
string cdnUrl = configService["Storage:S3StorageCdnUrl"];
|
||||
string? cdnUrl = _s3Config.CdnUrl;
|
||||
|
||||
// 如果配置了CDN URL,使用CDN
|
||||
if (!string.IsNullOrEmpty(cdnUrl))
|
||||
{
|
||||
return $"{cdnUrl}/{storagePath}";
|
||||
return $"{cdnUrl.TrimEnd('/')}/{storagePath}";
|
||||
}
|
||||
|
||||
// 否则使用S3直链或生成预签名URL
|
||||
using var client = CreateClient();
|
||||
var request = new GetPreSignedUrlRequest
|
||||
{
|
||||
BucketName = configService["Storage:S3StorageBucketName"],
|
||||
BucketName = _s3Config.BucketName,
|
||||
Key = storagePath,
|
||||
Expires = DateTime.UtcNow.AddHours(1) // URL有效期1小时
|
||||
};
|
||||
@@ -121,7 +152,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "生成S3文件URL时出错");
|
||||
_logger.LogError(ex, "生成S3文件URL时出错");
|
||||
return "/images/unavailable.gif";
|
||||
}
|
||||
}
|
||||
@@ -150,7 +181,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
using var client = CreateClient();
|
||||
var request = new GetObjectRequest
|
||||
{
|
||||
BucketName = configService["Storage:S3StorageBucketName"],
|
||||
BucketName = _s3Config.BucketName,
|
||||
Key = storagePath
|
||||
};
|
||||
|
||||
@@ -162,7 +193,7 @@ public class S3StorageProvider(IConfigService configService, ILogger<S3StoragePr
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "从S3下载文件时出错");
|
||||
_logger.LogError(ex, "从S3下载文件时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,37 @@ using System.Text.Json.Serialization;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Configuration;
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
public class TelegramStorageConfig
|
||||
{
|
||||
public string BotToken { get; set; } = string.Empty;
|
||||
public string ChatId { get; set; } = string.Empty;
|
||||
public string? ProxyAddress { get; set; }
|
||||
public string? ProxyPort { get; set; }
|
||||
public string? ProxyUsername { get; set; }
|
||||
public string? ProxyPassword { get; set; }
|
||||
}
|
||||
|
||||
[StorageProvider(StorageType.Telegram)]
|
||||
public class TelegramStorageProvider(IConfigService configService, ILogger<TelegramStorageProvider> logger) : IStorageProvider
|
||||
public class TelegramStorageProvider(TelegramStorageConfig _telegramConfig, IConfigService configService, ILogger<TelegramStorageProvider> logger) : IStorageProvider
|
||||
{
|
||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
string botToken = configService["Storage:TelegramStorageBotToken"];
|
||||
string chatId = configService["Storage:TelegramStorageChatId"];
|
||||
string botToken = _telegramConfig.BotToken;
|
||||
string chatId = _telegramConfig.ChatId;
|
||||
if (string.IsNullOrEmpty(botToken) || string.IsNullOrEmpty(chatId))
|
||||
{
|
||||
logger.LogError("Telegram BotToken 或 ChatId 未在配置中提供。");
|
||||
throw new InvalidOperationException("Telegram BotToken 或 ChatId 未配置。");
|
||||
}
|
||||
|
||||
using var httpClient = CreateHttpClient();
|
||||
using var formData = new MultipartFormDataContent();
|
||||
formData.Add(new StringContent(chatId), "chat_id");
|
||||
using var formData = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(chatId), "chat_id" }
|
||||
};
|
||||
var safeFileName = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (safeFileName.Length > 100)
|
||||
safeFileName = safeFileName.Substring(0, 100);
|
||||
@@ -103,23 +119,37 @@ public class TelegramStorageProvider(IConfigService configService, ILogger<Teleg
|
||||
var metadata = JsonSerializer.Deserialize<TelegramFileMetadata>(storagePath);
|
||||
if (metadata == null || string.IsNullOrEmpty(metadata.ChatId) || metadata.MessageId <= 0)
|
||||
{
|
||||
logger.LogWarning("无效的 Telegram 元数据,无法删除: {StoragePath}", storagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
string botToken = configService["Storage:TelegramStorageBotToken"];
|
||||
|
||||
string botToken = _telegramConfig.BotToken;
|
||||
if (string.IsNullOrEmpty(botToken))
|
||||
{
|
||||
logger.LogError("Telegram BotToken 未在配置中提供,无法删除文件。");
|
||||
return;
|
||||
}
|
||||
using var httpClient = CreateHttpClient();
|
||||
var url =
|
||||
$"https://api.telegram.org/bot{botToken}/deleteMessage?chat_id={metadata.ChatId}&message_id={metadata.MessageId}";
|
||||
await httpClient.GetAsync(url);
|
||||
var response = await httpClient.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
logger.LogWarning("删除 Telegram 消息失败: ChatId={ChatId}, MessageId={MessageId}, Status={StatusCode}, Response={ErrorContent}", metadata.ChatId, metadata.MessageId, response.StatusCode, errorContent);
|
||||
}
|
||||
}
|
||||
catch (JsonException jsonEx)
|
||||
{
|
||||
logger.LogWarning(jsonEx, "解析 Telegram 元数据以进行删除时出错: {StoragePath}", storagePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "删除 Telegram 文件时出错");
|
||||
logger.LogWarning(ex, "删除 Telegram 文件时出错: {StoragePath}", storagePath);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
public string GetUrl(int pictureId,string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -130,12 +160,17 @@ public class TelegramStorageProvider(IConfigService configService, ILogger<Teleg
|
||||
}
|
||||
|
||||
string serverUrl = configService["AppSettings:ServerUrl"];
|
||||
return $"{serverUrl}/api/picture/get_telegram_file?fileId={metadata.FileId}";
|
||||
return $"{serverUrl}/api/picture/file/{pictureId}";
|
||||
}
|
||||
catch (JsonException jsonEx)
|
||||
{
|
||||
logger.LogError(jsonEx, "解析 Telegram 元数据以生成 URL 时出错: {StoragePath}", storagePath);
|
||||
return "/images/unavailable.gif";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "生成 Telegram 文件 URL 时出错");
|
||||
return $"/images/unavailable.gif";
|
||||
logger.LogError(ex, "生成 Telegram 文件 URL 时出错: {StoragePath}", storagePath);
|
||||
return "/images/unavailable.gif";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +189,12 @@ public class TelegramStorageProvider(IConfigService configService, ILogger<Teleg
|
||||
throw new ApplicationException("无效的存储路径或元数据");
|
||||
}
|
||||
|
||||
string botToken = configService["Storage:TelegramStorageBotToken"];
|
||||
string botToken = _telegramConfig.BotToken;
|
||||
if (string.IsNullOrEmpty(botToken))
|
||||
{
|
||||
logger.LogError("Telegram BotToken 未在配置中提供,无法下载文件。");
|
||||
throw new InvalidOperationException("Telegram BotToken 未配置。");
|
||||
}
|
||||
|
||||
using var httpClient = CreateHttpClient();
|
||||
var getFileUrl = $"https://api.telegram.org/bot{botToken}/getFile?file_id={metadata.FileId}";
|
||||
@@ -218,10 +258,10 @@ public class TelegramStorageProvider(IConfigService configService, ILogger<Teleg
|
||||
HttpClient client;
|
||||
|
||||
// 检查是否有代理配置
|
||||
string proxyAddress = configService["Storage:TelegramProxyAddress"];
|
||||
string proxyPort = configService["Storage:TelegramProxyPort"];
|
||||
string proxyUsername = configService["Storage:TelegramProxyUsername"];
|
||||
string proxyPassword = configService["Storage:TelegramProxyPassword"];
|
||||
string? proxyAddress = _telegramConfig.ProxyAddress;
|
||||
string? proxyPort = _telegramConfig.ProxyPort;
|
||||
string? proxyUsername = _telegramConfig.ProxyUsername;
|
||||
string? proxyPassword = _telegramConfig.ProxyPassword;
|
||||
|
||||
if (!string.IsNullOrEmpty(proxyAddress) && !string.IsNullOrEmpty(proxyPort) && int.TryParse(proxyPort, out int port))
|
||||
{
|
||||
|
||||
@@ -2,18 +2,43 @@ using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.WebDAV)]
|
||||
public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavStorageProvider> logger) : IStorageProvider
|
||||
public class WebDavStorageConfig
|
||||
{
|
||||
public string ServerUrl { get; set; } = string.Empty;
|
||||
public string BasePath { get; set; } = string.Empty;
|
||||
public string? UserName { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? PublicUrl { get; set; }
|
||||
}
|
||||
|
||||
[StorageProvider(StorageType.WebDAV)]
|
||||
public class WebDavStorageProvider : IStorageProvider
|
||||
{
|
||||
private readonly WebDavStorageConfig _webDavConfig;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ILogger<WebDavStorageProvider> _logger;
|
||||
|
||||
public WebDavStorageProvider(WebDavStorageConfig webDavConfig, IConfigService configService, ILogger<WebDavStorageProvider> logger)
|
||||
{
|
||||
_webDavConfig = webDavConfig;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
|
||||
if (string.IsNullOrEmpty(_webDavConfig.ServerUrl))
|
||||
{
|
||||
_logger.LogError("WebDAV Storage配置不完整 (ServerUrl 是必需的).");
|
||||
throw new InvalidOperationException("WebDAV Storage配置不完整。");
|
||||
}
|
||||
}
|
||||
|
||||
private HttpClient CreateClient()
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var userName = configService["Storage:WebDAVUserName"];
|
||||
var password = configService["Storage:WebDAVPassword"];
|
||||
var userName = _webDavConfig.UserName;
|
||||
var password = _webDavConfig.Password;
|
||||
|
||||
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
|
||||
{
|
||||
@@ -28,8 +53,8 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
{
|
||||
try
|
||||
{
|
||||
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
|
||||
string basePath = configService["Storage:WebDAVBasePath"].Trim('/');
|
||||
string webDavServerUrl = _webDavConfig.ServerUrl.TrimEnd('/');
|
||||
string basePath = _webDavConfig.BasePath?.Trim('/') ?? string.Empty;
|
||||
|
||||
// 创建唯一的文件存储路径
|
||||
string currentDate = DateTime.Now.ToString("yyyy/MM");
|
||||
@@ -58,7 +83,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "上传文件到WebDAV时出错");
|
||||
_logger.LogError(ex, "上传文件到WebDAV时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +95,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return;
|
||||
|
||||
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
|
||||
string webDavServerUrl = _webDavConfig.ServerUrl.TrimEnd('/');
|
||||
var requestUri = $"{webDavServerUrl}/{storagePath}";
|
||||
|
||||
using var client = CreateClient();
|
||||
@@ -83,30 +108,30 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "从WebDAV删除文件时出错");
|
||||
_logger.LogWarning(ex, "从WebDAV删除文件时出错");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
public string GetUrl(int pictureId,string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return "/images/unavailable.gif";
|
||||
|
||||
string publicUrl = configService["Storage:WebDAVPublicUrl"].TrimEnd('/');
|
||||
string serverUrl = configService["AppSettings:ServerUrl"];
|
||||
string? publicUrl = _webDavConfig.PublicUrl?.TrimEnd('/');
|
||||
string serverUrl = _configService["AppSettings:ServerUrl"];
|
||||
|
||||
if (!string.IsNullOrEmpty(publicUrl))
|
||||
{
|
||||
return $"{publicUrl}/{storagePath}";
|
||||
}
|
||||
|
||||
return $"{serverUrl}/api/picture/proxy?path={Uri.EscapeDataString(storagePath)}";
|
||||
return $"{serverUrl}/api/picture/file/{pictureId}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "生成WebDAV文件URL时出错");
|
||||
_logger.LogError(ex, "生成WebDAV文件URL时出错");
|
||||
return "/images/unavailable.gif";
|
||||
}
|
||||
}
|
||||
@@ -120,7 +145,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
throw new ArgumentException("存储路径不能为空");
|
||||
}
|
||||
|
||||
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
|
||||
string webDavServerUrl = _webDavConfig.ServerUrl.TrimEnd('/');
|
||||
|
||||
// 创建临时目录
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "FoxelWebDAVTemp");
|
||||
@@ -147,7 +172,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "从WebDAV下载文件时出错");
|
||||
_logger.LogError(ex, "从WebDAV下载文件时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +184,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
{
|
||||
try
|
||||
{
|
||||
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
|
||||
string webDavServerUrl = _webDavConfig.ServerUrl.TrimEnd('/');
|
||||
var requestUri = $"{webDavServerUrl}/{directoryPath}";
|
||||
using var client = CreateClient();
|
||||
|
||||
@@ -213,7 +238,7 @@ public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "确保WebDAV目录存在时出错");
|
||||
_logger.LogError(ex, "确保WebDAV目录存在时出错");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Reflection;
|
||||
using Foxel.Services.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore; // For IDbContextFactory
|
||||
using System.Text.Json; // For JsonSerializer
|
||||
using Foxel.Services.Storage.Providers; // For specific config classes
|
||||
|
||||
namespace Foxel.Services.Storage;
|
||||
|
||||
@@ -12,11 +14,16 @@ public class StorageService : IStorageService
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<StorageService> _logger;
|
||||
private readonly Dictionary<StorageType, Type> _storageProviders = new();
|
||||
private readonly IDbContextFactory<MyDbContext> _contextFactory;
|
||||
|
||||
public StorageService(IServiceProvider serviceProvider, ILogger<StorageService> logger)
|
||||
public StorageService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<StorageService> logger,
|
||||
IDbContextFactory<MyDbContext> contextFactory)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_contextFactory = contextFactory;
|
||||
RegisterStorageProviders();
|
||||
}
|
||||
|
||||
@@ -27,14 +34,14 @@ public class StorageService : IStorageService
|
||||
{
|
||||
// 获取当前应用程序域中所有程序集
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 扫描每个程序集中的所有类型
|
||||
var types = assembly.GetTypes()
|
||||
.Where(type => type is { IsClass: true, IsAbstract: false } &&
|
||||
.Where(type => type is { IsClass: true, IsAbstract: false } &&
|
||||
type.GetInterfaces().Contains(typeof(IStorageProvider)) &&
|
||||
type.GetCustomAttribute<StorageProviderAttribute>() != null);
|
||||
|
||||
@@ -45,45 +52,123 @@ public class StorageService : IStorageService
|
||||
{
|
||||
// 注册存储提供者类型与对应的存储类型
|
||||
_storageProviders[attribute.StorageType] = type;
|
||||
_logger.LogInformation("已注册存储提供者: {StorageType} -> {ProviderType}", attribute.StorageType, type.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex) // 更具体地捕获加载类型时的异常
|
||||
{
|
||||
_logger.LogWarning(ex, "扫描程序集 {AssemblyName} 时发生类型加载错误。详细信息: {LoaderExceptions}", assembly.FullName,
|
||||
string.Join(", ", ex.LoaderExceptions.Select(e => e?.Message ?? "N/A")));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "扫描程序集 {AssemblyName} 时发生错误", assembly.FullName);
|
||||
// 继续扫描其他程序集
|
||||
}
|
||||
}
|
||||
if (!_storageProviders.Any())
|
||||
{
|
||||
_logger.LogWarning("未能注册任何存储提供者。请检查提供者是否正确标记了 [StorageProvider] 特性并且位于扫描的程序集中。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定存储类型的提供者实例
|
||||
/// 根据 StorageModeId 获取并配置提供者实例
|
||||
/// </summary>
|
||||
private IStorageProvider GetProvider(StorageType storageType)
|
||||
private IStorageProvider GetProvider(int storageModeId)
|
||||
{
|
||||
if (!_storageProviders.TryGetValue(storageType, out var providerType))
|
||||
using var context = _contextFactory.CreateDbContext();
|
||||
var storageMode = context.StorageModes
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(sm => sm.Id == storageModeId);
|
||||
|
||||
if (storageMode == null)
|
||||
{
|
||||
throw new ArgumentException($"未找到存储类型 {storageType} 的提供者");
|
||||
_logger.LogError("ID 为 {StorageModeId} 的 StorageMode 未找到。", storageModeId);
|
||||
throw new ArgumentException($"ID 为 {storageModeId} 的 StorageMode 未找到。");
|
||||
}
|
||||
|
||||
return (IStorageProvider)_serviceProvider.GetRequiredService(providerType);
|
||||
if (!storageMode.IsEnabled)
|
||||
{
|
||||
_logger.LogWarning("StorageMode {StorageModeId} ({StorageModeName}) 未启用。", storageModeId, storageMode.Name);
|
||||
throw new InvalidOperationException($"StorageMode '{storageMode.Name}' (ID: {storageModeId}) 未启用。");
|
||||
}
|
||||
|
||||
if (!_storageProviders.TryGetValue(storageMode.StorageType, out var providerType))
|
||||
{
|
||||
_logger.LogError("未找到 StorageType {StorageType} (来自 StorageMode {StorageModeId}) 的已注册提供者。", storageMode.StorageType, storageModeId);
|
||||
throw new ArgumentException($"未找到 StorageType {storageMode.StorageType} 的提供者。");
|
||||
}
|
||||
|
||||
object specificConfig = DeserializeProviderConfig(storageMode.StorageType, storageMode.ConfigurationJson, storageMode.Name);
|
||||
|
||||
try
|
||||
{
|
||||
return (IStorageProvider)ActivatorUtilities.CreateInstance(_serviceProvider, providerType, specificConfig);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "为 StorageMode {StorageModeName} (ID: {StorageModeId}, Type: {StorageType}) 创建提供者 {ProviderType} 的实例失败。",
|
||||
storageMode.Name, storageModeId, storageMode.StorageType, providerType.FullName);
|
||||
throw new InvalidOperationException($"创建存储提供者 '{providerType.Name}' 失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private object DeserializeProviderConfig(StorageType storageType, string? jsonConfig, string storageModeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jsonConfig))
|
||||
{
|
||||
_logger.LogError("StorageMode '{StorageModeName}' (Type: {StorageType}) 的 ConfigurationJson 为空或空白。", storageModeName, storageType);
|
||||
throw new InvalidOperationException($"StorageMode '{storageModeName}' (Type: {storageType}) 的配置 (ConfigurationJson) 为空。");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (storageType)
|
||||
{
|
||||
case StorageType.Local:
|
||||
return JsonSerializer.Deserialize<LocalStorageConfig>(jsonConfig)
|
||||
?? throw new JsonException($"无法反序列化 LocalStorageConfig。JSON: {jsonConfig}");
|
||||
case StorageType.Telegram:
|
||||
return JsonSerializer.Deserialize<TelegramStorageConfig>(jsonConfig)
|
||||
?? throw new JsonException($"无法反序列化 TelegramStorageConfig。JSON: {jsonConfig}");
|
||||
case StorageType.S3:
|
||||
return JsonSerializer.Deserialize<S3StorageConfig>(jsonConfig)
|
||||
?? throw new JsonException($"无法反序列化 S3StorageConfig。JSON: {jsonConfig}");
|
||||
case StorageType.Cos:
|
||||
return JsonSerializer.Deserialize<CosStorageConfig>(jsonConfig)
|
||||
?? throw new JsonException($"无法反序列化 CosStorageConfig。JSON: {jsonConfig}");
|
||||
case StorageType.WebDAV:
|
||||
return JsonSerializer.Deserialize<WebDavStorageConfig>(jsonConfig)
|
||||
?? throw new JsonException($"无法反序列化 WebDavStorageConfig。JSON: {jsonConfig}");
|
||||
default:
|
||||
_logger.LogError("不支持的存储类型配置反序列化: {StorageType} (来自 StorageMode '{StorageModeName}')", storageType, storageModeName);
|
||||
throw new NotSupportedException($"不支持 StorageType {storageType} 的配置反序列化。");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "反序列化 StorageMode '{StorageModeName}' (Type: {StorageType}) 的配置失败。JSON: {JsonConfig}", storageModeName, storageType, jsonConfig);
|
||||
throw new InvalidOperationException($"StorageMode '{storageModeName}' (Type: {storageType}) 的配置格式无效。", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定存储类型上执行操作
|
||||
/// 在指定存储模式上执行操作
|
||||
/// </summary>
|
||||
public async Task<TResult> ExecuteAsync<TResult>(StorageType storageType, Func<IStorageProvider, Task<TResult>> operation)
|
||||
public async Task<TResult> ExecuteAsync<TResult>(int storageModeId, Func<IStorageProvider, Task<TResult>> operation)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
var provider = GetProvider(storageModeId);
|
||||
return await operation(provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定存储类型上执行无返回值的操作
|
||||
/// 在指定存储模式上执行无返回值的操作
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(StorageType storageType, Func<IStorageProvider, Task> operation)
|
||||
public async Task ExecuteAsync(int storageModeId, Func<IStorageProvider, Task> operation)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
var provider = GetProvider(storageModeId);
|
||||
await operation(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user