# 文件存储 (16_OSS)
最后更新: 2024-09-20
# 📚 概述
DarkM 框架的文件存储模块提供了统一的对象存储服务接口。支持多种存储提供器(本地存储、七牛云、阿里云 OSS),实现一次开发,自由切换存储方案。
源代码位置: DarkM/src/Framework/OSS
# 🏗️ 模块架构
# 项目结构
OSS/
├── OSS.Abstractions/ # 抽象层(接口、配置)
│ ├── IFileStorageProvider.cs # 文件存储接口
│ ├── OSSProvider.cs # OSS 提供器枚举
│ ├── OSSConfig.cs # OSS 配置类
│ ├── QiniuConfig.cs # 七牛配置
│ └── AliyunConfig.cs # 阿里云配置
│
├── OSS.Integration/ # 集成层(DI 注册)
│ └── ServiceCollectionExtensions.cs # DI 扩展
│
├── OSS.Local/ # 本地存储实现
│ └── LocalFileStorageProvider.cs # 本地文件存储
│
├── OSS.Qiniu/ # 七牛云实现
│ └── QiniuFileStorageProvider.cs # 七牛文件存储
│
├── OSS.Aliyun/ # 阿里云实现
│ └── AliyunFileStorageProvider.cs # 阿里云 OSS 存储
│
└── OSS.Qiniu.SDK/ # 七牛 SDK(封装)
├── Util/ # 工具类
│ ├── CRC32.cs
│ ├── ETag.cs
│ ├── Auth.cs
│ └── Signature.cs
└── Storage/ # 存储类
├── UploadManager.cs # 上传管理器
├── BucketManager.cs # 空间管理
└── ResumableUploader.cs # 断点续传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 🔧 核心接口
# IFileStorageProvider(文件存储提供器)
/// <summary>
/// 文件存储提供器
/// </summary>
public interface IFileStorageProvider
{
/// <summary>
/// 上传文件到 OSS
/// </summary>
/// <param name="fileName">实际文件名</param>
/// <param name="path">保存到 OSS 的路径</param>
/// <param name="saveName">保存到 OSS 的文件名</param>
/// <param name="moduleCode">数据所属模块编码</param>
/// <param name="group">数据所属分组</param>
/// <param name="contentOrUrl">内容或者内容对应的路径</param>
/// <param name="isStringData">是否字符串数据(true 表示 contentOrUrl 为字符串数据,false 表示为文件路径)</param>
/// <param name="accessMode">访问模式</param>
/// <returns>上传结果</returns>
ValueTask<bool> Upload(
string fileName,
string path,
string saveName,
string moduleCode,
string group,
string contentOrUrl,
bool isStringData = true,
FileAccessMode accessMode = FileAccessMode.Open);
/// <summary>
/// 上传文件对象
/// </summary>
/// <param name="fileObject">文件对象</param>
ValueTask<bool> Upload(FileObject fileObject);
/// <summary>
/// 删除文件
/// </summary>
/// <param name="fileObject">文件对象</param>
ValueTask<bool> Delete(FileObject fileObject);
/// <summary>
/// 获取文件的物理绝对路径(主要用于本地存储)
/// </summary>
/// <param name="fullPath">完整路径</param>
/// <param name="accessMode">访问模式</param>
string GetPhysicalPath(string fullPath, FileAccessMode accessMode = FileAccessMode.Open);
/// <summary>
/// 获取完整的访问 URL
/// </summary>
/// <param name="fullPath">文件完整路径</param>
/// <param name="accessMode">访问模式</param>
string GetUrl(string fullPath, FileAccessMode accessMode = FileAccessMode.Open);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# OSSProvider(OSS 提供器)
[Description("OSS 提供器")]
public enum OSSProvider
{
/// <summary>
/// 本地存储
/// </summary>
[Description("本地存储")]
Local,
/// <summary>
/// 七牛云
/// </summary>
[Description("七牛")]
Qiniu,
/// <summary>
/// 阿里云 OSS
/// </summary>
[Description("阿里云")]
Aliyun
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# OSSConfig(OSS 配置)
public class OSSConfig
{
/// <summary>
/// OSS 提供器
/// </summary>
public OSSProvider Provider { get; set; } = OSSProvider.Local;
/// <summary>
/// 七牛配置
/// </summary>
public QiniuConfig Qiniu { get; set; } = new QiniuConfig();
/// <summary>
/// 阿里云配置
/// </summary>
public AliyunConfig Aliyun { get; set; } = new AliyunConfig();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# QiniuConfig(七牛配置)
public class QiniuConfig
{
/// <summary>
/// 访问 Key
/// </summary>
public string AccessKey { get; set; }
/// <summary>
/// 密钥
/// </summary>
public string SecretKey { get; set; }
/// <summary>
/// 域名
/// </summary>
public string Domain { get; set; }
/// <summary>
/// 空间名称
/// </summary>
public string Bucket { get; set; }
/// <summary>
/// 存储区域
/// </summary>
public QiniuZone Zone { get; set; }
/// <summary>
/// 令牌有效期(秒)
/// </summary>
public int TokenExpires { get; set; } = 7200;
/// <summary>
/// 配置检查
/// </summary>
public bool Check()
{
return AccessKey.NotNull() && SecretKey.NotNull() && Domain.NotNull();
}
}
/// <summary>
/// 七牛存储区域
/// </summary>
public enum QiniuZone
{
/// <summary>
/// 华东(浙江)
/// </summary>
[Description("华东")]
ZONE_CN_East,
/// <summary>
/// 华北(北京)
/// </summary>
[Description("华北")]
ZONE_CN_North,
/// <summary>
/// 华南(广东)
/// </summary>
[Description("华南")]
ZONE_CN_South,
/// <summary>
/// 北美(洛杉矶)
/// </summary>
[Description("北美")]
ZONE_US_North
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# AliyunConfig(阿里云配置)
public class AliyunConfig
{
/// <summary>
/// 域名(Endpoint)
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// 访问令牌 ID
/// </summary>
public string AccessKeyId { get; set; }
/// <summary>
/// 访问令牌密钥
/// </summary>
public string AccessKeySecret { get; set; }
/// <summary>
/// 存储空间名称(Bucket)
/// </summary>
public string BucketName { get; set; }
/// <summary>
/// 自定义域名
/// </summary>
public string Domain { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 🏗️ 依赖注入
# 自动注册机制
// OSS.Integration/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加 OSS 功能
/// </summary>
public static IServiceCollection AddOSS(
this IServiceCollection services,
IConfiguration cfg)
{
var config = new OSSConfig();
var section = cfg.GetSection("OSS");
if (section != null)
{
section.Bind(config);
}
// 自动修正域名(添加末尾斜杠)
if (config.Qiniu != null && config.Qiniu.Domain.NotNull()
&& !config.Qiniu.Domain.EndsWith("/"))
{
config.Qiniu.Domain += "/";
}
if (config.Aliyun != null && config.Aliyun.Domain.NotNull()
&& !config.Aliyun.Domain.EndsWith("/"))
{
config.Aliyun.Domain += "/";
}
services.AddSingleton(config);
// 根据配置加载对应的实现程序集
var assembly = AssemblyHelper.LoadByNameEndString(
$".Lib.OSS.{config.Provider.ToString()}");
if (assembly == null)
return services;
// 注册文件存储提供器
var providerType = assembly.GetTypes()
.FirstOrDefault(m => typeof(IFileStorageProvider).IsAssignableFrom(m));
if (providerType != null)
{
services.AddSingleton(typeof(IFileStorageProvider), providerType);
}
return services;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 💡 使用示例
# 1. 配置 OSS 模块
// appsettings.json
{
"OSS": {
"Provider": "Qiniu",
"Qiniu": {
"AccessKey": "your-access-key",
"SecretKey": "your-secret-key",
"Domain": "https://cdn.example.com",
"Bucket": "your-bucket-name",
"Zone": "ZONE_CN_East",
"TokenExpires": 7200
},
"Aliyun": {
"Endpoint": "oss-cn-hangzhou.aliyuncs.com",
"AccessKeyId": "your-access-key-id",
"AccessKeySecret": "your-access-key-secret",
"BucketName": "your-bucket-name",
"Domain": "https://cdn.example.com"
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2. 注册服务
// Program.cs 或 Startup.cs
var builder = WebApplication.CreateBuilder(args);
// 添加 OSS 服务
builder.Services.AddOSS(builder.Configuration);
var app = builder.Build();
1
2
3
4
5
6
7
2
3
4
5
6
7
# 3. 上传文件(字符串内容)
public class FileUploadService
{
private readonly IFileStorageProvider _storageProvider;
private readonly ILogger<FileUploadService> _logger;
public FileUploadService(
IFileStorageProvider storageProvider,
ILogger<FileUploadService> logger)
{
_storageProvider = storageProvider;
_logger = logger;
}
/// <summary>
/// 上传 Base64 图片
/// </summary>
public async Task<string> UploadBase64Image(string base64Data, string moduleCode, string group)
{
// 生成文件名
var fileName = $"{Guid.NewGuid()}.jpg";
var path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}";
var saveName = fileName;
// 上传
var success = await _storageProvider.Upload(
fileName: fileName,
path: path,
saveName: saveName,
moduleCode: moduleCode,
group: group,
contentOrUrl: base64Data,
isStringData: true // 表示 contentOrUrl 是字符串数据
);
if (!success)
throw new Exception("文件上传失败");
// 返回完整 URL
var fullPath = $"{path}/{saveName}";
return _storageProvider.GetUrl(fullPath);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 4. 上传文件(文件对象)
public class FileUploadController : ModuleController
{
private readonly IFileStorageProvider _storageProvider;
public FileUploadController(IFileStorageProvider storageProvider)
{
_storageProvider = storageProvider;
}
/// <summary>
/// 上传单个文件
/// </summary>
[HttpPost("upload")]
public async Task<IResultModel> UploadFile(
[FromForm] IFormFile file,
[FromForm] string moduleCode = "common",
[FromForm] string group = "default")
{
if (file == null || file.Length == 0)
return ResultModel.Error("请选择要上传的文件");
// 构建文件对象
var fileObject = new FileObject
{
FileName = file.FileName,
OriginalName = file.FileName,
ContentType = file.ContentType,
Size = file.Length,
ModuleCode = moduleCode,
Group = group,
Path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}"
};
// 读取文件内容
using var stream = file.OpenReadStream();
using var reader = new StreamReader(stream);
fileObject.Content = await reader.ReadToEndAsync();
// 上传
var success = await _storageProvider.Upload(fileObject);
if (!success)
return ResultModel.Error("文件上传失败");
// 返回文件信息
var fullPath = $"{fileObject.Path}/{fileObject.SaveName}";
return ResultModel.Success(new
{
fileName = fileObject.SaveName,
originalName = fileObject.OriginalName,
size = fileObject.Size,
url = _storageProvider.GetUrl(fullPath),
fullPath = fullPath
});
}
/// <summary>
/// 批量上传文件
/// </summary>
[HttpPost("upload-batch")]
public async Task<IResultModel> UploadBatch(
[FromForm] List<IFormFile> files,
[FromForm] string moduleCode = "common",
[FromForm] string group = "default")
{
var results = new List<object>();
foreach (var file in files)
{
var fileObject = new FileObject
{
FileName = file.FileName,
OriginalName = file.FileName,
ContentType = file.ContentType,
Size = file.Length,
ModuleCode = moduleCode,
Group = group,
Path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}"
};
using var stream = file.OpenReadStream();
using var reader = new StreamReader(stream);
fileObject.Content = await reader.ReadToEndAsync();
var success = await _storageProvider.Upload(fileObject);
if (success)
{
var fullPath = $"{fileObject.Path}/{fileObject.SaveName}";
results.Add(new
{
fileName = fileObject.SaveName,
originalName = fileObject.OriginalName,
url = _storageProvider.GetUrl(fullPath)
});
}
}
return ResultModel.Success(results);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 5. 删除文件
public class FileManagementService
{
private readonly IFileStorageProvider _storageProvider;
public FileManagementService(IFileStorageProvider storageProvider)
{
_storageProvider = storageProvider;
}
/// <summary>
/// 删除文件
/// </summary>
public async Task<bool> DeleteFile(string fullPath)
{
var fileObject = new FileObject
{
FullPath = fullPath
};
return await _storageProvider.Delete(fileObject);
}
/// <summary>
/// 批量删除文件
/// </summary>
public async Task<int> DeleteBatch(List<string> fullPaths)
{
var deletedCount = 0;
foreach (var fullPath in fullPaths)
{
try
{
var success = await DeleteFile(fullPath);
if (success)
deletedCount++;
}
catch (Exception ex)
{
// 记录日志
_logger.LogError(ex, "删除文件失败:{FullPath}", fullPath);
}
}
return deletedCount;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 6. 获取文件 URL
public class FileUrlService
{
private readonly IFileStorageProvider _storageProvider;
public FileUrlService(IFileStorageProvider storageProvider)
{
_storageProvider = storageProvider;
}
/// <summary>
/// 获取文件访问 URL
/// </summary>
public string GetFileUrl(string fullPath)
{
return _storageProvider.GetUrl(fullPath);
}
/// <summary>
/// 获取文件物理路径(仅本地存储有效)
/// </summary>
public string GetPhysicalPath(string fullPath)
{
return _storageProvider.GetPhysicalPath(fullPath);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 🔧 存储提供器对比
# OSS.Local(本地存储)
特点:
- ✅ 无需外部服务,部署简单
- ✅ 零成本
- ✅ 适合开发测试环境
- ⚠️ 需要自行处理备份和 CDN
- ⚠️ 不适合大规模生产环境
适用场景:
- 开发环境
- 内部系统
- 小规模应用
# OSS.Qiniu(七牛云)
特点:
- ✅ 国内 CDN 覆盖广
- ✅ 价格亲民
- ✅ 支持断点续传
- ✅ 支持图片处理(缩放、水印等)
- ⚠️ 免费额度有限(10GB 存储 +10GB 流量/月)
适用场景:
- 中小企业应用
- 图片/视频存储
- 需要 CDN 加速
# OSS.Aliyun(阿里云 OSS)
特点:
- ✅ 阿里云生态集成
- ✅ 高可靠性(99.9999999999%)
- ✅ 支持多种存储类型(标准/低频/归档)
- ✅ 完善的安全控制
- ⚠️ 价格相对较高
适用场景:
- 大型企业应用
- 高可靠性要求
- 已在阿里云部署
# 💡 最佳实践
# 1. 文件命名规范
public class FileNameGenerator
{
/// <summary>
/// 生成唯一文件名
/// </summary>
public static string GenerateFileName(string originalFileName)
{
var extension = Path.GetExtension(originalFileName);
var guid = Guid.NewGuid().ToString("N");
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
return $"{timestamp}_{guid}{extension}";
}
/// <summary>
/// 生成存储路径(按日期分目录)
/// </summary>
public static string GeneratePath(string moduleCode, string group)
{
var date = DateTime.Now.ToString("yyyyMMdd");
return $"/{moduleCode}/{group}/{date}";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. 文件类型限制
public class FileUploadValidator
{
private static readonly Dictionary<string, long> AllowedTypes = new()
{
// 图片
{ ".jpg", 10 * 1024 * 1024 }, // 10MB
{ ".jpeg", 10 * 1024 * 1024 },
{ ".png", 10 * 1024 * 1024 },
{ ".gif", 5 * 1024 * 1024 }, // 5MB
// 文档
{ ".pdf", 50 * 1024 * 1024 }, // 50MB
{ ".doc", 50 * 1024 * 1024 },
{ ".docx", 50 * 1024 * 1024 },
{ ".xls", 50 * 1024 * 1024 },
{ ".xlsx", 50 * 1024 * 1024 },
// 视频
{ ".mp4", 500 * 1024 * 1024 }, // 500MB
{ ".avi", 500 * 1024 * 1024 },
{ ".mov", 500 * 1024 * 1024 }
};
public static (bool isValid, string message) Validate(IFormFile file)
{
var extension = Path.GetExtension(file.FileName).ToLower();
if (!AllowedTypes.ContainsKey(extension))
return (false, $"不支持的文件类型:{extension}");
if (file.Length > AllowedTypes[extension])
return (false, $"文件大小超过限制({AllowedTypes[extension] / 1024 / 1024}MB)");
return (true, "验证通过");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 3. 上传进度跟踪
public class UploadProgressService
{
private readonly IMemoryCache _cache;
public UploadProgressService(IMemoryCache cache)
{
_cache = cache;
}
public void ReportProgress(string uploadId, int percent)
{
_cache.Set(uploadId, percent, TimeSpan.FromMinutes(30));
}
public int GetProgress(string uploadId)
{
return _cache.TryGetValue(uploadId, out int percent) ? percent : 0;
}
public void Complete(string uploadId)
{
_cache.Remove(uploadId);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 4. 敏感文件加密存储
public class EncryptedFileStorage
{
private readonly IFileStorageProvider _storageProvider;
private readonly IEncryptionService _encryptionService;
public EncryptedFileStorage(
IFileStorageProvider storageProvider,
IEncryptionService encryptionService)
{
_storageProvider = storageProvider;
_encryptionService = encryptionService;
}
public async Task<string> UploadEncrypted(
string content,
string moduleCode,
string group)
{
// 加密内容
var encryptedContent = _encryptionService.Encrypt(content);
// 上传加密后的内容
var fileName = FileNameGenerator.GenerateFileName("encrypted.dat");
var path = FileNameGenerator.GeneratePath(moduleCode, group);
await _storageProvider.Upload(
fileName: fileName,
path: path,
saveName: fileName,
moduleCode: moduleCode,
group: group,
contentOrUrl: encryptedContent,
isStringData: true
);
return $"{path}/{fileName}";
}
public async Task<string> DownloadDecrypted(string fullPath)
{
// 下载加密内容(需要实现下载接口)
var encryptedContent = await DownloadContent(fullPath);
// 解密
return _encryptionService.Decrypt(encryptedContent);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 🔍 常见问题
# Q1: 如何选择存储提供器?
| 场景 | 推荐提供器 | 理由 |
|---|---|---|
| 开发测试 | Local | 零成本、部署简单 |
| 中小企业 | Qiniu | 性价比高、CDN 覆盖好 |
| 大型企业 | Aliyun | 高可靠、阿里云生态 |
| 已有阿里云 | Aliyun | 内网传输、生态集成 |
| 图片/视频为主 | Qiniu | 图片处理功能强 |
# Q2: 七牛云 Token 过期如何处理?
public class QiniuTokenService
{
private readonly OSSConfig _config;
private readonly IMemoryCache _cache;
public QiniuTokenService(
IOptions<OSSConfig> config,
IMemoryCache cache)
{
_config = config.Value;
_cache = cache;
}
public string GetUploadToken()
{
return _cache.GetOrCreate("qiniu_upload_token", entry =>
{
entry.AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(_config.Qiniu.TokenExpires - 600); // 提前 10 分钟过期
// 生成上传凭证
var putPolicy = new PutPolicy(_config.Qiniu.Bucket);
var auth = new Auth(_config.Qiniu.AccessKey, _config.Qiniu.SecretKey);
return auth.CreateUploadToken(putPolicy.ToJsonString());
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Q3: 如何实现断点续传?
public class ResumableUploadService
{
private readonly IFileStorageProvider _storageProvider;
private readonly string _tempPath;
public ResumableUploadService(
IFileStorageProvider storageProvider,
IOptions<OSSConfig> config)
{
_storageProvider = storageProvider;
_tempPath = config.Value.Qiniu.TempPath ?? "/temp/upload";
}
/// <summary>
/// 分片上传初始化
/// </summary>
public string InitMultipartUpload(string fileName, long fileSize)
{
var uploadId = Guid.NewGuid().ToString("N");
var tempFile = Path.Combine(_tempPath, uploadId);
// 记录上传信息
_cache.Set(uploadId, new UploadInfo
{
FileName = fileName,
FileSize = fileSize,
TempFile = tempFile,
UploadedChunks = new List<int>()
});
return uploadId;
}
/// <summary>
/// 上传分片
/// </summary>
public async Task UploadChunk(string uploadId, int chunkIndex, byte[] chunkData)
{
var info = _cache.Get<UploadInfo>(uploadId);
var tempFile = info.TempFile;
// 追加写入临时文件
await File.AppendAllBytesAsync(tempFile, chunkData);
info.UploadedChunks.Add(chunkIndex);
}
/// <summary>
/// 完成上传
/// </summary>
public async Task CompleteUpload(string uploadId, string moduleCode, string group)
{
var info = _cache.Get<UploadInfo>(uploadId);
// 上传完整文件
var content = await File.ReadAllBytesAsync(info.TempFile);
var base64 = Convert.ToBase64String(content);
await _storageProvider.Upload(
fileName: info.FileName,
path: $"/{moduleCode}/{group}",
saveName: info.FileName,
moduleCode: moduleCode,
group: group,
contentOrUrl: base64,
isStringData: true
);
// 清理临时文件
File.Delete(info.TempFile);
_cache.Remove(uploadId);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 📚 相关文档
# 🔗 参考链接
- 源代码 (opens new window) -
src/Framework/OSS - 七牛云文档 (opens new window)
- 阿里云 OSS 文档 (opens new window)
最后更新: 2024-09-20