RuoVea.OmiApi.Upload
10.0.0.8
dotnet add package RuoVea.OmiApi.Upload --version 10.0.0.8
NuGet\Install-Package RuoVea.OmiApi.Upload -Version 10.0.0.8
<PackageReference Include="RuoVea.OmiApi.Upload" Version="10.0.0.8" />
<PackageVersion Include="RuoVea.OmiApi.Upload" Version="10.0.0.8" />
<PackageReference Include="RuoVea.OmiApi.Upload" />
paket add RuoVea.OmiApi.Upload --version 10.0.0.8
#r "nuget: RuoVea.OmiApi.Upload, 10.0.0.8"
#:package RuoVea.OmiApi.Upload@10.0.0.8
#addin nuget:?package=RuoVea.OmiApi.Upload&version=10.0.0.8
#tool nuget:?package=RuoVea.OmiApi.Upload&version=10.0.0.8
RuoVea.OmiApi.Upload
文件上传模块 —— 基于 RuoVea 框架构建的附件上传管理组件,支持按文件类型分类存储、验证和上传。
RuoVea.OmiApi.Upload 是一个开箱即用的文件上传 NuGet 包,提供单文件/多文件/Base64 上传、分类存储、文件预览与下载的完整能力。基于 DynamicWebApi,注册即自动生成 RESTful API 端点,支持 10 种文件类型分类管理,内置 i18n 多语言(中文简繁/英文/越南语/法语/日语)。
目录
概览
功能特性
| 模块 | 功能 |
|---|---|
| 📤 分类上传 | 10 种文件类型(Images/Documents/Spreadsheets/Presentations/Archives/Code/Videos/Audios/Ebooks/Others),路由自动匹配 |
| 📦 批量上传 | 多文件并行上传,支持部分成功/失败结果分别返回 |
| 🔒 文件验证 | 大小、扩展名、MIME 类型、文件名长度四重校验,支持通配符 * |
| 🎯 统一上传 | 自定义存储路径、日期占位符(:yyyy/:MM/:dd)、允许后缀白名单 |
| 🔐 Base64 上传 | 支持 data:image/png;base64,xxx 格式,自动解析 MIME 类型和文件扩展名 |
| 👁️ 预览与下载 | inline 预览和 attachment 下载双模式,支持 Base64 格式下载 |
| ⚙️ 层级配置 | 全局配置 + 分类级别配置覆盖(MaxFileSize/Extensions/MimeTypes/StoragePath 等 8 项) |
| 🗂️ 灵活存储 | 相对路径与绝对路径自动识别(Path.IsPathRooted),日期占位符动态路径 |
| 🌐 i18n 多语言 | 中文简体/繁体(香港/台湾)/英文/越南语/法语/日语,共 7 种语言 |
| 🧩 自动 API | 实现 IApplicationService 即自动映射为 REST 控制器,Swagger 分组 Upload |
| 🔄 三种注册 | 配置文件 / IConfiguration / Action<UploadConfig> 代码配置,支持 Scoped/Singleton/Transient 生命周期 |
架构一览
┌─────────────────────────────────────────────────────┐
│ NuGet Package │
│ RuoVea.OmiApi.Upload │
├─────────────────────────────────────────────────────┤
│ Service Layer (1 Service) │
│ ┌───────────────────────────────────────┐ │
│ │ FileUploadService │ │
│ │ ┌──────┐ ┌──────────┐ ┌───────────┐ │ │
│ │ │ Image│ │ Category │ │UploadFile │ │ │
│ │ └──────┘ └──────────┘ └───────────┘ │ │
│ │ ┌──────┐ ┌──────────┐ ┌───────────┐ │ │
│ │ │Base64│ │Multiple │ │Preview │ │ │
│ │ └──────┘ └──────────┘ └───────────┘ │ │
│ │ ┌──────────┐ ┌───────────────────┐ │ │
│ │ │ Download │ │ Validate/GetStream │ │ │
│ │ └──────────┘ └───────────────────┘ │ │
│ └───────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ DTO Layer (5 DTOs) │
│ UploadFileInput · UploadFileFromBase64Input │
│ UploadResult · UploadConfig │
│ FileTypePathMapping │
├─────────────────────────────────────────────────────┤
│ Validation Pipeline │
│ 文件大小 → 扩展名 → MIME类型 → 文件名长度 │
│ (全局配置 ← 分类配置覆盖) │
├─────────────────────────────────────────────────────┤
│ Infrastructure │
│ DynamicWebApi · ExSugar · ExLog · IdGenerator │
└─────────────────────────────────────────────────────┘
文件分类体系
UploadPath (Uploads/)
├── Images/ ← .jpg .png .gif .webp .svg .ico ...
├── Documents/ ← .pdf .doc .docx .txt .rtf ...
├── Spreadsheets/ ← .xls .xlsx .csv .ods
├── Presentations/ ← .ppt .pptx .odp
├── Archives/ ← .zip .rar .7z .tar .gz
├── Code/ ← .html .css .js .json .xml .md
├── Videos/ ← .mp4 .avi .mov .mkv .webm ...
├── Audios/ ← .mp3 .wav .ogg .flac .aac ...
├── Ebooks/ ← .epub .mobi .azw .chm
└── Others/ ← 通配符 *,兜底分类
支持的 .NET 版本
| TFM | NuGet 版本 |
|---|---|
net8.0 |
8.0.0.19 |
net10.0 |
10.0.0.6 |
安装
NuGet 包管理器
# .NET 8 项目
Install-Package RuoVea.OmiApi.Upload -Version 8.0.0.19
# .NET 10 项目
Install-Package RuoVea.OmiApi.Upload -Version 10.0.0.6
.NET CLI
dotnet add package RuoVea.OmiApi.Upload --version 8.0.0.19
依赖项
本包依赖以下组件(安装时会自动引入):
| 包名 | 用途 |
|---|---|
RuoVea.DynamicWebApi |
动态 API 控制器生成,自动将 Service 映射为 REST 端点 |
RuoVea.ExSugar |
框架扩展工具集(IdGenerator 唯一 ID 生成等) |
RuoVea.ExLog |
日志记录框架,文件上传过程日志追踪 |
30 秒快速开始
1. 配置文件上传 (appsettings.json)
{
"UploadConfig": {
"MaxFileSize": 10485760,
"UploadPath": "Uploads",
"UseOriginalFileName": false,
"MaxFileNameLength": 255,
"OverwriteExisting": false,
"FileTypePathMappings": [
{
"Category": "Images",
"Extensions": [ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" ],
"MimeTypes": [ "image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp" ],
"StoragePath": "Images",
"MaxFileSize": 20971520
}
]
},
"Swagger": {
"ApiVersions": [
{
"Title": "附件上传管理",
"Version": "Upload"
}
]
}
}
2. 注册服务 (Program.cs)
// <summary>
// 在 Program.cs 中注册 OmiApi.Upload 组件服务
// </summary>
var builder = WebApplication.CreateBuilder(args);
// 注册动态 Web API(自动将 Service 映射为 REST 控制器)
builder.Services.AddDynamicWebApi(options =>
{
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
// 注册上传模块服务(默认 Scoped 生命周期,自动从 appsettings.json 读取 UploadConfig)
builder.Services.AddOmiUploadSetup();
// (可选)配置 i18n 多语言
builder.Services.AddLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("zh"),
new CultureInfo("zh-HK"),
new CultureInfo("zh-TW"),
new CultureInfo("en"),
new CultureInfo("vi"),
new CultureInfo("fr"),
new CultureInfo("ja"),
};
options.DefaultRequestCulture = new RequestCulture("zh");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
var app = builder.Build();
// 启用多语言中间件
var localizationOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(localizationOptions.Value);
app.Run();
3. 启动并访问 Swagger
启动项目后,访问 https://localhost:xxxx/swagger,即可看到 "附件上传管理" 分组下的全部 RESTful API 端点。
实际路由前缀取决于
AddDynamicWebApi的DefaultApiPrefix配置,生产环境一般设置为/openapi/api。
核心场景
场景一:图片文件上传(便捷接口)
通过专用图片上传端点,自动验证文件类型是否为 Images 分类。
// <summary>
// 图片上传 —— 便捷接口,前端直接 POST FormData 到 /api/fileUpload/image。
// 自动验证文件是否为 Images 分类,不匹配则返回 Error_FileCategoryMismatch。
// </summary>
public async Task<bool> UploadImageAsync(HttpClient client, string filePath)
{
using var form = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
form.Add(fileContent, "file", Path.GetFileName(filePath));
// POST /api/fileUpload/image
var response = await client.PostAsync("/api/fileUpload/image", form);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
return result.Code == CodeStatus.OK;
}
// <summary>
// 图片上传 —— 同步写法(仅在 Console/测试环境使用)
// </summary>
public bool UploadImage(HttpClient client, string filePath)
{
return UploadImageAsync(client, filePath).GetAwaiter().GetResult();
}
图片上传流程:
接收 IFormFile
│
├─ 1. 空文件检查 → Prompt_SelectFile
│
├─ 2. GetFileCategory(file) → 判断文件归属分类
│ └─ 扩展名匹配 → MIME类型匹配 → Unknown
│
├─ 3. 分类匹配验证 → Error_FileCategoryMismatch
│
├─ 4. ValidateFile(file)
│ ├─ 文件大小校验 (分类配置 > 全局配置)
│ ├─ 扩展名白名单校验 (支持通配符 *)
│ ├─ MIME类型白名单校验 (支持通配符 *)
│ └─ 文件名长度校验
│
└─ 5. UploadFileAsync → 保存到磁盘 → 返回 UploadResult
场景二:统一文件上传(自定义路径 + 日期占位符)
使用统一上传接口,指定 targetPath 和允许后缀白名单。
// <summary>
// 统一文件上传 —— 指定自定义存储路径和允许后缀。
// targetPath 支持日期占位符:upload/:yyyy/:MM/:dd → upload/2026/06/26
// </summary>
public async Task<bool> UploadWithCustomPathAsync(
HttpClient client, string filePath, string targetPath)
{
using var form = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
var fileContent = new StreamContent(fileStream);
form.Add(fileContent, "file", Path.GetFileName(filePath));
// allowSuffix 白名单:仅允许 jpg 和 png
form.Add(new StringContent(".jpg.png"), "allowSuffix");
// POST /api/fileUpload/uploadFile?targetPath=upload/:yyyy/:MM/:dd
var response = await client.PostAsync(
$"/api/fileUpload/uploadFile?targetPath={Uri.EscapeDataString(targetPath)}", form);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
return result.Code == CodeStatus.OK;
}
// <summary>
// 统一文件上传 —— 同步写法
// </summary>
public bool UploadWithCustomPath(HttpClient client, string filePath, string targetPath)
{
return UploadWithCustomPathAsync(client, filePath, targetPath).GetAwaiter().GetResult();
}
统一上传流程:
UploadFile([FromForm] UploadFileInput, [FromQuery] targetPath)
│
├─ 1. 空文件检查 → Prompt_SelectFile
│
├─ 2. 文件名非法字符检查 (Path.GetInvalidFileNameChars)
│ └─ → Error_InvalidFileNameChars
│
├─ 3. ValidateFile 基础验证
│
├─ 4. 后缀规范化 (.jpeg → .jpg) + 白名单检查
│ └─ → Error_UnsupportedSuffix
│
├─ 5. targetPath 为空?
│ ├─ YES → UploadFileAsync(按分类子目录存储)
│ └─ NO → UploadFileToCustomPath
│ ├─ ParseToDateTimePath (日期占位符替换)
│ ├─ ResolveUploadRootPath (根路径解析)
│ └─ SaveFileToDisk (文件写入)
│
└─ 返回 UploadResult
场景三:Base64 文件上传
支持标准 data:image/png;base64,xxx 格式和纯 Base64 字符串。
// <summary>
// Base64 文件上传 —— 支持 data URI 前缀格式和纯 Base64 字符串。
// 格式示例:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
// 未指定 fileName 时自动生成唯一文件名。
// </summary>
public async Task<bool> UploadFromBase64Async(HttpClient client)
{
var imageBytes = await File.ReadAllBytesAsync("photo.png");
var base64 = Convert.ToBase64String(imageBytes);
var fileData = $"data:image/png;base64,{base64}";
var payload = new UploadFileFromBase64Input
{
FileName = "photo.png",
FileDataBase64 = fileData
};
var response = await client.PostAsJsonAsync("/api/fileUpload/uploadFileFromBase64", payload);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
if (result.Code == CodeStatus.OK && result.Data is UploadResult uploadResult)
{
Console.WriteLine($"上传成功: {uploadResult.FilePath}");
return true;
}
return false;
}
// <summary>
// Base64 上传 —— 纯 Base64 字符串(无 data URI 前缀),自动生成文件名
// </summary>
public async Task<bool> UploadPureBase64Async(HttpClient client, string pureBase64)
{
var payload = new UploadFileFromBase64Input
{
FileName = "", // 留空自动生成 ID + 扩展名
FileDataBase64 = pureBase64 // 纯 Base64,无前缀
};
var response = await client.PostAsJsonAsync("/api/fileUpload/uploadFileFromBase64", payload);
return response.IsSuccessStatusCode;
}
Base64 上传流程:
UploadFileFromBase64(input)
│
├─ 1. 正则匹配 data URI: data:(type);base64,(payload)
│ ├─ 匹配成功 → 解析 type 和 data,提取 MIME
│ └─ 匹配失败 → 当作纯 Base64 处理,contentType="application/octet-stream"
│
├─ 2. fileName 为空? → 自动生成: IdGenerator.Id + "." + ext
│ (.jpeg/.jpe → 修正为 .jpg)
│
├─ 3. 构建 IFormFile (MemoryStream + FormFile)
│
├─ 4. 构造 UploadFileInput → 委托给 UploadFile
│
├─ Exception Handling:
│ ├─ FormatException → Error_Base64FormatInvalid
│ └─ 其他 → Error_UploadFailed
│
└─ 返回 UploadResult
场景四:多文件批量上传(部分成功容错)
// <summary>
// 多文件批量上传 —— 逐个文件独立上传,失败不影响其他文件。
// 返回 SuccessfulUploads 和 FailedUploads 两部分结果。
// </summary>
public async Task<(List<UploadResult> Success, List<string> Failed)> UploadMultipleAsync(
HttpClient client, List<string> filePaths)
{
using var form = new MultipartFormDataContent();
foreach (var path in filePaths)
{
var stream = File.OpenRead(path);
var fileContent = new StreamContent(stream);
form.Add(fileContent, "files", Path.GetFileName(path));
}
// POST /api/fileUpload/uploadFiles 或 /api/fileUpload/multipleFilesByCategory
var response = await client.PostAsync("/api/fileUpload/uploadFiles", form);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
if (result.Data is JsonElement data)
{
var success = JsonSerializer.Deserialize<List<UploadResult>>(
data.GetProperty("successfulUploads").GetRawText());
var failed = JsonSerializer.Deserialize<List<string>>(
data.GetProperty("failedUploads").GetRawText());
return (success, failed);
}
return (new List<UploadResult>(), new List<string>());
}
// <summary>
// 多文件批量上传 —— 按分类(仅 Images 分类文件)
// </summary>
public async Task UploadImagesByCategoryAsync(HttpClient client, List<string> imagePaths)
{
using var form = new MultipartFormDataContent();
foreach (var path in imagePaths)
{
var stream = File.OpenRead(path);
var fileContent = new StreamContent(stream);
form.Add(fileContent, "files", Path.GetFileName(path));
}
// POST /api/fileUpload/multipleFilesByCategory?category=image
var response = await client.PostAsync(
"/api/fileUpload/multipleFilesByCategory?category=image", form);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
}
多文件上传流程:
MultipleFiles / MultipleFilesByCategory
│
├─ 空列表检查 → Prompt_SelectFile
│
├─ FOREACH file IN files:
│ │
│ ├─ file.Length == 0? → skip
│ │
│ ├─ ValidateFile(file) → 失败? → errors.Add + continue
│ │
│ ├─ UploadFileAsync(file) → try/catch
│ │ ├─ 成功 → results.Add(UploadResult)
│ │ └─ 失败 → errors.Add(fileName: error)
│ │
│
├─ 汇总结果:
│ ├─ errors.Count > 0 → Message_PartialUploadSuccess
│ └─ errors.Count = 0 → Message_AllUploadSuccess
│
└─ 返回 { SuccessfulUploads, FailedUploads }
场景五:文件预览与下载
// <summary>
// 文件预览 —— inline 模式,浏览器直接显示图片/PDF/文本等。
// GET /api/fileUpload/preview?filePath=/Images/2026/06/abc123.jpg
// </summary>
public async Task PreviewFileAsync(HttpClient client, string filePath)
{
var response = await client.GetAsync(
$"/api/fileUpload/preview?filePath={Uri.EscapeDataString(filePath)}");
// Content-Disposition: inline
// Content-Type: 自动匹配 MIME(image/png, application/pdf, text/plain ...)
}
// <summary>
// 文件下载 —— attachment 模式,浏览器弹出下载对话框。
// GET /api/fileUpload/downloadByPath?filePath=/Documents/report.pdf
// </summary>
public async Task DownloadFileAsync(HttpClient client, string filePath)
{
var response = await client.GetAsync(
$"/api/fileUpload/downloadByPath?filePath={Uri.EscapeDataString(filePath)}");
// Content-Disposition: attachment; filename="report.pdf"
}
// <summary>
// 下载文件为 Base64 字符串 —— POST 方式,返回 data URI 格式。
// </summary>
public async Task<string> DownloadAsBase64Async(HttpClient client, string filePath)
{
var content = new StringContent($"\"{filePath}\"", Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/fileUpload/downloadFileBase64", content);
var result = await response.Content.ReadFromJsonAsync<RestfulResult>();
if (result.Code == CodeStatus.OK)
{
// result.Data = "data:image/png;base64,iVBORw0KG..."
return result.Data.ToString();
}
return null;
}
// <summary>
// 文件下载 —— 同步写法
// </summary>
public void DownloadFile(HttpClient client, string filePath)
{
DownloadFileAsync(client, filePath).GetAwaiter().GetResult();
}
文件预览/下载流程:
Preview(filePath) / DownloadByPath(filePath)
│
├─ 参数检查 (IsNullOrWhiteSpace) → Error_InvalidParameters
│
├─ ResolveFullPath(filePath)
│ └─ UploadRoot + relativePath
│
├─ File.Exists(fullPath)? → NO → Error_FileNotFound
│
├─ GetContentType(fileName) → 28种 MIME 映射
│
└─ FileStreamResult(stream, contentType)
├─ Preview: FileDownloadName = null → inline
└─ Download: FileDownloadName = name → attachment
配置选项详解
全局上传配置 (UploadConfig)
{
"UploadConfig": {
"MaxFileSize": 10485760,
"UploadPath": "Uploads",
"UseOriginalFileName": false,
"MaxFileNameLength": 255,
"OverwriteExisting": false,
"FileTypePathMappings": []
}
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MaxFileSize |
long |
10485760 |
全局默认最大文件大小(字节),默认 10 MB |
UploadPath |
string |
"" |
上传根目录。相对路径(如 Uploads)相对于 ContentRootPath,绝对路径(如 D:/DevUploads)直接使用 |
UseOriginalFileName |
bool |
false |
是否保留原始文件名。false 时使用 IdGenerator.Id 生成唯一文件名 |
MaxFileNameLength |
int |
255 |
文件名最大长度限制。超过时自动截断(仅在使用原始文件名时生效) |
OverwriteExisting |
bool |
false |
同名文件是否覆盖。false 时自动添加 _1、_2 后缀重命名 |
FileTypePathMappings |
array |
[] |
按文件类型分类的配置映射,数组中的配置可覆盖全局配置 |
UploadPath 路径机制
| 路径格式 | 示例 | 判断逻辑 | 最终物理路径 |
|---|---|---|---|
| 相对路径 | "Uploads" |
!Path.IsPathRooted |
ContentRootPath + "/" + Uploads |
| Windows 绝对路径 | "D:/DevUploads" |
Path.IsPathRooted |
D:/DevUploads |
| Linux 绝对路径 | "/var/uploads" |
Path.IsPathRooted |
/var/uploads |
Linux 注意: 以
/开头的路径在 Linux 上被视为绝对路径,指向系统根目录。若需存储在项目目录下,请使用相对路径"Uploads"(无前导/)。
分类级别配置 (FileTypePathMapping)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Category |
string |
"" |
文件分类名称(如 Images、Documents)。必填的唯一标识 |
Extensions |
string[] |
[] |
允许的扩展名数组。"*" 表示匹配所有扩展名。空数组跳过该校验 |
MimeTypes |
string[] |
[] |
允许的 MIME 类型数组。"*" 表示匹配所有 MIME 类型。空数组跳过该校验 |
StoragePath |
string |
"" |
该分类文件的存储子目录(相对于 UploadPath)。最终路径 = UploadPath/StoragePath |
MaxFileSize |
long |
0 |
该分类的最大文件大小(字节)。0 时使用全局 MaxFileSize |
UseOriginalFileName |
bool? |
null |
是否使用原始文件名。null 时使用全局 UseOriginalFileName |
MaxFileNameLength |
int |
0 |
文件名最大长度。0 时使用全局 MaxFileNameLength |
OverwriteExisting |
bool? |
null |
是否覆盖同名文件。null 时使用全局 OverwriteExisting |
分类配置优先级高于全局配置。每条分类配置中的值为默认值(
0、null、空数组)时,自动回退到全局配置。这使得每个分类可以只覆盖需要的配置项。
日期占位符
targetPath 参数支持以下日期占位符,上传时自动替换为当前时间:
| 占位符 | 含义 | 输入示例 | 解析结果(假设 2026-06-26 14:30:00) |
|---|---|---|---|
:yyyy |
四位年份 | upload/:yyyy |
upload/2026 |
:MM |
两位月份 | upload/:yyyy/:MM |
upload/2026/06 |
:dd |
两位日期 | upload/:yyyy/:MM/:dd |
upload/2026/06/26 |
:HH |
两位小时(24H) | logs/:HH |
logs/14 |
:mm |
两位分钟 | logs/:HH:mm |
logs/14:30 |
:ss |
两位秒数 | logs/:HH:mm:ss |
logs/14:30:00 |
DI 注册配置
// <summary>
// AddOmiUploadSetup —— 三种重载,适应不同配置来源。
// </summary>
// 重载 1:自动从 appsettings.json 的 UploadConfig 节读取
builder.Services.AddOmiUploadSetup();
// 重载 2:传入自定义 IConfiguration
builder.Services.AddOmiUploadSetup(builder.Configuration.GetSection("MyUploadConfig"));
// 重载 3:通过 Action<UploadConfig> 代码配置
builder.Services.AddOmiUploadSetup(config =>
{
config.MaxFileSize = 20971520;
config.UploadPath = "D:/DevUploads";
config.UseOriginalFileName = false;
config.MaxFileNameLength = 255;
config.OverwriteExisting = false;
});
// 自定义服务生命周期(默认 Scoped)
builder.Services.AddOmiUploadSetup(ServiceLifetime.Singleton); // 单例
builder.Services.AddOmiUploadSetup(ServiceLifetime.Transient); // 瞬态
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
serviceLifetime |
ServiceLifetime |
Scoped |
FileUploadService 的注册生命周期 |
config (IConfiguration) |
IConfiguration |
— | 自定义配置节 |
config (Action) |
Action<UploadConfig> |
— | 代码内配置 |
⚠️ 线程安全: 切换为
Singleton生命周期时,IWebHostEnvironment本身是单例安全的,但需注意UploadConfig(通过IOptions<UploadConfig>注入)在单例模式下不会随配置文件热更新而刷新。如需热更新,请使用IOptionsSnapshot或保持Scoped生命周期。
API 接口速览
所有接口由 FileUploadService 通过 RuoVea.DynamicWebApi 自动生成 RESTful 路由,归入 Swagger "Upload" 分组。
分类上传
| HTTP | 端点 | 方法 | 说明 |
|---|---|---|---|
| POST | /api/fileUpload/image |
Image |
图片文件便捷上传,自动验证 Images 分类 |
| POST | /api/fileUpload/{category} |
Category |
按分类路由上传。category 正则匹配:image document spreadsheet presentation archive code video audio ebook other |
| POST | /api/fileUpload/multipleFilesByCategory |
MultipleFilesByCategory |
按分类多文件批量上传 |
统一上传
| HTTP | 端点 | 方法 | 说明 |
|---|---|---|---|
| POST | /api/fileUpload/uploadFile |
UploadFile |
统一文件上传,支持 targetPath(日期占位符)和 AllowSuffix 白名单 |
| POST | /api/fileUpload/uploadFileFromBase64 |
UploadFileFromBase64 |
Base64 文件上传,支持 data:image/png;base64,xxx 格式 |
| POST | /api/fileUpload/uploadFiles |
MultipleFiles |
多文件批量上传(逐个调用统一上传,部分成功容错) |
文件预览与下载
| HTTP | 端点 | 方法 | 说明 |
|---|---|---|---|
| GET | /api/fileUpload/preview |
Preview |
文件内联预览(inline),浏览器直接打开图片/PDF/文本 |
| GET | /api/fileUpload/downloadByPath |
DownloadByPath |
文件下载(attachment),浏览器弹出下载对话框 |
| POST | /api/fileUpload/downloadFileBase64 |
DownloadFileBase64 |
下载文件为 Base64 字符串,返回 data:{MIME};base64,{content} 格式 |
公开工具方法(可编程调用)
| 可见性 | 方法 | 说明 |
|---|---|---|
[NonAction] (public) |
ValidateFile(IFormFile, out string) |
验证文件是否满足配置要求,返回错误信息 |
[NonAction] (public) |
UploadFileAsync(IFormFile, string) |
执行文件上传到分类子目录,返回 UploadResult |
[NonAction] (public) |
GetFileCategory(IFormFile) |
获取文件归属分类名称 |
[NonAction] (public) |
GetFileStream(string) |
根据路径获取原始 FileStream,不存在时返回 null |
请求参数说明
统一上传 (uploadFile)
| 参数 | 类型 | 来源 | 说明 |
|---|---|---|---|
file |
IFormFile |
FormData | 要上传的文件(必填) |
fileType |
string |
FormData | 文件类型标识(可选) |
allowSuffix |
string |
FormData | 允许的文件后缀白名单,如 ".jpg.png"(可选) |
targetPath |
string |
QueryString | 存储子目录,支持日期占位符(可选) |
Base64 上传 (uploadFileFromBase64)
| 参数 | 类型 | 说明 |
|---|---|---|
fileDataBase64 |
string |
Base64 文件数据,支持 data:{type};base64,{data} 前缀格式(必填) |
fileName |
string |
文件名,为空时自动生成唯一文件名 |
多文件上传 (uploadFiles / multipleFilesByCategory)
| 参数 | 类型 | 说明 |
|---|---|---|
files |
List<IFormFile> |
要上传的文件列表(必填) |
category |
string |
文件分类(仅 multipleFilesByCategory) |
返回结果 (UploadResult)
| 字段 | 类型 | 说明 |
|---|---|---|
FileName |
string |
原始文件名 |
StoredFileName |
string |
存储后的文件名(可能被重命名) |
FileSize |
long |
文件大小(字节) |
FilePath |
string |
文件存储相对路径(相对于 UploadPath) |
ContentType |
string |
文件的 MIME 类型 |
PreviewUrl |
string |
文件预览 URL(如果支持预览) |
UploadTime |
DateTime |
上传时间(UTC) |
Category |
string |
文件分类名称(如 Images、Documents) |
UploadRoute |
string |
上传路由标识 |
错误处理与日志
错误码速查
组件内部使用 i18n 国际化资源管理错误信息,支持 7 种语言:
| 错误键 | 中文(zh-CN) | 触发场景 |
|---|---|---|
Error_FileCategoryMismatch |
文件分类不匹配:期望类型为 {0},但实际检测到 {1} 类型文件 | 分类上传时文件类型不匹配 |
Error_FileCategoryNotFound |
文件分类不存在:{0} | 路由 category 不在 10 种已注册分类中 |
Error_FileNameTooLong |
文件名长度不能超过 {0} 个字符 | 文件名超过 MaxFileNameLength 限制 |
Error_FileNotFound |
文件不存在:{0} | 预览/下载时文件路径无效 |
Error_InvalidFileNameChars |
文件名包含非法字符 | 文件名包含 Path.GetInvalidFileNameChars() |
Error_UnsupportedSuffix |
不支持的文件后缀:{0},允许:{1} | 文件后缀不在 AllowSuffix 白名单中 |
Error_Base64FormatInvalid |
Base64 格式无效 | Base64 字符串无法解码(FormatException) |
Error_FileSizeExceeded |
文件大小不能超过 {0}MB | 文件超过 MaxFileSize 限制 |
Error_InvalidParameters |
参数无效 | 预览/下载时 filePath 为空 |
Error_UnsupportedExtension |
不支持的文件类型:{0} | 扩展名不在分类配置的白名单中 |
Error_UnsupportedMimeType |
不支持的 MIME 类型:{0} | MIME 类型不在分类配置的白名单中 |
Error_UploadFailed |
上传失败 | 文件写入磁盘时发生异常 |
异常处理示例
// <summary>
// 安全上传文件 —— 捕获验证、格式和 IO 异常。
// 所有异常通过 RestfulResult 返回,而非抛出未处理异常。
// </summary>
public async Task<(bool Success, string Message, UploadResult Data)> SafeUploadAsync(
FileUploadService uploadService, IFormFile file)
{
try
{
// ValidateFile 是公开的非 Action 方法,可独立调用预检
if (!uploadService.ValidateFile(file, out var errorMessage))
{
return (false, errorMessage, null);
}
// 获取文件分类,按分类路径上传
var category = uploadService.GetFileCategory(file);
var result = await uploadService.UploadFileAsync(file, category.ToLower());
return (true, "上传成功", result);
}
catch (IOException ex)
{
// 磁盘空间不足 / 路径不可写
return (false, $"磁盘 IO 错误: {ex.Message}", null);
}
catch (UnauthorizedAccessException ex)
{
// 目录无写权限
return (false, $"文件系统权限不足: {ex.Message}", null);
}
catch (Exception ex)
{
// 其他未预期异常
return (false, $"上传异常: {ex.Message}", null);
}
}
// <summary>
// 安全上传文件 —— 同步写法(仅推荐在测试/控制台环境)
// </summary>
public (bool Success, string Message, UploadResult Data) SafeUpload(
FileUploadService uploadService, IFormFile file)
{
return SafeUploadAsync(uploadService, file).GetAwaiter().GetResult();
}
日志集成
组件通过 RuoVea.ExLog 的 LogFactory 输出日志,关键操作和异常会自动记录:
LogFactory.Info— 目录创建("创建存储目录: {path}")LogFactory.Warn— 非法字符警告("文件名包含非法字符: {file.FileName}")LogFactory.Error— 上传失败异常("文件上传失败: {file.FileName}")
// 日志输出示例(由组件内部自动生成)
// [INFO] 创建存储目录: D:/app/Uploads/Images
// [WARN] 文件名包含非法字符: report?.pdf
// [ERROR] 文件上传失败: report.pdf | System.IO.IOException: 磁盘空间不足
版本迁移指南
从 8.0.0.x 升级到 10.0.0.x
| 变更项 | 说明 |
|---|---|
| TFM 升级 | net8.0 → net10.0,需同步升级依赖包到 10.0.* 版本 |
| 包版本对齐 | RuoVea.DynamicWebApi、RuoVea.ExSugar、RuoVea.ExLog 需升至对应 10.0.* |
| API 兼容 | 所有公开 API 向后兼容,无需修改业务代码 |
| 配置兼容 | UploadConfig 和 FileTypePathMapping 结构无变化 |
| i18n 资源 | 多语言资源文件向后兼容,新语言无需额外迁移 |
API 变更历史
| 版本 | 变更 |
|---|---|
8.0.0.19 |
修复多字段查询时 SqlSugar 因重复参数 @value 键报错的问题 |
8.0.0.18 |
组件版本升级,图表数据缓存 |
8.0.0.17 |
表结构初始化处理 |
| 更早版本 | 初始发布,完整的上传/预览/下载功能,10 种文件分类,i18n 多语言 |
常见问题
Q: 如何只允许上传特定格式的文件?
在文件分类配置的 Extensions 和 MimeTypes 中配置白名单,或在使用统一上传接口时通过 AllowSuffix 指定:
// 配置示例:Documents 分类仅允许 PDF 和 Word
{
"Category": "Documents",
"Extensions": [ ".pdf", ".doc", ".docx" ],
"MimeTypes": [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ]
}
// 代码示例:运行时通过 AllowSuffix 限制
// POST /api/fileUpload/uploadFile
// FormData: file=sample.jpg, allowSuffix=.jpg.png
Q: 如何配置按日期自动分目录存储?
通过统一上传接口的 targetPath 查询参数使用日期占位符:
POST /api/fileUpload/uploadFile?targetPath=photos/:yyyy/:MM/:dd
→ 存储路径:Uploads/photos/2026/06/26/xxx.jpg
Q: 上传的文件如何通过 HTTP 直接访问?
在 Program.cs 中配置静态文件中间件,将上传目录映射为 HTTP 路径:
// <summary>
// 配置上传目录的静态文件访问
// </summary>
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "Uploads")),
RequestPath = "/Uploads"
});
// 访问示例: GET /Uploads/Images/abc123.jpg
当使用绝对路径(如
D:/DevUploads)时,需将PhysicalFileProvider的路径改为对应的绝对路径。
Q: ⚠️ 使用原始文件名时的潜在风险?
启用 UseOriginalFileName = true 时需注意:
- 文件名可能包含特殊字符,组件内部会自动调用
RemoveInvalidFileNameChars过滤 - 文件名过长时自动按
MaxFileNameLength截断 - 中文文件名在 URL 中需要编码,建议前端使用
encodeURIComponent - 同名文件冲突时(
OverwriteExisting = false),自动添加_1、_2后缀
Q: ❗ 大文件上传时的性能调优?
默认情况下接口带有 [DisableRequestSizeLimit] 特性,不受 ASP.NET Core 默认的 30MB 限制。但需注意:
- Kestrel 限制: 在
Program.cs中设置builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = null) - 缓冲策略: 组件使用
FileStream+CopyToAsync流式写入,不将整个文件加载到内存 - 磁盘 IO: 如需高性能,使用 SSD 存储或配置
UploadPath为高速存储路径 - 分类限制: 可在分类配置中为 Videos/Archives 等大文件类型设置独立的
MaxFileSize
// Kestrel 不限制请求体大小
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = null;
});
Q: 如何在其他服务中复用上传功能?
通过 DI 注入 FileUploadService,调用其公开的 [NonAction] 方法:
// <summary>
// 在其他服务中注入 FileUploadService 使用文件上传功能
// </summary>
public class MyBusinessService
{
private readonly FileUploadService _uploadService;
public MyBusinessService(FileUploadService uploadService)
{
_uploadService = uploadService;
}
public async Task<UploadResult> SaveAttachmentAsync(IFormFile file)
{
if (!_uploadService.ValidateFile(file, out var error))
throw new InvalidOperationException(error);
var category = _uploadService.GetFileCategory(file);
return await _uploadService.UploadFileAsync(file, category.ToLower());
}
// GetFileStream 返回 null 而非抛异常,调用前需判空
public Stream GetFile(string path)
{
return _uploadService.GetFileStream(path);
}
}
Q: 如何切换多语言?
默认语言为中文(zh),在请求头中设置 Accept-Language 即可切换:
Accept-Language: en → 英文
Accept-Language: vi → 越南语
Accept-Language: fr → 法语
Accept-Language: ja → 日语
Accept-Language: zh-HK → 中文(香港繁体)
Accept-Language: zh-TW → 中文(台湾繁体)
确保已在 Program.cs 中注册 RequestLocalization 中间件(见快速开始)。
许可证
本项目基于 Apache 2.0 License 开源发布。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- RuoVea.DynamicWebApi (>= 10.0.0)
- RuoVea.ExLog (>= 10.0.0.2)
- RuoVea.ExSugar (>= 10.0.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on RuoVea.OmiApi.Upload:
| Package | Downloads |
|---|---|
|
RuoVea.OmiArticle
文章分类和内容管理 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.0.8 | 88 | 7/17/2026 |
| 10.0.0.7 | 92 | 7/15/2026 |
| 10.0.0.6 | 109 | 6/24/2026 |
| 10.0.0.5 | 100 | 6/9/2026 |
| 10.0.0.4 | 107 | 6/8/2026 |
| 10.0.0.3 | 115 | 5/29/2026 |
| 10.0.0.2 | 110 | 5/28/2026 |
| 9.0.0.4 | 122 | 5/29/2026 |
| 9.0.0.3 | 116 | 5/28/2026 |
| 9.0.0.2 | 100 | 5/28/2026 |
| 8.0.0.21 | 94 | 7/17/2026 |
| 8.0.0.20 | 85 | 7/15/2026 |
| 8.0.0.19 | 126 | 6/24/2026 |
| 8.0.0.18 | 105 | 6/9/2026 |
| 8.0.0.17 | 101 | 6/8/2026 |
| 8.0.0.16 | 121 | 5/29/2026 |
| 7.0.0.9 | 117 | 5/29/2026 |
| 7.0.0.8 | 119 | 5/28/2026 |
| 6.0.0.9 | 132 | 5/29/2026 |
| 6.0.0.8 | 126 | 5/28/2026 |