⭐️ Furion v4 版本支持【所有历史版本】无缝升级,一套代码兼容 .NET 5+ ⭐️
Skip to main content

31. 虚拟文件系统

版本说明

以下内容仅限 Furion 2.5.0 + 版本使用。

31.1 关于文件系统

本章所谓的 文件系统 有点名不副实,其实根本算不上一个系统,它仅仅是利用一个抽象化的 IFileProvider 以统一的方式提供所需的文件而已。通过该 文件系统 可以读取物理文件和嵌入资源文件,包括目录结果读取,文件内容读取,文件内容监听等等。

31.1.1 文件系统类型

Furion 提供了两种文件系统类型:

  • Physical:物理文件系统类型,也就是物理机中实际存在的文件
  • Embedded:嵌入资源文件系统类型,也就是资源文件嵌入到了程序集中,常用于模块化开发

31.2 注册虚拟文件系统服务

services.AddVirtualFileServer();

31.3 获取文件系统 IFileProvider 实例

31.3.1 Func<FileProviderTypes, object, IFileProvider> 方式

Furion 框架提供了 Func<FileProviderTypes, object, IFileProvider> 委托供构造函数注入或解析服务,如:

public class PersonServices{    private readonly IFileProvider _physicalFileProvider;    private readonly IFileProvider _embeddedFileProvider;    public PersonServices(Func<FileProviderTypes, object, IFileProvider> fileProviderResolve)    {        // 解析物理文件系统        _physicalFileProvider = fileProviderResolve(FileProviderTypes.Physical, @"c:/test");        // 解析嵌入资源文件系统        _embeddedFileProvider = fileProviderResolve(FileProviderTypes.Embedded, Assembly.GetEntryAssembly());    }}

31.3.2 FS 静态类方式

Furion 框架也提供了 FS 静态类方式创建,如:

// 解析物理文件系统var physicalFileProvider = FS.GetPhysicalFileProvider(@"c:/test");// 解析嵌入资源文件系统var embeddedFileProvider = FS.GetEmbeddedFileProvider(Assembly.GetEntryAssembly());

31.4 IFileProvider 常见操作

31.4.1 读取文件内容

byte[] buffer;using (Stream readStream = _fileProvider.GetFileInfo("你的文件路径").CreateReadStream()){    buffer = new byte[readStream.Length];    await readStream.ReadAsync(buffer.AsMemory(0, buffer.Length));}// 读取文件内容var content = Encoding.UTF8.GetString(buffer);

31.4.2 获取文件目录内容(需递归查找)

var rootPath = "当前目录路径";var fileinfos = _fileProvider.GetDirectoryContents(rootPath);foreach (var fileinfo in fileinfos){    if(fileinfo.IsDirectory)    {        // 这里递归。。。    }}

31.4.4 监听文件变化

ChangeToken.OnChange(() => _fileProvider.Watch("监听的文件"), () =>{    // 这里写你的逻辑});

31.5 模块化静态资源配置

通常我们采用模块化开发,静态资源都是嵌入进程序集中,这时候我们需要通过配置 UseFileServer 指定模块静态资源路径,如:

// 默认静态资源调用,wwwrootapp.UseStaticFiles();// 配置模块化静态资源app.UseFileServer(new FileServerOptions{    FileProvider = new EmbeddedFileProvider(模块程序集),    RequestPath = "/模块名称",  // 后续所有资源都是通过 /模块名称/xxx.css 调用    EnableDirectoryBrowsing = true});

31.6 文件上传下载

在应用开发中,文件上传下载属于非常常用的功能,这里贴出常见的文件上传下载示例。

31.6.1 文件下载

  • 文件路径的方式
[HttpGet, NonUnify]public IActionResult FileDownload(){    string filePath = "这里获取完整的文件下载路径";    return new FileStreamResult(new FileStream(filePath, FileMode.Open), "application/octet-stream") {        FileDownloadName = fileName // 配置文件下载显示名    };}
  • byte[] 方式
[HttpGet, NonUnify]public IActionResult FileDownload(){    return new FileStreamResult(byte数组, "application/octet-stream") {        FileDownloadName = fileName // 配置文件下载显示名    };}
关于前端获取文件名

如果前端获取不到文件夹,可添加以下配置:

_httpContextAccessor.HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename={文件名}");_httpContextAccessor.HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");

31.6.2 文件上传

小提醒

IFormFile 类型对应前端的 Content-Type 为: multipart/form-data

  • 单文件 IFormFile 类型参数(存储到硬盘)
[HttpPost]public async Task<IActionResult> UploadFileAsync(IFormFile file){    // 如:保存到网站根目录下的 uploads 目录    var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads");    if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath);    //// 这里还可以获取文件的信息    // var size = file.Length / 1024.0;  // 文件大小 KB    // var clientFileName = file.FileName; // 客户端上传的文件名    // var contentType = file.ContentType; // 获取文件 ContentType 或解析 MIME 类型    // 避免文件名重复,采用 GUID 生成    var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);    var filePath = Path.Combine(savePath, fileName);    // 保存到指定路径    using (var stream = System.IO.File.Create(filePath))    {        await file.CopyToAsync(stream);    }    // 在动态 API 直接返回对象即可,无需 OK 和 IActionResult    return Ok(new { filename });}
  • 单文件 Base64 类型参数(存储到硬盘)
[HttpPost]public async Task UploadFileAsync([FromBody] string fileBase64, string clientFileName){    // 如:保存到网站根目录下的 uploads 目录    var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads");    if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath);    // 将 base64 字符串转 byte[]    var bytes = Convert.FromBase64String(fileBase64);    // 这里还可以获取文件的信息    // var size = bytes.Length / 1024.0;  // 文件大小 KB    // 避免文件名重复,采用 GUID 生成    var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(clientFileName);    var filePath = Path.Combine(savePath, fileName);    // 保存到指定路径    using (var fs = new FileStream(filePath, FileMode.Create))    {        await fs.WriteAsync(bytes);    }    // 在动态 API 直接返回对象即可,无需 OK 和 IActionResult    return Ok(new { fileName });}
特别注意

文件 Base64 字符串如果带 data:text/plain;base64, 开头则,需要手动去掉 , 之前(含逗号)的字符串。

  • 多文件 List<IFormFile> 类型参数(存储到硬盘)

代码和 单文件处理一致,只需 foreach 即可。

[HttpPost]public async Task<IActionResult> UploadFileAsync(List<IFormFile> files){    // 保存到网站根目录下的 uploads 目录    var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads");    if(!Directory.Exists(savePath)) Directory.CreateDirectory(savePath);    // 总上传大小    long size = files.Sum(f => f.Length);    // 遍历所有文件逐一上传    foreach (var formFile in files)    {        if (formFile.Length > 0)        {            // 避免文件名重复,采用 GUID 生成            var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(clientFileName);            var filePath = Path.Combine(savePath, fileName);            // 保存到指定路径            using (var stream = System.IO.File.Create(filePath))            {                await formFile.CopyToAsync(stream);            }        }    }    // 在动态 API 直接返回对象即可,无需 OK 和 IActionResult    return Ok(new { count = files.Count, size });}
  • 多文件 List<string> Base64 类型参数(存储到硬盘)

代码和 单文件处理一致,只需 foreach 即可(参上)。

31.6.3 将 IFormFilebyte[]

有时候我们需要将文件转换成 byte[] 存储到数据库,而不是存储到硬盘中。

[HttpPost]public async Task<IActionResult> UploadFileAsync(IFormFile file){    var fileLength = file.Length;    using var stream = file.OpenReadStream();    var bytes = new byte[fileLength];    stream.Read(bytes, 0, (int)fileLength);    // 这里将 bytes 存储到你想要的介质中即可}
便捷拓展方法

在 Furion v3.2.0 新增了 IFormFileToByteArray 拓展,如:

[HttpPost]public async Task<IActionResult> UploadFileAsync(IFormFile file){    var bytes = file.ToByteArray();    // 这里将 bytes 存储到你想要的介质中即可}

31.6.4 将 byte[] 输出为 Url 地址

由于一些项目直接将文件二进制存储在数据库中,读取到内存的时候都是 byte[] 数组,比如我们将图片文件存储在数据库中,然后前端通过 Url 链接进行访问,这个时候就需要将 byte[] 转换为有效的资源路径格式,如:

[NonUnify, HttpGet, AllowAnonymous]public async Task<IActionResult> attachment(string resourceId){    // 根据 resourceId 查询 byte[] 字节数组和 content-type    // 返回 FileContentResult 类型    return new FileContentResult(字节数组,content-type);}

之后我们就可以通过 https://localhost/attachment/资源id 访问文件或图片了。

31.7 请求大小控制(上传文件大小控制)

Web 项目中,KestrelHttpSys 都强制实施 30M (~28.6MiB) 的最大请求正文大小限制,如果请求正文大小超过配置的最大请求正文大小限制,则引发 Request body too large. The max request body size is xxxxx 异常,状态码为 413500

31.7.1 对特定的接口进行控制

可通过 [RequestSizeLimit] 特性进行特定限制

[HttpPost][RequestSizeLimit(100_000_000)]public IActionResult MyAction([FromBody] MyViewModel data){}

31.7.2 对特定接口取消限制

如果不需要对请求大小进行限制,也就是支持提交无限大小,则贴 [DisableRequestSizeLimit] 特性即可。

31.7.3 通用中间件进行控制

我们也可以通过中间件的方式在 Startup.cs 中进行配置:

app.Run(async context =>{    context.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize = 100_000_000; // 设置 null 就是不限制,具体值就是限制最大多少 M}

如果设置 MaxRequestBodySizenull ,则等同于取消限制,也就是 [DisableRequestSizeLimit] 的效果。

小注意

有时候配置了中间件效果发现没起作用,很有可能和中间件顺序有关,可以通过 .IsReadOnly 属性判断,如果为 true ,说明你的配置无效,只有 false 才有效。

31.7.4 全局配置

  • IIS 方式:
  1. 开发环境(IISExpress)

Web 启动层(通常是 XXX.Web.Entry)根目录下创建 web.config 文件,内容如下:

<?xml version="1.0" encoding="utf-8"?><configuration>    <system.webServer>        <security>            <requestFiltering>                <requestLimits maxAllowedContentLength="1073741824" />            </requestFiltering>        </security>    </system.webServer></configuration>
  1. 生产环境

通常生产环境 IIS 自动项目添加了 web.config 文件,这时候只需要在 <configuration> 节点下添加下面内容即可:

<system.webServer>    <security>        <requestFiltering>            <requestLimits maxAllowedContentLength="1073741824" />        </requestFiltering>    </security></system.webServer>
  • Kestrel 方式:
小知识

未使用 IIS 托管时,ASP.NET Core 默认使用 Kestrel 方式。

// .NET5 方式,在 .ConfigureWebHostDefaults 里面配置.UseStartup<Startup>().UseKestrel(options =>{    options.Limits.MaxRequestBodySize = null;   // 设置 null 就是不限制,具体值就是限制最大多少 M}// .NET6 方式,在 progame.cs 文件 var app = builder.Build(); 之后配置app.Configuration.Get<WebHostBuilder>().ConfigureKestrel(options =>{    options.Limits.MaxRequestBodySize = null;   // 设置 null 就是不限制,具体值就是限制最大多少 M});
  • HttpSys 方式:
小知识

HTTP.sys 是仅在 Windows 上运行的适用于 ASP.NET CoreWeb 服务器。 HTTP.sysKestrel 服务器的替代选择,提供了一些 Kestrel 不提供的功能。

// .NET5 方式同上.UseHttpSys(options =>{    options.MaxRequestBodySize = 100_000_000;   // 设置 null 就是不限制,具体值就是限制最大多少 M}// .NET6 方式同上

31.8 反馈与建议

与我们交流

给 Furion 提 Issue


演练场