背景:年前有个需求,要求在前端页面中展示某路径下的文件和文件夹等信息,同时拥有文件上传下载、文件夹创建等功能。最初选定的方案是在文件存储服务器中开放一个FTP路径(需登录)出来进行文件操作,为此写了一个模拟FTP功能的组件(文章:Bootstrap Blazor中实现模拟FTP功能的组件-优快云博客)。但是最终的效果并不理想,尤其是在大文件下载方面,由于要事先登陆,没法直接把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) {
// 将文件字节转换为 Blob
const 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 data
if (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 KB
long 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 KB
long 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、.......