Bootstrap Blazor实现模拟MinIO功能的组件

        背景:年前有个需求,要求在前端页面中展示某路径下的文件和文件夹等信息,同时拥有文件上传下载、文件夹创建等功能。最初选定的方案是在文件存储服务器中开放一个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、.......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值