9. 用20行代码实现文件上传,浏览目录功能 (Blazor server) - densen2014/Blazor100 GitHub Wiki

Blazor组件自做九: 用20行代码实现文件上传,浏览目录功能 (Blazor server)

1. 前言

今天有小伙伴咨询内网环境手机等移动设备怎样快速跟主机交换文件,群友齐齐出力讨论:es文件管理器开web服务,网盘中转,QQ发文件,各种方案各抒己见,好不繁华.

突然想到前段时间Net6正式发布后,带来了一个新的IBrowserFile接口, Blazor 文件上传变得非常便利,小的应用场景直接抛弃传统的Controller形式上传文件,话不多说,直接开撸.

运行截图

2. IBrowserFile接口

属性 解释
ContentType 获取浏览器指定的文件的 MIME 类型。
LastModified 获取浏览器指定的上次修改日期
Name 获取浏览器指定的文件的名称
Size 获取浏览器指定的文件大小(以字节为单位)
方法 解释
OpenReadStream(Int64, CancellationToken) 打开用于读取已上传文件的流
RequestImageFileAsync(IBrowserFile, String, Int32, Int32) 尝试将当前图像文件转换为指定文件类型和最大文件维度的新文件之一

3. 使用 InputFile 组件将浏览器文件数据读入

使用 InputFile 组件将浏览器文件数据读入 .NET 代码。 InputFile 组件呈现 file 类型的 HTML < input > 元素。 默认情况下,用户选择单个文件。 可添加 multiple 属性以允许用户一次上传多个文件。

发生 OnChange (change) 事件时,以下 InputFile 组件执行 LoadFiles 方法。 InputFileChangeEventArgs 提供对所选文件列表和每个文件的详细信息的访问:

<InputFile OnChange="@LoadFiles" multiple />

@code {
   private void LoadFiles(InputFileChangeEventArgs e)
   {
       ...
   }
}

若要从用户选择的文件中读取数据,请对该文件调用 IBrowserFile.OpenReadStream,并从返回的流中读取。OpenReadStream 强制采用其 Stream 的最大大小(以字节为单位)。 读取一个或多个大于 512,000 字节 (500 KB) 的文件会引发异常。 此限制可防止开发人员意外地将大型文件读入到内存中。 如果需要,可以使用 OpenReadStream 上的 maxAllowedSize 参数指定更大的大小。

使用以下方法,因为文件的 是直接提供给使用者的,FileStream 会在提供的路径中创建文件:

await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);

3. 新建BlazorServer工程命名为BlazorFileUpload,也可以cmd来一通

mkdir BlazorFileUpload
cd BlazorFileUpload
dotnet new blazorserver

4. Pages/Index.razor

加入两个上传按钮组件

<div>
    上传文件
    <InputFile OnChange="OnChange" class="form-control" multiple />
</div>
<div style="padding-top:20px">
    上传图片
    <InputFile OnChange="OnChange" class="form-control" multiple accept='image/*' />
</div>

加入浏览目录按钮以及显示上传进度代码块

<div style="padding-top:30px">
    <a class="btn btn-primary" href="Upload">
        <span class="oi oi-file" aria-hidden="true"></span> 浏览文件
    </a>
</div>

<pre>
    <code>
        @uploadstatus
    </code>
</pre>

编写cs代码

@code{
    [Inject] protected Microsoft.AspNetCore.Hosting.IWebHostEnvironment? HostEnvironment { get; set; } //获取IWebHostEnvironment

    protected string UploadPath = "";
    protected string? uploadstatus;
    long maxFileSize = 1024 * 1024 * 15;

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender) return;
        UploadPath = Path.Combine(HostEnvironment!.WebRootPath, "Upload"); //初始化上传路径
        if (!Directory.Exists(UploadPath)) Directory.CreateDirectory(UploadPath); //不存在则新建目录 
    }

    protected async Task OnChange(InputFileChangeEventArgs e)
    {
        int i = 0;
        var selectedFiles = e.GetMultipleFiles(100);
        foreach (var item in selectedFiles)
        {
            i++;
            await OnSubmit(item);
            uploadstatus += Environment.NewLine + $"[{i}]: " + item.Name;
        }
    }

    protected async Task OnSubmit(IBrowserFile efile)
    {
        if (efile == null) return;
        var tempfilename = Path.Combine(UploadPath, efile.Name);
        await using FileStream fs = new(tempfilename, FileMode.Create);
        using var stream = efile.OpenReadStream(maxFileSize);
        await stream.CopyToAsync(fs);
        StateHasChanged();
    }

}

