mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-10 17:43:35 +08:00
feat(storage): add S3-compatible storage support
This commit is contained in:
@@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddHostedService<QueuedHostedService>();
|
||||
services.AddSingleton<LocalStorageProvider>();
|
||||
services.AddSingleton<TelegramStorageProvider>();
|
||||
services.AddSingleton<S3StorageProvider>();
|
||||
services.AddSingleton<IStorageProviderFactory, StorageProviderFactory>();
|
||||
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" Version="4.0.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||
|
||||
@@ -9,6 +9,7 @@ public enum StorageType
|
||||
{
|
||||
Local = 0,
|
||||
Telegram = 1,
|
||||
S3 = 2,
|
||||
}
|
||||
|
||||
public class Picture : BaseModel
|
||||
|
||||
177
Services/StorageProvider/S3StorageProvider.cs
Normal file
177
Services/StorageProvider/S3StorageProvider.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Foxel.Services.Interface;
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Amazon.S3.Transfer;
|
||||
|
||||
namespace Foxel.Services.StorageProvider;
|
||||
|
||||
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 _serverUrl;
|
||||
private readonly string _cdnUrl;
|
||||
|
||||
public S3StorageProvider(IConfigService configService)
|
||||
{
|
||||
_accessKey = configService["Storage:S3StorageAccessKey"];
|
||||
_secretKey = configService["Storage:S3StorageSecretKey"];
|
||||
_bucketName = configService["Storage:S3StorageBucketName"];
|
||||
_region = configService["Storage:S3StorageRegion"];
|
||||
_serverUrl = configService["AppSettings:ServerUrl"];
|
||||
_cdnUrl = configService["Storage:S3StorageCdnUrl"] ?? string.Empty;
|
||||
_endpoint = configService["Storage:S3StorageEndpoint"] ?? $"https://s3.{_region}.amazonaws.com";
|
||||
_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);
|
||||
using var fileStream = new FileStream(tempFilePath, FileMode.Create);
|
||||
await response.ResponseStream.CopyToAsync(fileStream);
|
||||
|
||||
return tempFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"从S3下载文件时出错: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,22 @@
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Services.Interface;
|
||||
using Foxel.Services.StorageProvider;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Foxel.Services;
|
||||
|
||||
public class StorageProviderFactory : IStorageProviderFactory
|
||||
public class StorageProviderFactory(
|
||||
LocalStorageProvider localStorageProvider,
|
||||
TelegramStorageProvider telegramStorageProvider,
|
||||
S3StorageProvider s3StorageProvider) : IStorageProviderFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public StorageProviderFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IStorageProvider GetProvider(StorageType storageType)
|
||||
{
|
||||
return storageType switch
|
||||
{
|
||||
StorageType.Local => _serviceProvider.GetRequiredService<LocalStorageProvider>(),
|
||||
StorageType.Telegram => _serviceProvider.GetRequiredService<TelegramStorageProvider>(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(storageType), $"不支持的存储类型: {storageType}")
|
||||
StorageType.Local => localStorageProvider,
|
||||
StorageType.Telegram => telegramStorageProvider,
|
||||
StorageType.S3 => s3StorageProvider,
|
||||
_ => throw new ArgumentException($"不支持的存储类型: {storageType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Tabs, Card, message, Spin, Select } from 'antd';
|
||||
import { CloudOutlined, DatabaseOutlined } from '@ant-design/icons';
|
||||
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined } from '@ant-design/icons';
|
||||
import { getAllConfigs, setConfig } from '../../api';
|
||||
import ConfigGroup from './ConfigGroup.tsx';
|
||||
import useIsMobile from '../../hooks/useIsMobile';
|
||||
@@ -84,9 +84,9 @@ const SystemConfig: React.FC = () => {
|
||||
|
||||
// 存储类型选项
|
||||
const storageOptions = [
|
||||
{ value: 'Telegram', label: 'Telegram存储', icon: <CloudOutlined style={{ color: '#0088cc' }} /> },
|
||||
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
|
||||
// 未来可以添加更多存储选项
|
||||
{ value: 'Telegram', label: 'Telegram存储', icon: <CloudOutlined style={{ color: '#0088cc' }} /> },
|
||||
{ value: 'S3', label: 'S3兼容存储', icon: <CloudServerOutlined style={{ color: '#ff9900' }} /> },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -199,7 +199,7 @@ const SystemConfig: React.FC = () => {
|
||||
默认存储:
|
||||
</span>
|
||||
<Select
|
||||
value={configs.Storage?.DefaultStorage || 'Telegram'}
|
||||
value={configs.Storage?.DefaultStorage || 'Local'}
|
||||
onChange={(value) => {
|
||||
handleSaveConfig('Storage', 'DefaultStorage', value);
|
||||
}}
|
||||
@@ -266,6 +266,32 @@ const SystemConfig: React.FC = () => {
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{storageType === 'S3' && (
|
||||
<ConfigGroup
|
||||
groupName="Storage"
|
||||
configs={{
|
||||
"S3StorageAccessKey": configs.Storage?.S3StorageAccessKey || '',
|
||||
"S3StorageSecretKey": configs.Storage?.S3StorageSecretKey || '',
|
||||
"S3StorageBucketName": configs.Storage?.S3StorageBucketName || '',
|
||||
"S3StorageRegion": configs.Storage?.S3StorageRegion || '',
|
||||
"S3StorageEndpoint": configs.Storage?.S3StorageEndpoint || '',
|
||||
"S3StorageCdnUrl": configs.Storage?.S3StorageCdnUrl || '',
|
||||
"S3StorageUsePathStyleUrls": configs.Storage?.S3StorageUsePathStyleUrls || 'false'
|
||||
}}
|
||||
onSave={handleSaveConfig}
|
||||
descriptions={{
|
||||
"S3StorageAccessKey": 'S3访问密钥',
|
||||
"S3StorageSecretKey": 'S3私有密钥',
|
||||
"S3StorageBucketName": 'S3存储桶名称',
|
||||
"S3StorageRegion": 'S3区域 (例如:us-east-1)',
|
||||
"S3StorageEndpoint": 'S3端点URL (可选,默认为AWS S3)',
|
||||
"S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
|
||||
"S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)'
|
||||
}}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user