mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-08-28 19:46:41 +08:00
feat(storage): add support for Tencent Cloud COS
This commit is contained in:
@@ -24,6 +24,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<LocalStorageProvider>();
|
services.AddSingleton<LocalStorageProvider>();
|
||||||
services.AddSingleton<TelegramStorageProvider>();
|
services.AddSingleton<TelegramStorageProvider>();
|
||||||
services.AddSingleton<S3StorageProvider>();
|
services.AddSingleton<S3StorageProvider>();
|
||||||
|
services.AddSingleton<CosStorageProvider>();
|
||||||
services.AddSingleton<IStorageProviderFactory, StorageProviderFactory>();
|
services.AddSingleton<IStorageProviderFactory, StorageProviderFactory>();
|
||||||
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.3.1" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.3.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
||||||
|
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.46" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public enum StorageType
|
|||||||
Local = 0,
|
Local = 0,
|
||||||
Telegram = 1,
|
Telegram = 1,
|
||||||
S3 = 2,
|
S3 = 2,
|
||||||
|
Cos = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Picture : BaseModel
|
public class Picture : BaseModel
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
using Foxel.Services.Interface;
|
||||||
|
using COSXML;
|
||||||
|
using COSXML.Auth;
|
||||||
|
using COSXML.Model.Object;
|
||||||
|
using COSXML.Model.Bucket;
|
||||||
|
using COSXML.Transfer;
|
||||||
|
using COSXML.CosException;
|
||||||
|
using COSXML.Model.Tag;
|
||||||
|
|
||||||
|
namespace Foxel.Services.StorageProvider;
|
||||||
|
|
||||||
|
public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider
|
||||||
|
{
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
|
||||||
|
public CustomQCloudCredentialProvider(IConfigService configService)
|
||||||
|
: base(null, null, 0L, null)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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,
|
||||||
|
String.Format("{0};{1}", tmpStartTime, tmpExpiredTime), tmpToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"刷新临时密钥时出错: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CosStorageProvider : IStorageProvider
|
||||||
|
{
|
||||||
|
private readonly string _secretId;
|
||||||
|
private readonly string _secretKey;
|
||||||
|
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;
|
||||||
|
_secretId = configService["Storage:CosStorageSecretId"];
|
||||||
|
_secretKey = configService["Storage:CosStorageSecretKey"];
|
||||||
|
_bucketName = configService["Storage:CosStorageBucketName"];
|
||||||
|
_region = configService["Storage:CosStorageRegion"];
|
||||||
|
_cdnUrl = configService["Storage:CosStorageCdnUrl"] ?? string.Empty;
|
||||||
|
|
||||||
|
// 检查桶是否为公开读取(从配置获取)
|
||||||
|
bool.TryParse(configService["Storage:CosStoragePublicRead"] ?? "false", 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
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
var result = 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);
|
||||||
|
var result = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using Foxel.Services.Interface;
|
using Foxel.Services.Interface;
|
||||||
using Foxel.Services.StorageProvider;
|
using Foxel.Services.StorageProvider;
|
||||||
|
using Pgvector.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxel.Services;
|
namespace Foxel.Services;
|
||||||
|
|
||||||
public class StorageProviderFactory(
|
public class StorageProviderFactory(
|
||||||
LocalStorageProvider localStorageProvider,
|
LocalStorageProvider localStorageProvider,
|
||||||
TelegramStorageProvider telegramStorageProvider,
|
TelegramStorageProvider telegramStorageProvider,
|
||||||
|
CosStorageProvider cosStorageProvider,
|
||||||
S3StorageProvider s3StorageProvider) : IStorageProviderFactory
|
S3StorageProvider s3StorageProvider) : IStorageProviderFactory
|
||||||
{
|
{
|
||||||
public IStorageProvider GetProvider(StorageType storageType)
|
public IStorageProvider GetProvider(StorageType storageType)
|
||||||
@@ -16,6 +18,7 @@ public class StorageProviderFactory(
|
|||||||
StorageType.Local => localStorageProvider,
|
StorageType.Local => localStorageProvider,
|
||||||
StorageType.Telegram => telegramStorageProvider,
|
StorageType.Telegram => telegramStorageProvider,
|
||||||
StorageType.S3 => s3StorageProvider,
|
StorageType.S3 => s3StorageProvider,
|
||||||
|
StorageType.Cos => cosStorageProvider,
|
||||||
_ => throw new ArgumentException($"不支持的存储类型: {storageType}")
|
_ => throw new ArgumentException($"不支持的存储类型: {storageType}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import AllImages from '../pages/allImages/Index';
|
|||||||
import Albums from '../pages/albums/Index';
|
import Albums from '../pages/albums/Index';
|
||||||
import AlbumDetail from '../pages/albumDetail/Index';
|
import AlbumDetail from '../pages/albumDetail/Index';
|
||||||
import Favorites from '../pages/favorites/Index';
|
import Favorites from '../pages/favorites/Index';
|
||||||
import Upload from '../pages/upload/Index';
|
|
||||||
import Settings from '../pages/settings/Index';
|
import Settings from '../pages/settings/Index';
|
||||||
import BackgroundTasks from '../pages/backgroundTasks/Index';
|
import BackgroundTasks from '../pages/backgroundTasks/Index';
|
||||||
import PixHub from '../pages/pixHub/Index';
|
import PixHub from '../pages/pixHub/Index';
|
||||||
@@ -111,16 +110,6 @@ const routes: RouteConfig[] = [
|
|||||||
title: '设置'
|
title: '设置'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'upload',
|
|
||||||
key: 'upload',
|
|
||||||
label: '上传',
|
|
||||||
element: <Upload />,
|
|
||||||
hideInMenu: true,
|
|
||||||
breadcrumb: {
|
|
||||||
title: '上传'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ const SystemConfig: React.FC = () => {
|
|||||||
// 存储类型选项
|
// 存储类型选项
|
||||||
const storageOptions = [
|
const storageOptions = [
|
||||||
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
|
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
|
||||||
{ value: 'Telegram', label: 'Telegram存储', icon: <CloudOutlined style={{ color: '#0088cc' }} /> },
|
{ value: 'Telegram', label: 'Telegram 频道', icon: <CloudOutlined style={{ color: '#0088cc' }} /> },
|
||||||
{ value: 'S3', label: 'S3兼容存储', icon: <CloudServerOutlined style={{ color: '#ff9900' }} /> },
|
{ value: 'S3', label: '亚马逊 S3', icon: <CloudServerOutlined style={{ color: '#ff9900' }} /> },
|
||||||
|
{ value: 'Cos', label: '腾讯云 COS', icon: <CloudServerOutlined style={{ color: '#00a4ff' }} /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -292,6 +293,30 @@ const SystemConfig: React.FC = () => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{storageType === 'Cos' && (
|
||||||
|
<ConfigGroup
|
||||||
|
groupName="Storage"
|
||||||
|
configs={{
|
||||||
|
"CosStorageSecretId": configs.Storage?.CosStorageSecretId || '',
|
||||||
|
"CosStorageSecretKey": configs.Storage?.CosStorageSecretKey || '',
|
||||||
|
"CosStorageToken": configs.Storage?.CosStorageToken || '',
|
||||||
|
"CosStorageBucketName": configs.Storage?.CosStorageBucketName || '',
|
||||||
|
"CosStorageRegion": configs.Storage?.CosStorageRegion || '',
|
||||||
|
"CosStorageCdnUrl": configs.Storage?.CosStorageCdnUrl || '',
|
||||||
|
}}
|
||||||
|
onSave={handleSaveConfig}
|
||||||
|
descriptions={{
|
||||||
|
"CosStorageSecretId": '腾讯云COS密钥ID',
|
||||||
|
"CosStorageSecretKey": '腾讯云COS私有密钥',
|
||||||
|
"CosStorageToken": '腾讯云COS临时令牌(可选)',
|
||||||
|
"CosStorageBucketName": 'COS存储桶名称',
|
||||||
|
"CosStorageRegion": 'COS区域 (例如:ap-shanghai)',
|
||||||
|
"CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
|
||||||
|
}}
|
||||||
|
isMobile={isMobile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Typography } from 'antd';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
|
||||||
|
|
||||||
function Upload() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title level={2} style={{
|
|
||||||
margin: 0,
|
|
||||||
marginBottom: 20,
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
fontSize: 32,
|
|
||||||
background: 'linear-gradient(120deg, #000000, #444444)',
|
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
}}>上传图片</Title>
|
|
||||||
{/* 上传表单 */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Upload;
|
|
||||||
Reference in New Issue
Block a user