现在运行一下看看效果

可以看到,第一个按钮可以多选文件,并直接成功上传到了工程的wwwroot/Upload目录,不限制格式.

第二个按钮也可以多选文件,但是限制为image/*图片格式.

上传功能完成.

点击 浏览文件 按钮,显示 Sorry, there's nothing at this address. 翻车了......

5. 添加目录浏览功能

打开Program.cs文件,在 app.UseStaticFiles(); 之后加入一句 app.UseDirectoryBrowser(); 就可以启动默认的目录浏览功能, 我们这里加入一点限制,只浏览Upload文件夹,并且把默认的界面英文改为中文,并且按修改时间逆序排序.

右键打开NuGet包管理,安装 AME.SortedDirectoryChs 包, 这是一个DirectoryBrowserFormatter,支持中文界面并按修改时间逆序排序,因为篇幅关系,就不贴源码了, 源码在文末项目内可找到.

文件头部加入引用

using Microsoft.Extensions.FileProviders;
using System.Text.Encodings.Web;

语句app.UseDirectoryBrowser();修改为

var dir = Path.Combine(app.Environment.WebRootPath, "Upload");
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

var opt = new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(dir),
    Formatter = new AME.HtmlDirectoryFormatterChsSorted(HtmlEncoder.Default),
    RequestPath = new PathString("/Upload")
}; 
app.UseDirectoryBrowser(opt);

现在运行一下看看效果

点击 浏览文件 按钮,显示了预期的界面,修复上一节翻车事件.

6. 发布工程.

用命令行发布的大佬跳过. 😅

右键发布,选择发布到文件夹,然后来个单文件发布试试

打开发布后的目录,双击运行,复制链接到浏览器,运行正常

查找本机局域网ip,在手机上打开

咦?又翻车了?

原因是默认只监听了http://localhost:5000,外部是不能访问的.

有很多方式可设置, 可用命令行加 --Urls 指定 dotnet run --urls "http://localhost:5100" , 也可以配置 webBuilder.UseUrls 方式指定.

7. 使用配置文件指定监听地址

打开 appsettings.json 文件,加入一行

  "UseUrls": "http://localhost:8000;http://0.0.0.0:8000;",

完整文件如下

{
  "UseUrls": "http://localhost:8000;http://0.0.0.0:8000;", //加入这句
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

打开Program.cs文件,在 var builder = WebApplication.CreateBuilder(args); 之后加入一句 builder.WebHost.UseUrls(builder.Configuration["UseUrls"]);

完整Program.cs代码

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using BlazorFileUpload.Data;
using Microsoft.Extensions.FileProviders;
using System.Text.Encodings.Web;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseUrls(builder.Configuration["UseUrls"]); //加入这句

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

//设置文件上传的大小限制 , 默认值128MB
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = long.MaxValue;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

var dir = Path.Combine(app.Environment.WebRootPath, "Upload");
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

var opt = new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(dir),
    Formatter = new AME.HtmlDirectoryFormatterChsSorted(HtmlEncoder.Default),
    RequestPath = new PathString("/Upload")
};
app.UseDirectoryBrowser(opt);

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

8. 一些锦上添花的小处理

获取本机IP,生成外部链接按钮分发复制给移动设备用. 脑洞一下可以扩展二维码扫码上传等功能.

LocalIP.cs

using System.Net;
using System.Net.Sockets;

namespace BlazorFileUpload
{
    public class LocalIP
    {
        public static string GetLocalIp()
        {
            //得到本机名 
            string hostname = Dns.GetHostName();
            //解析主机名称或IP地址的system.net.iphostentry实例。
            IPHostEntry localhost = Dns.GetHostEntry(hostname);
            if (localhost != null)
            {
                foreach (IPAddress item in localhost.AddressList)
                {
                    //判断是否是内网IPv4地址
                    if (item.AddressFamily == AddressFamily.InterNetwork)
                    {
                        return item.MapToIPv4().ToString();
                    }
                }
            }
            return "0.0.0.0";
        }
    }
}

获取本机局域网IP string? ip = LocalIP.GetLocalIp();

从配置文件分离端口号 string? port = Config!["UseUrls"].ToString().Split(';')[0].Split(':')[2];

Index.razor 添加外部地址按钮

    <a class="btn btn-primary" href="@($"http://{ip}:{port}")" target="_blank">
        <span class="oi oi-browser" aria-hidden="true"></span> 外部地址
    </a>

完整Index.razor代码

@page "/"

<PageTitle>BlazorFileUpload</PageTitle>


<div>
    上传文件(Max100)
    <InputFile OnChange="OnChange" style="max-width:400px" class="form-control" multiple />
</div>
<div style="padding-top:20px">
    上传图片
    <InputFile OnChange="OnChange" style="max-width: 400px" class="form-control" multiple accept='image/*' />
</div>
<div style="padding-top:30px">
    <a class="btn btn-primary" href="Upload">
        <span class="oi oi-file" aria-hidden="true"></span> 浏览文件
    </a>
    <a class="btn btn-primary" href="@($"http://{ip}:{port}")" target="_blank">
        <span class="oi oi-browser" aria-hidden="true"></span> 外部地址
    </a>
</div>

<pre>
    <code>
        @uploadstatus
    </code>
</pre>

<button class="btn btn-info" @onclick="(()=>help=!help)">说明</button>
@if (help)
{
<pre>
你的IP @ip
<code>
        配置:
        appsettings.json 文件
        自由修改监听ip和端口
        "UseUrls": "@Config!["UseUrls"]" //默认 "http://localhost:8000;https://0.0.0.0:8000;"
        
</code>
</pre>
}


@code{
    [Inject] protected Microsoft.AspNetCore.Hosting.IWebHostEnvironment? HostEnvironment { get; set; }
    [Inject] protected IConfiguration? Config { get; set; }

    protected string UploadPath = "";

    protected string? tempfilename = null;
    protected bool displayProgress;
    protected string? uploadstatus;
    long maxFileSize = 1024 * 1024 * 15;
    string? ip;
    string? port;
    bool help;

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender) return;
        UploadPath = Path.Combine(HostEnvironment!.WebRootPath, "Upload");
        if (!Directory.Exists(UploadPath)) Directory.CreateDirectory(UploadPath);
        ip = LocalIP.GetLocalIp();
        try
        {
            port = Config!["UseUrls"].ToString().Split(';')[0].Split(':')[2];
        }
        catch
        {
            port = "8000";
        }
        StateHasChanged();
    }

    protected async Task OnChange(InputFileChangeEventArgs e)
    {
        int i = 0;
        var selectedFiles = e.GetMultipleFiles(100);
        foreach (var item in selectedFiles)
        {
            i++;
            await OnSubmit(item);
            uploadstatus += Environment.NewLine + $"[{i}]: " + item.Name;
        }
    }

    protected async Task OnSubmit(IBrowserFile efile)
    {
        if (efile == null) return;
        var tempfilename = Path.Combine(UploadPath, efile.Name);
        await using FileStream fs = new(tempfilename, FileMode.Create);
        using var stream = efile.OpenReadStream(maxFileSize);
        displayProgress = true;
        await stream.CopyToAsync(fs);
        displayProgress = false;
        StateHasChanged();
    }

}

7. 写在最后

这个小工程单文件框架依赖打包win-x64平台999KB,压缩分发280KB,香不香.

国内环境有相当大一批人一直不愿意接触 .Net5, .Net6 总守着老的技术不放,不愿意接受新的改变,现在.Net生态环境变得越来越开发越来越顺手,再加上Blazor这个新事物,还有什么可以犹豫的呢?跟着我们一起玩玩Blazor吧!

8. AME.SortedDirectoryChs库 源码

https://gitee.com/densen2014/Densen.Extensions/tree/master/HtmlDirectoryFormatterExtensions

⚠️ **GitHub.com Fallback** ⚠️