mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-11 15:33:18 +08:00
refactor(services): split services into separate folders and update namespaces
This commit is contained in:
30
Services/Storage/IStorageProvider.cs
Normal file
30
Services/Storage/IStorageProvider.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Foxel.Services.Storage;
|
||||
|
||||
public interface IStorageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存文件
|
||||
/// </summary>
|
||||
Task<string> SaveAsync(Stream fileStream, string fileName, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
Task DeleteAsync(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件URL
|
||||
/// </summary>
|
||||
string GetUrl(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件到本地临时目录
|
||||
/// </summary>
|
||||
/// <param name="storagePath">存储路径</param>
|
||||
/// <returns>本地文件路径</returns>
|
||||
Task<string> DownloadFileAsync(string storagePath)
|
||||
{
|
||||
// 默认实现 - 子类应重写此方法
|
||||
throw new NotImplementedException("此存储提供者不支持下载文件功能");
|
||||
}
|
||||
}
|
||||
49
Services/Storage/IStorageService.cs
Normal file
49
Services/Storage/IStorageService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Foxel.Services.Attributes;
|
||||
|
||||
namespace Foxel.Services.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 统一的存储服务接口
|
||||
/// </summary>
|
||||
public interface IStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据存储类型获取对应的存储提供者
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <returns>存储提供者实例</returns>
|
||||
IStorageProvider GetProvider(StorageType storageType);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型保存文件
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <param name="contentType">内容类型</param>
|
||||
/// <returns>存储路径</returns>
|
||||
Task<string> SaveAsync(StorageType storageType, Stream fileStream, string fileName, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型删除文件
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="storagePath">存储路径</param>
|
||||
Task DeleteAsync(StorageType storageType, string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型获取文件URL
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="storagePath">存储路径</param>
|
||||
/// <returns>文件URL</returns>
|
||||
string GetUrl(StorageType storageType, string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型下载文件
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
/// <param name="storagePath">存储路径</param>
|
||||
/// <returns>本地文件路径</returns>
|
||||
Task<string> DownloadFileAsync(StorageType storageType, string storagePath);
|
||||
}
|
||||
234
Services/Storage/Providers/CosStorageProvider.cs
Normal file
234
Services/Storage/Providers/CosStorageProvider.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
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;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
public CustomQCloudCredentialProvider(IConfigService configService)
|
||||
: base(null, null, 0L, null)
|
||||
{
|
||||
_configService = configService;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public sealed override void Refresh()
|
||||
{
|
||||
try
|
||||
{
|
||||
string tmpSecretId = _configService["Storage:CosStorageSecretId"];
|
||||
string tmpSecretKey = _configService["Storage:CosStorageSecretKey"];
|
||||
string tmpToken = _configService["Storage:CosStorageToken"];
|
||||
long tmpStartTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
long tmpExpiredTime = tmpStartTime + 7200;
|
||||
SetQCloudCredential(tmpSecretId, tmpSecretKey,
|
||||
$"{tmpStartTime};{tmpExpiredTime}", tmpToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"刷新临时密钥时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
[StorageProvider(StorageType.Cos)]
|
||||
public class CosStorageProvider : IStorageProvider
|
||||
{
|
||||
private readonly string _bucketName;
|
||||
private readonly string _region;
|
||||
private readonly string _cdnUrl;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly CosXml _cosXmlClient;
|
||||
|
||||
private readonly bool _isPublicRead;
|
||||
|
||||
public CosStorageProvider(IConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
_bucketName = configService["Storage:CosStorageBucketName"];
|
||||
_region = configService["Storage:CosStorageRegion"];
|
||||
_cdnUrl = configService["Storage:CosStorageCdnUrl"];
|
||||
|
||||
// 检查桶是否为公开读取(从配置获取)
|
||||
bool.TryParse(configService["Storage:CosStoragePublicRead"], out _isPublicRead);
|
||||
|
||||
// 在构造函数中初始化客户端,作为单例使用
|
||||
_cosXmlClient = CreateClient();
|
||||
}
|
||||
|
||||
private CosXml CreateClient()
|
||||
{
|
||||
// 优化配置:启用HTTPS和日志
|
||||
var config = new CosXmlConfig.Builder()
|
||||
.IsHttps(true) // 设置默认HTTPS请求
|
||||
.SetRegion(_region)
|
||||
.SetDebugLog(true) // 显示日志
|
||||
.Build();
|
||||
|
||||
// 使用自定义凭证提供者,支持持续更新临时密钥
|
||||
var cosCredentialProvider = new CustomQCloudCredentialProvider(_configService);
|
||||
|
||||
return new CosXmlServer(config, cosCredentialProvider);
|
||||
}
|
||||
|
||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建唯一的文件存储路径
|
||||
string currentDate = DateTime.Now.ToString("yyyy/MM");
|
||||
string ext = Path.GetExtension(fileName);
|
||||
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}";
|
||||
|
||||
// 创建临时文件
|
||||
string tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await using (var fileStream2 = new FileStream(tempPath, FileMode.Create))
|
||||
{
|
||||
await fileStream.CopyToAsync(fileStream2);
|
||||
}
|
||||
|
||||
var transferConfig = new TransferConfig();
|
||||
var transferManager = new TransferManager(_cosXmlClient, transferConfig);
|
||||
var uploadTask = new COSXMLUploadTask(_bucketName, objectKey);
|
||||
uploadTask.SetSrcPath(tempPath);
|
||||
await transferManager.UploadAsync(uploadTask);
|
||||
return objectKey;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保临时文件被删除
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
||||
throw;
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"上传文件到腾讯云COS时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return;
|
||||
|
||||
var request = new DeleteObjectRequest(_bucketName, storagePath);
|
||||
await Task.Run(() => _cosXmlClient.DeleteObject(request));
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"从腾讯云COS删除文件时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return "/images/unavailable.gif";
|
||||
|
||||
// 优先使用CDN
|
||||
if (!string.IsNullOrEmpty(_cdnUrl))
|
||||
return $"{_cdnUrl}/{storagePath}";
|
||||
|
||||
// 公开读取的桶可直接访问
|
||||
if (_isPublicRead)
|
||||
return $"https://{_bucketName}.cos.{_region}.myqcloud.com/{storagePath}";
|
||||
|
||||
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)
|
||||
{
|
||||
Console.WriteLine($"生成腾讯云COS文件URL时出错: {ex.Message}");
|
||||
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 fileName = Path.GetFileName(storagePath);
|
||||
string localFilePath = Path.Combine(tempDir, fileName);
|
||||
var transferConfig = new TransferConfig();
|
||||
var transferManager = new TransferManager(_cosXmlClient, transferConfig);
|
||||
var downloadTask = new COSXMLDownloadTask(_bucketName, storagePath, tempDir, fileName);
|
||||
await transferManager.DownloadAsync(downloadTask);
|
||||
return localFilePath;
|
||||
}
|
||||
catch (CosClientException clientEx)
|
||||
{
|
||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
||||
throw;
|
||||
}
|
||||
catch (CosServerException serverEx)
|
||||
{
|
||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"从腾讯云COS下载文件时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Services/Storage/Providers/LocalStorageProvider.cs
Normal file
52
Services/Storage/Providers/LocalStorageProvider.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Configuration;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.Local)]
|
||||
public class LocalStorageProvider(IConfigService config) : IStorageProvider
|
||||
{
|
||||
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
||||
private readonly string _serverUrl = config["AppSettings:ServerUrl"];
|
||||
|
||||
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);
|
||||
|
||||
string ext = Path.GetExtension(fileName);
|
||||
string newFileName = $"{Guid.NewGuid()}{ext}";
|
||||
string filePath = Path.Combine(folder, newFileName);
|
||||
|
||||
await using var output = new FileStream(filePath, FileMode.Create);
|
||||
await fileStream.CopyToAsync(output);
|
||||
return $"/Uploads/{currentDate}/{newFileName}";
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string storagePath)
|
||||
{
|
||||
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), storagePath.TrimStart('/'));
|
||||
if (File.Exists(fullPath))
|
||||
File.Delete(fullPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetUrl(string? storagePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return $"/images/unavailable.gif";
|
||||
return $"{_serverUrl}{storagePath}";
|
||||
}
|
||||
|
||||
|
||||
public Task<string> DownloadFileAsync(string storagePath)
|
||||
{
|
||||
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), storagePath.TrimStart('/'));
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
throw new FileNotFoundException($"找不到文件: {fullPath}");
|
||||
}
|
||||
return Task.FromResult(fullPath);
|
||||
}
|
||||
}
|
||||
177
Services/Storage/Providers/S3StorageProvider.cs
Normal file
177
Services/Storage/Providers/S3StorageProvider.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Amazon.S3.Transfer;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Configuration;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.S3)]
|
||||
public class S3StorageProvider : IStorageProvider
|
||||
{
|
||||
private readonly string _accessKey;
|
||||
private readonly string _secretKey;
|
||||
private readonly string _bucketName;
|
||||
private readonly string _region;
|
||||
private readonly string _endpoint;
|
||||
private readonly bool _usePathStyleUrls;
|
||||
private readonly string _cdnUrl;
|
||||
|
||||
public S3StorageProvider(IConfigService configService)
|
||||
{
|
||||
_accessKey = configService["Storage:S3StorageAccessKey"];
|
||||
_secretKey = configService["Storage:S3StorageSecretKey"];
|
||||
_bucketName = configService["Storage:S3StorageBucketName"];
|
||||
_region = configService["Storage:S3StorageRegion"];
|
||||
_cdnUrl = configService["Storage:S3StorageCdnUrl"];
|
||||
_endpoint = configService["Storage:S3StorageEndpoint"];
|
||||
_usePathStyleUrls = bool.TryParse(configService["Storage:S3StorageUsePathStyleUrls"], out var usePathStyle) && usePathStyle;
|
||||
}
|
||||
|
||||
private AmazonS3Client CreateClient()
|
||||
{
|
||||
var config = new AmazonS3Config
|
||||
{
|
||||
ServiceURL = _endpoint,
|
||||
UseHttp = !_endpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase),
|
||||
ForcePathStyle = _usePathStyleUrls
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_region) && _endpoint.Contains("amazonaws.com"))
|
||||
{
|
||||
config.RegionEndpoint = Amazon.RegionEndpoint.GetBySystemName(_region);
|
||||
}
|
||||
|
||||
return new AmazonS3Client(
|
||||
_accessKey,
|
||||
_secretKey,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建唯一的文件存储路径
|
||||
string currentDate = DateTime.Now.ToString("yyyy/MM");
|
||||
string ext = Path.GetExtension(fileName);
|
||||
string objectKey = $"{currentDate}/{Guid.NewGuid()}{ext}";
|
||||
|
||||
using var client = CreateClient();
|
||||
using var transferUtility = new TransferUtility(client);
|
||||
|
||||
var uploadRequest = new TransferUtilityUploadRequest
|
||||
{
|
||||
InputStream = fileStream,
|
||||
Key = objectKey,
|
||||
BucketName = _bucketName,
|
||||
ContentType = contentType
|
||||
};
|
||||
|
||||
await transferUtility.UploadAsync(uploadRequest);
|
||||
|
||||
// 返回文件的路径
|
||||
return objectKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"上传文件到S3时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return;
|
||||
|
||||
using var client = CreateClient();
|
||||
var deleteRequest = new DeleteObjectRequest
|
||||
{
|
||||
BucketName = _bucketName,
|
||||
Key = storagePath
|
||||
};
|
||||
|
||||
await client.DeleteObjectAsync(deleteRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"从S3删除文件时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(storagePath))
|
||||
return "/images/unavailable.gif";
|
||||
|
||||
// 如果配置了CDN URL,使用CDN
|
||||
if (!string.IsNullOrEmpty(_cdnUrl))
|
||||
{
|
||||
return $"{_cdnUrl}/{storagePath}";
|
||||
}
|
||||
|
||||
// 否则使用S3直链或生成预签名URL
|
||||
using var client = CreateClient();
|
||||
var request = new GetPreSignedUrlRequest
|
||||
{
|
||||
BucketName = _bucketName,
|
||||
Key = storagePath,
|
||||
Expires = DateTime.UtcNow.AddHours(1) // URL有效期1小时
|
||||
};
|
||||
|
||||
return client.GetPreSignedURL(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"生成S3文件URL时出错: {ex.Message}");
|
||||
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(), "FoxelS3Temp");
|
||||
if (!Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
}
|
||||
|
||||
// 创建临时文件名
|
||||
string fileName = Path.GetFileName(storagePath);
|
||||
string tempFilePath = Path.Combine(tempDir, fileName);
|
||||
|
||||
// 下载文件
|
||||
using var client = CreateClient();
|
||||
var request = new GetObjectRequest
|
||||
{
|
||||
BucketName = _bucketName,
|
||||
Key = storagePath
|
||||
};
|
||||
|
||||
using var response = await client.GetObjectAsync(request);
|
||||
await using var fileStream = new FileStream(tempFilePath, FileMode.Create);
|
||||
await response.ResponseStream.CopyToAsync(fileStream);
|
||||
|
||||
return tempFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"从S3下载文件时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
234
Services/Storage/Providers/TelegramStorageProvider.cs
Normal file
234
Services/Storage/Providers/TelegramStorageProvider.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Configuration;
|
||||
|
||||
namespace Foxel.Services.Storage.Providers;
|
||||
|
||||
[StorageProvider(StorageType.Telegram)]
|
||||
public class TelegramStorageProvider(IConfigService configService) : IStorageProvider
|
||||
{
|
||||
private readonly string _botToken = configService["Storage:TelegramStorageBotToken"];
|
||||
private readonly string _chatId = configService["Storage:TelegramStorageChatId"];
|
||||
private readonly string _serverUrl = configService["AppSettings:ServerUrl"];
|
||||
|
||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var formData = new MultipartFormDataContent();
|
||||
formData.Add(new StringContent(_chatId), "chat_id");
|
||||
var safeFileName = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (safeFileName.Length > 100)
|
||||
safeFileName = safeFileName.Substring(0, 100);
|
||||
formData.Add(new StringContent(safeFileName), "caption");
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var fileContent = new StreamContent(memoryStream);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
|
||||
formData.Add(fileContent, "document", fileName);
|
||||
|
||||
try
|
||||
{
|
||||
var response =
|
||||
await httpClient.PostAsync($"https://api.telegram.org/bot{_botToken}/sendDocument", formData);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"Telegram API 请求失败: 状态码: {response.StatusCode}, 响应: {errorContent}");
|
||||
throw new ApplicationException($"Telegram API 请求失败: {response.StatusCode}");
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var responseObj = JsonSerializer.Deserialize<TelegramResponse>(responseContent);
|
||||
if (responseObj == null || !responseObj.Ok || responseObj.Result?.Document == null)
|
||||
{
|
||||
throw new ApplicationException($"上传文件到 Telegram 失败: {responseContent}");
|
||||
}
|
||||
|
||||
var fileId = responseObj.Result.Document.FileId;
|
||||
|
||||
var metadata = new TelegramFileMetadata
|
||||
{
|
||||
FileId = fileId,
|
||||
FileUniqueId = responseObj.Result.Document.FileUniqueId,
|
||||
MessageId = responseObj.Result.MessageId,
|
||||
ChatId = _chatId,
|
||||
OriginalFileName = fileName,
|
||||
UploadDate = DateTime.UtcNow,
|
||||
MimeType = contentType
|
||||
};
|
||||
return JsonSerializer.Serialize(metadata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"发送文件到 Telegram 时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metadata = JsonSerializer.Deserialize<TelegramFileMetadata>(storagePath);
|
||||
if (metadata == null || string.IsNullOrEmpty(metadata.ChatId) || metadata.MessageId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var url =
|
||||
$"https://api.telegram.org/bot{_botToken}/deleteMessage?chat_id={metadata.ChatId}&message_id={metadata.MessageId}";
|
||||
await httpClient.GetAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"删除 Telegram 文件时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUrl(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metadata = JsonSerializer.Deserialize<TelegramFileMetadata>(storagePath);
|
||||
if (metadata == null || string.IsNullOrEmpty(metadata.FileId))
|
||||
{
|
||||
throw new ApplicationException("无效的存储路径或元数据");
|
||||
}
|
||||
|
||||
return $"{_serverUrl}/api/picture/get_telegram_file?fileId={metadata.FileId}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"生成 Telegram 文件 URL 时出错: {ex.Message}");
|
||||
return $"/images/unavailable.gif";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载Telegram文件到临时目录
|
||||
/// </summary>
|
||||
/// <param name="storagePath">存储的元数据JSON</param>
|
||||
/// <returns>临时文件的完整路径</returns>
|
||||
public async Task<string> DownloadFileAsync(string storagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metadata = JsonSerializer.Deserialize<TelegramFileMetadata>(storagePath);
|
||||
if (metadata == null || string.IsNullOrEmpty(metadata.FileId))
|
||||
{
|
||||
throw new ApplicationException("无效的存储路径或元数据");
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var getFileUrl = $"https://api.telegram.org/bot{_botToken}/getFile?file_id={metadata.FileId}";
|
||||
var getFileResponse = await httpClient.GetAsync(getFileUrl);
|
||||
|
||||
if (!getFileResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await getFileResponse.Content.ReadAsStringAsync();
|
||||
throw new ApplicationException($"获取 Telegram 文件路径失败: {getFileResponse.StatusCode}, {errorContent}");
|
||||
}
|
||||
|
||||
var getFileContent = await getFileResponse.Content.ReadAsStringAsync();
|
||||
var getFileResult = JsonSerializer.Deserialize<TelegramGetFileResponse>(getFileContent);
|
||||
if (getFileResult == null || !getFileResult.Ok || string.IsNullOrEmpty(getFileResult.Result?.FilePath))
|
||||
{
|
||||
throw new ApplicationException("无法解析 Telegram 文件路径");
|
||||
}
|
||||
|
||||
var filePath = getFileResult.Result.FilePath;
|
||||
var fileUrl = $"https://api.telegram.org/file/bot{_botToken}/{filePath}";
|
||||
|
||||
var fileResponse = await httpClient.GetAsync(fileUrl);
|
||||
if (!fileResponse.IsSuccessStatusCode)
|
||||
{
|
||||
throw new ApplicationException($"下载 Telegram 文件失败: {fileResponse.StatusCode}");
|
||||
}
|
||||
|
||||
// 创建临时目录
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "FoxelTelegramTemp");
|
||||
if (!Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
}
|
||||
|
||||
// 创建临时文件名 - 使用原始文件名或使用临时文件名
|
||||
string tempFileName = !string.IsNullOrEmpty(metadata.OriginalFileName)
|
||||
? Path.GetFileName(metadata.OriginalFileName)
|
||||
: $"{Guid.NewGuid()}{Path.GetExtension(filePath)}";
|
||||
string tempFilePath = Path.Combine(tempDir, tempFileName);
|
||||
|
||||
// 保存文件
|
||||
await using var fileStream = await fileResponse.Content.ReadAsStreamAsync();
|
||||
await using var outputStream = new FileStream(tempFilePath, FileMode.Create);
|
||||
await fileStream.CopyToAsync(outputStream);
|
||||
|
||||
return tempFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"下载 Telegram 文件时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于处理 Telegram API 响应的辅助类
|
||||
private class TelegramResponse
|
||||
{
|
||||
[JsonPropertyName("ok")] public bool Ok { get; set; }
|
||||
|
||||
[JsonPropertyName("result")] public TelegramResult? Result { get; set; }
|
||||
}
|
||||
|
||||
private class TelegramResult
|
||||
{
|
||||
[JsonPropertyName("message_id")] public int MessageId { get; set; }
|
||||
|
||||
[JsonPropertyName("document")] public TelegramDocument? Document { get; set; }
|
||||
}
|
||||
|
||||
private class TelegramDocument
|
||||
{
|
||||
[JsonPropertyName("file_id")] public string FileId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_unique_id")] public string FileUniqueId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_name")] public string? FileName { get; set; }
|
||||
|
||||
[JsonPropertyName("mime_type")] public string? MimeType { get; set; }
|
||||
|
||||
[JsonPropertyName("file_size")] public int FileSize { get; set; }
|
||||
}
|
||||
|
||||
// 存储关于上传文件的元数据
|
||||
private class TelegramFileMetadata
|
||||
{
|
||||
public string FileId { get; set; } = string.Empty;
|
||||
public string FileUniqueId { get; set; } = string.Empty;
|
||||
public int MessageId { get; set; }
|
||||
public string ChatId { get; set; } = string.Empty;
|
||||
public string OriginalFileName { get; set; } = string.Empty;
|
||||
public DateTime UploadDate { get; set; }
|
||||
public string? MimeType { get; set; }
|
||||
}
|
||||
|
||||
private class TelegramGetFileResponse
|
||||
{
|
||||
[JsonPropertyName("ok")] public bool Ok { get; set; }
|
||||
|
||||
[JsonPropertyName("result")] public TelegramFileResult? Result { get; set; }
|
||||
}
|
||||
|
||||
private class TelegramFileResult
|
||||
{
|
||||
[JsonPropertyName("file_path")] public string? FilePath { get; set; }
|
||||
}
|
||||
}
|
||||
33
Services/Storage/StorageProviderAttribute.cs
Normal file
33
Services/Storage/StorageProviderAttribute.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Foxel.Models.DataBase;
|
||||
|
||||
namespace Foxel.Services.Attributes;
|
||||
|
||||
public enum StorageType
|
||||
{
|
||||
Local = 0,
|
||||
Telegram = 1,
|
||||
S3 = 2,
|
||||
Cos = 3,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 标记存储提供者类的特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class StorageProviderAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储类型
|
||||
/// </summary>
|
||||
public StorageType StorageType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="storageType">存储类型</param>
|
||||
public StorageProviderAttribute(StorageType storageType)
|
||||
{
|
||||
StorageType = storageType;
|
||||
}
|
||||
}
|
||||
104
Services/Storage/StorageService.cs
Normal file
104
Services/Storage/StorageService.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Reflection;
|
||||
using Foxel.Services.Attributes;
|
||||
|
||||
namespace Foxel.Services.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 统一的存储服务实现
|
||||
/// </summary>
|
||||
public class StorageService : IStorageService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Dictionary<StorageType, Type> _storageProviders = new();
|
||||
|
||||
public StorageService(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
RegisterStorageProviders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用反射扫描和注册所有标记了StorageProviderAttribute的存储提供者
|
||||
/// </summary>
|
||||
private void RegisterStorageProviders()
|
||||
{
|
||||
// 获取当前应用程序域中所有程序集
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 扫描每个程序集中的所有类型
|
||||
var types = assembly.GetTypes()
|
||||
.Where(type => type is { IsClass: true, IsAbstract: false } &&
|
||||
type.GetInterfaces().Contains(typeof(IStorageProvider)) &&
|
||||
type.GetCustomAttribute<StorageProviderAttribute>() != null);
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<StorageProviderAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
// 注册存储提供者类型与对应的存储类型
|
||||
_storageProviders[attribute.StorageType] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"扫描程序集 {assembly.FullName} 出错: {ex.Message}");
|
||||
// 继续扫描其他程序集
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定存储类型的提供者实例
|
||||
/// </summary>
|
||||
public IStorageProvider GetProvider(StorageType storageType)
|
||||
{
|
||||
if (!_storageProviders.TryGetValue(storageType, out var providerType))
|
||||
{
|
||||
throw new ArgumentException($"未找到存储类型 {storageType} 的提供者");
|
||||
}
|
||||
|
||||
return (IStorageProvider)_serviceProvider.GetRequiredService(providerType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型保存文件
|
||||
/// </summary>
|
||||
public Task<string> SaveAsync(StorageType storageType, Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
return provider.SaveAsync(fileStream, fileName, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型删除文件
|
||||
/// </summary>
|
||||
public Task DeleteAsync(StorageType storageType, string storagePath)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
return provider.DeleteAsync(storagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型获取文件URL
|
||||
/// </summary>
|
||||
public string GetUrl(StorageType storageType, string storagePath)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
return provider.GetUrl(storagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定存储类型下载文件
|
||||
/// </summary>
|
||||
public Task<string> DownloadFileAsync(StorageType storageType, string storagePath)
|
||||
{
|
||||
var provider = GetProvider(storageType);
|
||||
return provider.DownloadFileAsync(storagePath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user