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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RuoVea.OmiApi.Upload" Version="10.0.0.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RuoVea.OmiApi.Upload" Version="10.0.0.8" />
                    
Directory.Packages.props
<PackageReference Include="RuoVea.OmiApi.Upload" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RuoVea.OmiApi.Upload --version 10.0.0.8
                    
#r "nuget: RuoVea.OmiApi.Upload, 10.0.0.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RuoVea.OmiApi.Upload@10.0.0.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RuoVea.OmiApi.Upload&version=10.0.0.8
                    
Install as a Cake Addin
#tool nuget:?package=RuoVea.OmiApi.Upload&version=10.0.0.8
                    
Install as a Cake Tool

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 端点。

实际路由前缀取决于 AddDynamicWebApiDefaultApiPrefix 配置,生产环境一般设置为 /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 "" 文件分类名称(如 ImagesDocuments)。必填的唯一标识
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

分类配置优先级高于全局配置。每条分类配置中的值为默认值(0null、空数组)时,自动回退到全局配置。这使得每个分类可以只覆盖需要的配置项。

日期占位符

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.ExLogLogFactory 输出日志,关键操作和异常会自动记录:

  • 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.0net10.0,需同步升级依赖包到 10.0.* 版本
包版本对齐 RuoVea.DynamicWebApiRuoVea.ExSugarRuoVea.ExLog 需升至对应 10.0.*
API 兼容 所有公开 API 向后兼容,无需修改业务代码
配置兼容 UploadConfigFileTypePathMapping 结构无变化
i18n 资源 多语言资源文件向后兼容,新语言无需额外迁移

API 变更历史

版本 变更
8.0.0.19 修复多字段查询时 SqlSugar 因重复参数 @value 键报错的问题
8.0.0.18 组件版本升级,图表数据缓存
8.0.0.17 表结构初始化处理
更早版本 初始发布,完整的上传/预览/下载功能,10 种文件分类,i18n 多语言

常见问题

Q: 如何只允许上传特定格式的文件?

在文件分类配置的 ExtensionsMimeTypes 中配置白名单,或在使用统一上传接口时通过 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 限制。但需注意:

  1. Kestrel 限制:Program.cs 中设置 builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = null)
  2. 缓冲策略: 组件使用 FileStream + CopyToAsync 流式写入,不将整个文件加载到内存
  3. 磁盘 IO: 如需高性能,使用 SSD 存储或配置 UploadPath 为高速存储路径
  4. 分类限制: 可在分类配置中为 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed