背景:年前有个需求,要求在前端页面中展示某路径下的文件和文件夹等信息,同时拥有文件上传下载、文件夹创建等功能。最初选定的方案是在文件存储服务器中开放一个FTP路径(需登录)出来进行文件操作,为此写了一个模拟FTP功能的组件(文章:Bootstrap Blazor中实现模拟FTP功能的组件-CSDN博客)。但是最终的效果并不理想,尤其是在大文件下载方面,由于要事先登陆,没法直接把FTP地址传给服务器下载(直接给下载地址的话,用户名和密码是明文存在地址中的,十分不安全)。转而用Stream流进行下载,但速度很慢,连服务器网速(100M)的十分之一都达不到,大概只有3~5M,原因大致是FTP下载是直接读取磁盘中的文件。没有找到提速的解决方案(如有大佬知道,欢迎评论),故只能转变方案,最后选定MinIO。
MinIO简单介绍:是一个高性能、轻量级的开源对象存储服务,专为云原生和容器化环境设计。它与 Amazon S3 API 兼容,支持大规模数据存储和管理,广泛应用于私有云、混合云和边缘计算场景。采用分布式架构和高效的纠删码(Erasure Coding)技术,提供高吞吐量和低延迟,适合处理海量非结构化数据(如图片、日志、视频等)。支持客户端和服务端加密(AES-256)、SSL/TLS 传输加密、细粒度权限控制(IAM、策略管理)及审计日志,满足企业级安全需求。
MinioShown组件效果图如下:
本篇文章中,BB的版本是9.2.6,MinIO用的版本是6.0.4,MinIO的帮助类放在文章的最后。下面详细讲下MinioShown组件的编写:
1、在appsettings.json中配置OSS
"OSS_Settings": {"EndPoint": "oss.xxx.com",//OSS终端"EnableSSL": true,//是否启用SSL"AccessKey_ID": "xxxxxx",//登录ID"Access_Key_Secret": "xxxxxxxxx",//登录密钥"BucketUrl": "https://oss.xxx.com/bucket",//桶地址"BucketName": "bucket",//桶名"DirName": "Dir/",//文件存储在桶里面的文件夹
}
2、编写MinioShown.razor
@if (IsLoading)
{<SkeletonTree />
}
else
{<div class="d-flex flex-column minio-shown-div"><div class="d-flex path-text-div"><div class="back-div"><Button Size="Size.ExtraSmall" Color="Color.Primary" IsAsync="true" OnClick="@OnClickToBack">返回</Button></div><div class="display-div"><Display @bind-Value="@ShowPath"></Display></div></div><div class="d-flex operation-div"><div class="add-folder-div" style="margin-right:8px;"><PopConfirmButton Placement="Placement.Bottom"ConfirmIcon="fa-solid fa-triangle-exclamation text-info"Color="Color.Primary"Size="Size.Small"ConfirmButtonColor="Color.Info"Text="新建文件夹" OnConfirm="() => OnClickToAddFolder()"><BodyTemplate><div class="d-flex align-items-center"><span class="me-2">名称: </span><BootstrapInput TValue="string" @bind-Value="@NewFolderName" style="width:11rem;" /></div></BodyTemplate></PopConfirmButton></div><div class="upload-div" style="margin-right:8px;"><ButtonUpload TValue="string" BrowserButtonText="上传文件" Size="Size.Small" ShowUploadFileList="false" IsMultiple="true" OnChange="@OnClickToUpload"></ButtonUpload></div><div style="margin-right:8px;"><Button Size="Size.Small" Color="Color.Primary" IsAsync="true" OnClick="@OnClickToSelectAllFiles">全选</Button></div><div style="margin-right:8px;"><Button Size="Size.Small" Color="Color.Primary" IsAsync="true" OnClick="@OnClickToUnSelectAllFiles">取消全选</Button></div><div class="d-flex align-items-center" style="margin-right:8px;"><div style="margin-right:5px;display:@(HideBatchDownloadLoading ? "none" : "")"><Spinner Size="Size.Small"></Spinner></div><Button Size="Size.Small" Color="Color.Primary" IsAsync="true" OnClick="@OnClickToBatchDownload">批量下载</Button></div><div class="upload-progress" style="display:@(ShowUploadProgress ? "" : "none");"><div class="d-flex align-items-center" style="margin-left: 5px;"><span>文件上传进度:</span><progress value="@_progress" max="100"></progress><span>@_progress.ToString("0.00")%</span><div style="margin-left: 5px;">@NowUploadFilename</div></div></div></div><div class="path-content-div">@if (ItemList == null || ItemList.Count <= 0){<div>此文件夹为空</div>}else{@for (int i = 0; i < ItemList.Count; i++){MinioItem item = ItemList[i];<div class="d-flex item-div" style="user-select: none;-webkit-user-select: none;background-color:@(i % 2 == 0 ? "#F4F6FA" : "#fff");" @ondblclick="() => OnClickInFolder(item)"><div class="d-flex item-name-div"><div style="margin-right:5px;"><Checkbox TValue="bool" @bind-Value="@item.IsCheck" IsDisabled="@item.IsDir" /></div><div style="margin-right:5px;display:@(item.HideLoading ? "none" : "")"><Spinner Size="Size.Small"></Spinner></div>@if (item.IsDir){<i class="fa-solid fa-folder"></i>}else{<i class="fa-solid fa-file"></i>}@item.Name</div><div class="item-modify-date-div"><span>@item.LastModifiedDateTime</span></div><div class="item-type-div"><span>@item.Type</span></div><div class="item-size-div"><span>@item.Size</span></div><div class="d-flex downlaod-div">@if (item.IsDir == false){<div style="margin-right:5px;display:@(item.HideDownloadLoading ? "none" : "")"><Spinner Size="Size.Small"></Spinner></div><Button TooltipText="下载文件" TooltipPlacement="Placement.Bottom" TooltipTrigger="hover" ButtonStyle="ButtonStyle.Circle" Size="Size.Small" IsAsync="true" IsBlock="false" IsOutline="false" Icon="fa-solid fa-file-arrow-down" Color="Color.None" OnClick="async () => await OnClickToDownload(item)"></Button>}</div></div>}}</div></div>
}
补充:
a、MinIO不能只创建空的文件夹,必须上传文件成功后,这个空文件夹才会保留;
b、使用双击进入文件夹,类似文件资源管理器效果,多加了转圈圈进度(应对网络问题);
c、行加了style="user-select: none;-webkit-user-select: none;"样式,避免每次双击进去都会选中文本。
3、编写MinioShown.razor.cs
using Microsoft.JSInterop;
using Minio.DataModel;namespace MinIO.Components
{public partial class MinioShown{/// <summary>/// 注入弹窗提示,替换为自己的/// </summary>[Inject][NotNull]private MessageBox? _MsgBox { get; set; }/// <summary>/// 注入MinIO帮助类/// </summary>[Inject][NotNull]private MinioService? _minioService { get; set; }/// <summary>/// 注入Js运行时/// </summary>[Inject][NotNull]private IJSRuntime? JSRuntime { get; set; }/// <summary>/// 传给组件默认打开的文件夹/// </summary>[Parameter][NotNull]public string FolderPath { get; set; }/// <summary>/// 是否加载/// </summary>private bool IsLoading { get; set; } = true;/// <summary>/// 文本框里展示的文件夹层级/// </summary>private string ShowPath { get; set; }/// <summary>/// 新建的文件夹名称/// </summary>private string NewFolderName { get; set; }/// <summary>/// 批量下载的转圈圈是否显示/// </summary>private bool HideBatchDownloadLoading { get; set; } = true;/// <summary>/// 某个路径下的Objects/// </summary>private List<MinioItem> ItemList { get; set; } = new List<MinioItem>();/// <summary>/// 上传进度/// </summary>private double _progress { get; set; } = 0;/// <summary>/// 上传的文件名/// </summary>private string NowUploadFilename { get; set; }/// <summary>/// 是否展示进度条,默认触发文件上传时显示/// </summary>private bool ShowUploadProgress { get; set; } = false;protected override async Task OnAfterRenderAsync(bool firstRender){try{await base.OnAfterRenderAsync(firstRender);if (firstRender){await OnLoadData(FolderPath);IsLoading = false;ShowPath = FolderPath;await InvokeAsync(StateHasChanged);}}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}finally{IsLoading = false;}}/// <summary>/// 读取路径下的对象/// </summary>/// <param name="prefix"></param>/// <returns></returns>private async Task OnLoadData(string prefix){ItemList = await _minioService.ListObjectsArgs(prefix);}#region 按钮/// <summary>/// 返回/// </summary>/// <returns></returns>private async Task OnClickToBack(){try{var ss = ShowPath.Replace(FolderPath, "").Trim('/').Split('/').ToList();ss.RemoveAt(ss.Count - 1);string remotePath = string.Join("/", ss);ShowPath = string.IsNullOrWhiteSpace(remotePath) ? FolderPath : (FolderPath + remotePath + "/");await OnLoadData(ShowPath);await InvokeAsync(StateHasChanged);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}/// <summary>/// 新建文件夹/// </summary>/// <returns></returns>private async Task OnClickToAddFolder(){try{ShowPath = ShowPath + NewFolderName + "/";await OnLoadData(ShowPath);await InvokeAsync(StateHasChanged);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}/// <summary>/// 上传文件/// </summary>/// <param name="file"></param>/// <returns></returns>private async Task OnClickToUpload(UploadFile file){try{if (ShowUploadProgress == false){ShowUploadProgress = true;}NowUploadFilename = file.GetFileName();_progress = 0;await InvokeAsync(StateHasChanged);long fileSizeBytes = file.File.Size;using var stream = new MemoryStream();await file.File.OpenReadStream(fileSizeBytes).CopyToAsync(stream);stream.Position = 0;var progress = new Progress<ProgressReport>(async progress =>{if (fileSizeBytes > 0){_progress = (double)progress.TotalBytesTransferred / fileSizeBytes * 100;}else{_progress = 0;}await InvokeAsync(StateHasChanged);});bool res = await _minioService.UploadFileAsync(ShowPath + file.GetFileName(), stream, progress: progress);await OnLoadData(ShowPath);_progress = 100;await InvokeAsync(StateHasChanged);_MsgBox.Show($"【{file.GetFileName()}】文件上传成功!", "", AlertTypes.Success, 2000);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}/// <summary>/// 全选/// </summary>/// <returns></returns>private Task OnClickToSelectAllFiles(){try{foreach (var item in ItemList){if (item.IsDir == false){item.IsCheck = true;}}StateHasChanged();}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}return Task.CompletedTask;}/// <summary>/// 取消全选/// </summary>/// <returns></returns>private Task OnClickToUnSelectAllFiles(){try{foreach (var item in ItemList){item.IsCheck = false;}StateHasChanged();}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}return Task.CompletedTask;}/// <summary>/// 批量下载/// </summary>/// <returns></returns>private async Task OnClickToBatchDownload(){try{var batchFiles = ItemList.Where(d => d.IsCheck);if (batchFiles == null || batchFiles.Count() <= 0){_MsgBox.Show("请先勾选当前路径下的文件!", "", AlertTypes.Error, 2000);return;}//注意使用Thread,不然转圈圈不会生效HideBatchDownloadLoading = false;await InvokeAsync(StateHasChanged);foreach (var item in batchFiles){//转"application/octet-stream"await ConvertContentType(item.Key);}var thread = new Thread(() => BatchDownloadFile(batchFiles));thread.IsBackground = true;thread.Start();}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}private async Task BatchDownloadFile(IEnumerable<MinioItem> batchFiles){try{var urls = batchFiles.Select(d => ConfigurationExtension.GetField(d.KeyF)).ToArray();var fileNames = batchFiles.Select(d => d.Name).ToArray();await JSRuntime.InvokeVoidAsync("downloadFiles", urls, fileNames);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}finally{HideBatchDownloadLoading = true;await InvokeAsync(StateHasChanged);}}/// <summary>/// 双击进入文件夹/// </summary>/// <param name="item"></param>private async Task OnClickInFolder(MinioItem item){try{//注意校验HideLoading,防止网络问题导致多次双击触发if (item.IsDir == false || item.HideLoading == false){return;}item.HideLoading = false;await InvokeAsync(StateHasChanged);Thread thread = new Thread(() =>{InvokeAsync(async () =>{try{ShowPath += (item.Name + "/");await OnLoadData(ShowPath);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}finally{item.HideLoading = true;StateHasChanged();}});});thread.IsBackground = true;thread.Start();}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}/// <summary>/// 单个文件下载/// </summary>/// <param name="item"></param>/// <returns></returns>private async Task OnClickToDownload(MinioItem item){try{//转"application/octet-stream"await ConvertContentType(item.Key);string url = ConfigurationExtension.GetField(item.KeyF);await JSRuntime.InvokeVoidAsync("downloadFile", url, item.Name);}catch (Exception ex){_MsgBox.Show(ex.Message, AlertTypes.Error);}}/// <summary>/// 转文件的ContentType类型/// </summary>/// <param name="objectName"></param>/// <returns></returns>private async Task ConvertContentType(string objectName){bool compare = await _minioService.CompareContentType(objectName);if (compare == false){await _minioService.CopyToReplaceFile(objectName);}}#endregion}
}
补充:
a、通过MinIO上传的图片、HTML文件等,它的Content-Type如果不是“application/octet-stream”,点击下载会变成预览,从而无法下载,解决方案是在上传时给定或者在下载前转换(通过MinIO的CopyObjectAsync方法,CopyObjectArgs设置参数WithReplaceMetadataDirective(true)来替换元数据;或者将文件转字节,通过js的Blob绑定新的type)。
4、编写MinioShown.razor.css
.minio-shown-div {position: relative;width: 100%;height: 600px;
}.path-text-div {width: 100%;align-items: center;
}.back-div {width: 50px;
}.display-div {width: calc(100% - 50px);
}.operation-div {margin-top: 5px;
}.path-content-div {width: 100%;height: calc(100% - 80px);padding: 5px;margin-top: 5px;overflow-y: auto;
}.item-div {align-items: center;justify-content: flex-start;margin-bottom: 0.5rem;height: 24px;
}.item-name-div {align-items: center;width: 700px;
}.item-name-div i {margin-right: 0.5rem;}.item-modify-date-div {width: 140px;
}.item-type-div {width: 100px;
}.item-size-div {width: 80px;
}.downlaod-div {width: 50px;padding-left: 5px;justify-content: center;align-items: center;
}
5、编写下载文件的MyFunc.js
//下载单个文件
window.downloadFile = function downloadFile(url, fileName) {const a = document.createElement('a');a.href = url;a.download = fileName;document.body.appendChild(a);a.click();document.body.removeChild(a);
}//批量下载文件
window.downloadFiles = function downloadFiles(urls, fileNames) {urls.forEach((url, index) => {const fileName = fileNames[index];//设置延迟,防止冲突setTimeout(() => {downloadFile(url, fileName);}, index * 500);});
}//通过字节自定义type下载单个文件
window.downloadFileFromFileBytes = function downloadFileFromFileBytes(fileName, fileBytes) {// 将文件字节转换为 Blobconst blob = new Blob([new Uint8Array(fileBytes)], { type: "application/octet-stream" });const url = URL.createObjectURL(blob);// 创建下载链接并点击触发下载const a = document.createElement("a");a.href = url;a.download = fileName;document.body.appendChild(a);a.click();// 清理 URL 和元素URL.revokeObjectURL(url);document.body.removeChild(a);
}
补充:
a、通过地址下载单个文件可以使用BB的DownloadService下载服务(内部也是基于类似downloadFile方法实现);
b、通过地址批量下载文件时,需要使用downloadFiles方法实现,每个任务之间间隔500ms(可自行调整),避免操作a标签时冲突;
c、这个MyFunc.js是全局的,在wwwroot的js文件夹中,需要在项目的_Host.cshtml文件的body中引入(最好放到最后引入),方式如下:
<script src="~/js/MyFunc.js"></script>
6、MinioShown组件调用方式
[Inject]
[NotNull]
private DialogService _DialogService { get; set; }private async Task ClickToOpenMinIOShown()
{await _DialogService.Show(new DialogOption(){Title = $"文件浏览",ShowCloseButton = false,Size = Size.ExtraExtraLarge,BodyTemplate = BootstrapDynamicComponent.CreateComponent<MinioShown>(new Dictionary<string, object?>{[nameof(MinioShown.FolderPath)] = "MyTest/"}).Render()});
}
7、MinIO帮助类MinioService.cs
using Minio;
using Minio.DataModel;
using Minio.DataModel.Args;
using Minio.DataModel.Response;
using MongoDB.Driver;
using System.Web;namespace MinIOExt;/// <summary>
/// Minio操作类
/// </summary>
public class MinioService
{private readonly IMinioClient _minioClient;private readonly string _bucketName;public MinioService(IConfiguration configuration){var minioConfig = configuration.GetSection("OSS_Settings");var endpoint = minioConfig["EndPoint"];var accessKey = minioConfig["AccessKey_ID"];var secretKey = minioConfig["Access_Key_Secret"];var enableSSL = minioConfig.GetValue<bool>("EnableSSL");// 确保读取 bucketName 配置项_bucketName = minioConfig["BucketName"] ?? throw new ArgumentNullException("BucketName未设置!");// 使用 MinioClient 的构造函数创建客户端_minioClient = new MinioClient().WithEndpoint(endpoint).WithCredentials(accessKey, secretKey).WithSSL(enableSSL).Build();}public async Task<bool> BucketExistAsync(string bucketName){var beArgs = new BucketExistsArgs().WithBucket(bucketName);bool found = await _minioClient.BucketExistsAsync(beArgs);return found;}public async Task MakeBucketAsync(string bucketName){var mbArgs = new MakeBucketArgs().WithBucket(bucketName);await _minioClient.MakeBucketAsync(mbArgs);var policy = $@"{{""Version"": ""2012-10-17"",""Statement"": [{{""Effect"": ""Allow"",""Principal"": {{""AWS"": [""*""]}},""Action"": [""s3:GetBucketLocation"",""s3:ListBucket""],""Resource"": [""arn:aws:s3:::{bucketName}""]}},{{""Effect"": ""Allow"",""Principal"": {{""AWS"": [""*""]}},""Action"": [""s3:GetObject""],""Resource"": [""arn:aws:s3:::{bucketName}/*""]}}]
}}";var policyArgs = new SetPolicyArgs().WithBucket(bucketName).WithPolicy(policy);// 设置桶的访问策略await _minioClient.SetPolicyAsync(policyArgs);}/// <summary>/// 文件上传/// </summary>/// <param name="objectName">文件流的文件名(格式):folderName/fileName,如果是在根目录下,则直接为fileName即可</param>/// <param name="fileStream">文件流</param>/// <param name="fileSize">文件大小</param>/// <param name="fileName">文件路径,如果直接读取本地文件,则使用这个;此参数不可与文件流的方式共用</param>/// <param name="contentType">文件MIME类型</param>/// <param name="headers">Http标头</param>/// <param name="progress">进度条</param>/// <returns>是否上传成功</returns>public async Task<bool> UploadFileAsync(string objectName, Stream? fileStream = null, long? fileSize = null, string? fileName = null,string? contentType = null, IDictionary<string, string>? headers = null,IProgress<ProgressReport>? progress = null){if (string.IsNullOrWhiteSpace(fileName) && fileStream is null)throw new InvalidOperationException("文件流和文件读取路径不能同时为空!");if (!string.IsNullOrWhiteSpace(fileName) && fileStream is not null)throw new InvalidOperationException("文件流方式上传和文件读取路径方式上传只能同时存在一个!");// Check object size when using stream dataif (fileStream is not null && fileStream.Length == 0 && fileSize is not > 0)throw new InvalidOperationException($"文件大小未设置!");// Make a bucket on the server, if not already present.bool found = await BucketExistAsync(_bucketName);if (!found){await MakeBucketAsync(_bucketName);}// Upload a file to bucket.var putObjectArgs = new PutObjectArgs().WithBucket(_bucketName).WithContentType(contentType).WithHeaders(headers).WithObject(objectName).WithProgress(progress);if (fileStream != null){putObjectArgs.WithStreamData(fileStream).WithObjectSize(fileSize ?? fileStream.Length);}else if (!string.IsNullOrWhiteSpace(fileName)){putObjectArgs.WithFileName(fileName);}PutObjectResponse success = await _minioClient.PutObjectAsync(putObjectArgs);return !string.IsNullOrWhiteSpace(success.Etag);}/// <summary>/// 下载文件/// </summary>/// <param name="objectName"></param>/// <returns></returns>public async Task<bool> DownloadFileAsync(string objectName, string savePath){var args = new GetObjectArgs().WithBucket(_bucketName).WithFile(savePath).WithObject(objectName);var res = await _minioClient.GetObjectAsync(args);return !(res is null or { Size: 0});}/// <summary>/// 下载文件流/// </summary>/// <param name="objectName"></param>/// <returns></returns>public async Task GetFileStreamAsync(string objectName, Action<Stream> HandleStream){var args = new GetObjectArgs().WithBucket(_bucketName).WithObject(objectName).WithCallbackStream(HandleStream);await _minioClient.GetObjectAsync(args);}/// <summary>/// 获取文件流并实时更新进度/// </summary>/// <param name="objectName">对象名称</param>/// <param name="progressCallback">进度更新回调</param>/// <returns>文件字节数组</returns>public async Task<MemoryStream> GetFileStreamWithProgressAsync(string objectName, Action<double> progressCallback){// 获取文件元数据var statObjectArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(objectName);var objectStat = await _minioClient.StatObjectAsync(statObjectArgs);var totalSize = objectStat.Size;const int chunkSize = 81920; // 每次下载 80 KBlong totalRead = 0;int chunkCount = (int)Math.Ceiling((double)totalSize / chunkSize);using MemoryStream outputStream = new MemoryStream();for (int i = 0; i < chunkCount; i++){long offset = i * chunkSize;long length = Math.Min(chunkSize, totalSize - offset);var getObjectArgs = new GetObjectArgs().WithBucket(_bucketName).WithObject(objectName).WithOffsetAndLength(offset, length).WithCallbackStream((stream) =>{stream.CopyTo(outputStream);});await _minioClient.GetObjectAsync(getObjectArgs);totalRead += length;double progress = (double)totalRead / totalSize * 100;progressCallback(progress);}outputStream.Position = 0;return outputStream;}/// <summary>/// 下载文件到本地/// </summary>/// <param name="bucketName"></param>/// <param name="objectName"></param>/// <param name="filePath"></param>/// <param name="onProgress"></param>/// <returns></returns>public async Task DownloadFileWithProgressAsync(string bucketName, string objectName, string filePath, Action<double> onProgress){long objectSize = 0;// 获取对象的元数据,确定大小var statObjectArgs = new StatObjectArgs().WithBucket(bucketName).WithObject(objectName);var objectStat = await _minioClient.StatObjectAsync(statObjectArgs);objectSize = objectStat.Size;// 下载文件var getObjectArgs = new GetObjectArgs().WithBucket(bucketName).WithObject(objectName).WithCallbackStream(async stream =>{using var fileStream = File.Create(filePath);var buffer = new byte[81920]; // 80 KBlong totalRead = 0;int bytesRead;while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0){await fileStream.WriteAsync(buffer, 0, bytesRead);totalRead += bytesRead;// 计算并更新进度double progress = (double)totalRead / objectSize * 100;onProgress(progress);}});await _minioClient.GetObjectAsync(getObjectArgs);}/// <summary>/// 删除单个文件/// </summary>/// <param name="objectName"></param>/// <returns></returns>public async Task RemoveFileAsync(string objectName){var args = new RemoveObjectArgs().WithBucket(_bucketName).WithObject(objectName);// 删除对象await _minioClient.RemoveObjectAsync(args);}/// <summary>/// 批量删除文件/// </summary>/// <param name="objectNames"></param>/// <returns></returns>public async Task RemoveFilesAsync(List<string> objectNames){var args = new RemoveObjectsArgs().WithBucket(_bucketName).WithObjects(objectNames);await _minioClient.RemoveObjectsAsync(args);}/// <summary>/// 获取Bucket下的所有Object对象/// </summary>/// <param name="isRecursive">是否递归</param>/// <returns></returns>public Task<List<MinioItem>> ListObjectsArgs(string prefix, bool isRecursive = false){var args = new ListObjectsArgs().WithBucket(_bucketName).WithPrefix(prefix).WithRecursive(isRecursive);var enums = _minioClient.ListObjectsEnumAsync(args).ToBlockingEnumerable();var res = enums.OrderByDescending(d => d.IsDir).Select(d =>{//解决中文乱码问题var decodedKey = HttpUtility.UrlDecode(d.Key);return new MinioItem(){Key = decodedKey,KeyF = "%2F" + decodedKey,Name = decodedKey.Replace(prefix, "").TrimEnd('/'),IsDir = d.IsDir,Size = d.IsDir ? null : ConvertSize(d.Size),LastModifiedDateTime = d.LastModifiedDateTime,};}).ToList();return Task.FromResult(res);}/// <summary>/// 单位转化/// </summary>/// <param name="size"></param>/// <returns></returns>private string ConvertSize(ulong size){var temp = Math.Round(size / 1024.0, 1);if (temp < 1024){return temp + "KB";}else{temp = Math.Round(temp / 1024, 1);if (temp < 1024){return temp + "MB";}else{temp = Math.Round(temp / 1024, 1);if (temp < 1024){return temp + "GB";}else{temp = Math.Round(temp / 1024, 1);return temp + "TB";}}}}/// <summary>/// 复制替换原文件的元数据/// </summary>/// <returns></returns>public async Task CopyToReplaceFile(string objectName, string contentType = "application/octet-stream"){var sourceArgs = new CopySourceObjectArgs().WithBucket(_bucketName).WithObject(objectName);var args = new CopyObjectArgs().WithBucket(_bucketName).WithCopyObjectSource(sourceArgs).WithObject(objectName).WithContentType(contentType).WithReplaceMetadataDirective(true);await _minioClient.CopyObjectAsync(args);}/// <summary>/// 获取文件的ContentType类型/// </summary>/// <param name="objectName"></param>/// <returns></returns>public async Task<string> GetContentType(string objectName){var stat = new StatObjectArgs().WithBucket(_bucketName).WithObject(objectName);var statObj = await _minioClient.StatObjectAsync(stat);return statObj == null ? throw new Exception($"【{Path.GetFileName(objectName)}】文件不存在!") : statObj.ContentType;}/// <summary>/// 比较ContentType/// </summary>/// <param name="objectName"></param>/// <param name="contentType"></param>/// <returns></returns>public async Task<bool> CompareContentType(string objectName, string contentType = "application/octet-stream"){string ct = await GetContentType(objectName);if (ct.Equals(contentType)){return true;}else{return false;}}/// <summary>/// 获取文件的ContentType类型/// </summary>/// <param name="objectName"></param>/// <returns></returns>public async Task<bool> ObjectExists(string objectName){try{var stat = new StatObjectArgs().WithBucket(_bucketName).WithObject(objectName);var statObj = await _minioClient.StatObjectAsync(stat);return !(statObj == null || statObj.Size <= 0);}catch (Exception){return false;}}
}//页面文件结构实体类
public class MinioItem
{/// <summary>/// ObjectName/// </summary>public string Key { get; set; }/// <summary>/// %2F拼接Key/// </summary>public string KeyF { get; set; }/// <summary>/// 剔除Prefix前缀后的内容/// </summary>public string Name { get; set; }/// <summary>/// 是否文件夹/// </summary>public bool IsDir { get; set; }/// <summary>/// 转换后的文件大小/// </summary>public string? Size { get; set; }/// <summary>/// 最后修改时间/// </summary>public DateTime? LastModifiedDateTime { get; set; }/// <summary>/// 文件或文件夹类型/// </summary>public string Type { get { return IsDir ? "文件夹" : (Path.GetExtension(Name) + "文件"); } }/// <summary>/// 是否隐藏文件夹双击进入转圈圈/// </summary>public bool HideLoading { get; set; } = true;/// <summary>/// 是否隐藏文件下载转圈圈/// </summary>public bool HideDownloadLoading { get; set; } = true;/// <summary>/// 是否勾选/// </summary>public bool IsCheck { get; set; } = false;
}
8、扩展和完善思路
a、文件删除;
b、批量上传文件时,进度显示个数,如:上传成功文件数/总文件数;
c、批量下载文件时,显示进度;
d、.......