59.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--MinIO对象存储服务

在孢子记账中我们需要存储用户的头像、账单的图片等文件,这些文件的存储我们可以使用MinIO对象存储服务,
MinIO提供了高性能、可扩展的对象存储解决方案,能够帮助我们轻松管理这些文件资源。通过MinIO,我们可以将用户上传的图片文件安全地存储在云端,并且可以随时通过HTTP访问这些资源。

一、关于MinIO

1. 什么是MinIO

MinIO是一个功能强大的开源对象存储服务器,基于Apache License v2.0开源协议发布。作为一个现代化的存储解决方案,它为用户提供了企业级的高可用性、无限的水平扩展能力以及卓越的性能表现。MinIO采用Go语言开发,具有轻量级、易部署的特点,同时提供与Amazon S3兼容的API接口,使得它能够无缝对接现有的云存储生态系统。

在架构设计上,MinIO采用分布式架构,支持多节点部署和数据复制,确保数据的可靠性和容错能力。它使用纠删码技术来保护数据,即使在多个节点故障的情况下也能保证数据的完整性。MinIO的性能表现尤为出色,单个节点就能达到数GB/s的读写速度,并且随着节点的增加,性能可以线性提升。

MinIO的应用场景十分广泛,特别适合现代云原生应用的开发需求。在大数据分析领域,它可以作为数据湖的存储后端,支持Hadoop、Spark等大数据框架的直接访问。在机器学习和AI训练场景中,MinIO能够高效处理大量的训练数据集。对于需要处理图片、视频等多媒体文件的应用,MinIO提供了快速的上传下载能力和便捷的文件管理功能。此外,它还可以作为备份存储、日志存储等基础设施的重要组成部分。

2. 安装部署MinIO

MinIO的安装部署非常简单,我们可以在官方网站下载对应操作系统的安装包,然后按照安装指南进行安装。MinIO支持在Linux、Windows和macOS等操作系统上运行,同时也提供了Docker镜像,方便在容器化环境中部署。我们以Docker为例,来看一下如何部署。

首先,我们需要拉取MinIO的Docker镜像:

docker pull minio/minio

这个命令会拉取最新的MinIO镜像文件,之后使用docker images命令查看MinIO的镜像是否已经已拉取到本地:

docker images

我们可以看到MinIO的镜像文件已经被成功拉取到本地。

接下来,我们要创建一个数据目录,用来存储MinIO的文件数据:

mkdir -p $HOME/minio/data
mkdir -p $HOME/minio/config

执行这个命令后,将在当前用户主目录下创建路径 minio/dataminio/config,这两个路径分别用来存储MinIO的文件数据和配置文件,后续用来存储MinIO的文件数据,并且这个两个目录将会挂在到MinIO容器中。

接下来,我们要创建MinIO的容器:

docker run -d --name minio \-p 9000:9000 -p 9001:9001 \-e "MINIO_ACCESS_KEY=minioadmin" \-e "MINIO_SECRET_KEY=minioadmin" \-v $HOME/minio/data:/data \-v $HOME/minio/config:/root/.minio \minio/minio server /data --console-address ":9001"

这个命令会创建一个名为minio的容器,将MinIO的文件数据存储在$HOME/minio/data目录下,将MinIO的配置文件存储在$HOME/minio/config目录下,并且创建了一个名为minioadmin的用户,用户的访问密钥为minioadmin。MinIO的API端口为9000,控制台端口为9001。

我们在浏览器中访问http://YOUR_IP:9001,使用minioadmin用户登录MinIO控制台,就看到了MinIO的首页。最后,我们创建一个名称为sporeaccounting的存储桶,作为孢子记账项目的存储根目录。

二、创建资源微服务

在这一小节,我们将创建资源微服务SP.ResourceService,这是一个专门为孢子记账项目提供文件资源管理的微服务。资源微服务将负责处理所有与文件相关的操作,包括用户头像和账单图片等资源的管理。通过与MinIO对象存储的集成,我们可以实现安全可靠的文件存储和访问机制。该服务将提供完整的文件生命周期管理功能,允许用户上传文件到MinIO存储系统,获取文件的访问URL以便前端展示,在不需要时删除文件释放存储空间。为了优化文件上传体验和提高性能,我们还将实现基于预签名URL的前端直传机制,客户端可以获取临时的上传凭证,直接将文件上传到MinIO存储,而无需经过应用服务器中转。在文件上传完成后,客户端会通知资源服务进行确认,以确保文件上传的完整性和有效性。这种设计不仅能提供良好的用户体验,还能有效减轻应用服务器的负载。

2.1 上传文件

我们首先实现上传文件功能,在Service文件夹下新建OSS服务接口IOssService,在这个接口中添加上传文件方法UploadAsync,代码如下:

/// <summary>
/// 上传文件
/// </summary>
/// <param name="file">文件</param>
/// <param name="isPublic">是否公开</param>
/// <param name="ct">取消令牌</param>
Task UploadAsync(IFormFile file, bool isPublic = true, CancellationToken ct = default);

UploadAsync方法是一个异步方法,用于将文件上传到MinIO对象存储服务。该方法接收IFormFile类型的file参数用于处理上传的文件,IFormFile是ASP.NET Core中用于处理HTTP请求中文件的接口,包含了文件的基本信息(如文件名、大小、内容类型等)和文件流;isPublic参数为bool类型且默认值为true,用于控制上传文件的访问权限,true表示文件可以通过URL直接访问,false表示需要授权才能访问;ct参数为CancellationToken类型且默认值为default,这是.NET中用于协调异步操作取消的标准机制,可以在长时间运行的文件上传过程中实现取消功能。

在实现类中,我们将使用MinIO的SDK来完成实际的文件上传工作。在Service/Impl文件夹下新建MinIo的实现类。

/// <summary>
/// MinIO 客户端
/// </summary>
private readonly IMinioClient _client;/// <summary>
/// MinIO 配置选项
/// </summary>
private readonly IOptions<MinioOptions> _options;/// <summary>
/// 日志记录器
/// </summary>
private readonly ILogger<MinioOssService> _logger;/// <summary>
/// 数据库上下文
/// </summary>
private readonly ResourceServiceDbContext _dbContext;/// <summary>
/// 构造函数
/// </summary>
/// <param name="options"></param>
/// <param name="logger"></param>
/// <param name="dbContext"></param>
public MinioOssService(IOptions<MinioOptions> options, ILogger<MinioOssService> logger,ResourceServiceDbContext dbContext)
{_options = options;_logger = logger;_dbContext = dbContext;try{// 验证配置ValidateConfiguration(options.Value);// 处理端点URL格式 - MinIO客户端期望的是主机名和端口,而不是完整的URLvar endpoint = ProcessEndpoint(options.Value.Endpoint);// 初始化 MinIO 客户端var clientBuilder = new MinioClient().WithEndpoint(endpoint).WithCredentials(options.Value.AccessKey, options.Value.SecretKey);if (options.Value.WithSSL){clientBuilder = clientBuilder.WithSSL();}// 构建客户端_client = clientBuilder.Build();}catch (Exception ex){_logger.LogError(ex, "构建客户端失败");throw;}
}/// <summary>
/// 上传文件
/// </summary>
/// <param name="file">文件</param>
/// <param name="isPublic">是否公开</param>
/// <param name="ct">取消令牌</param>
/// <returns></returns>
public async Task UploadAsync(IFormFile file, bool isPublic = true, CancellationToken ct = default)
{using var streamRead = file.OpenReadStream();var objectName = $"{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid():N}{Path.GetExtension(file.FileName)}";var bucket = isPublic ? _options.Value.PublicBucket : _options.Value.PrivateBucket;await EnsureBucketAsync(bucket, ct);var putArgs = new PutObjectArgs();// 计算大小(若不可Seek,先缓冲)long size;if (streamRead.CanSeek){size = streamRead.Length - streamRead.Position;putArgs.WithStreamData(streamRead);}else{var ms = new MemoryStream();await streamRead.CopyToAsync(ms, ct);ms.Position = 0;var stream = streamRead;stream = ms;size = ms.Length;putArgs.WithStreamData(stream);}putArgs.WithBucket(bucket).WithObject(objectName).WithObjectSize(size).WithContentType(file.ContentType);await _client.PutObjectAsync(putArgs, ct);Files fileInfo = new Files{ObjectName = objectName,IsPublic = isPublic,Size = size,ContentType = file.ContentType,OriginalName = file.FileName};SettingCommProperty.Create(fileInfo);_dbContext.Files.Add(fileInfo);await _dbContext.SaveChangesAsync(ct);
}

首先,MinioOssService 类的构造函数接收了三个重要的依赖:MinIO配置选项、日志记录器和数据库上下文。在构造函数中,我们首先验证配置的有效性,然后处理MinIO的端点URL格式,因为MinIO客户端需要的是主机名和端口,而不是完整的URL。接着使用Builder模式构建MinIO客户端实例,如果配置了SSL,则启用安全连接。

UploadAsync方法中,我们实现了文件上传的核心逻辑。该方法首先打开文件流,并根据当前日期和GUID生成一个唯一的对象名称,这样可以避免文件名冲突。根据isPublic参数选择使用公开或私有存储桶,并确保存储桶存在。文件上传的过程中,我们需要特别处理文件流的大小计算。如果流支持Seek操作,我们可以直接获取其长度;如果不支持,则需要先将内容复制到内存流中进行缓冲。这样的处理确保了上传过程的可靠性。上传参数的设置也很关键,我们通过PutObjectArgs设置了存储桶名称、对象名称、文件大小和内容类型等必要信息。使用MinIO客户端的PutObjectAsync方法执行实际的上传操作。

最后,我们将文件信息保存到数据库中。创建Files实体对象,设置对象名称、公开状态、大小、内容类型和原始文件名等信息,并通过SettingCommProperty.Create设置通用属性(如创建时间、创建者等)。最后将实体添加到数据库上下文并保存更改。

在代码中我们调用了ProcessEndpointValidateConfigurationEnsureBucketAsync 三个方法,它们都是私有方法,用来辅助文件上传功能,我们来看一下这三个方法的代码:

/// <summary>
/// 确保桶存在
/// </summary>
/// <param name="bucket">桶名称</param>
/// <param name="ct">取消令牌</param>
/// <returns></returns>
private async Task EnsureBucketAsync(string bucket, CancellationToken ct)
{var exists = await _client.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucket), ct);if (!exists){await _client.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucket), ct);_logger.LogInformation("Created bucket: {Bucket}", bucket);// 如果是公共桶,设置为公共访问策略if (bucket == _options.Value.PublicBucket){await SetPublicBucketPolicyAsync(bucket, ct);_logger.LogInformation("Set public access policy for bucket: {Bucket}", bucket);}}
}/// <summary>
/// 为桶设置公共访问策略
/// </summary>
/// <param name="bucket">桶名称</param>
/// <param name="ct">取消令牌</param>
/// <returns></returns>
private async Task SetPublicBucketPolicyAsync(string bucket, CancellationToken ct)
{try{// 创建公共访问策略JSONvar policy = new{Version = "2012-10-17",Statement = new[]{new{Effect = "Allow",Principal = new { AWS = new[] { "*" } },Action = new[] { "s3:GetObject" },Resource = new[] { $"arn:aws:s3:::{bucket}/*" }}}};var policyJson = JsonSerializer.Serialize(policy, new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase,WriteIndented = false});// 使用SetPolicyAsync方法设置桶策略var setPolicyArgs = new SetPolicyArgs().WithBucket(bucket).WithPolicy(policyJson);await _client.SetPolicyAsync(setPolicyArgs, ct);}catch (Exception ex){// 不抛出异常,因为桶已经创建成功,只是策略设置失败_logger.LogError(ex, "为桶设置公共访问策略失败: {Bucket}。桶仍可用,但对象无法通过URL直接访问。", bucket);}
}/// <summary>
/// 验证MinIO配置
/// </summary>
/// <param name="options">MinIO配置选项</param>
/// <exception cref="ArgumentException">配置无效时抛出异常</exception>
private void ValidateConfiguration(MinioOptions options)
{if (string.IsNullOrWhiteSpace(options.Endpoint)){throw new ArgumentException("MinIO 端点不能为空");}if (string.IsNullOrWhiteSpace(options.AccessKey)){throw new ArgumentException("MinIO AccessKey 不能为空");}if (string.IsNullOrWhiteSpace(options.SecretKey)){throw new ArgumentException("MinIO SecretKey 不能为空");}if (string.IsNullOrWhiteSpace(options.PublicBucket)){throw new ArgumentException("MinIO 公共桶不能为空");}if (string.IsNullOrWhiteSpace(options.PrivateBucket)){throw new ArgumentException("MinIO 私有桶不能为空");}
}/// <summary>
/// 处理端点URL格式
/// </summary>
/// <param name="endpoint">原始端点</param>
/// <returns>处理后的端点</returns>
private string ProcessEndpoint(string endpoint)
{// 如果端点包含协议前缀,需要移除if (endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)){endpoint = endpoint.Substring("http://".Length);}else if (endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase)){endpoint = endpoint.Substring("https://".Length);}// 移除末尾的斜杠endpoint = endpoint.TrimEnd('/');return endpoint;
}

上述代码是上传文件功能的关键私有辅助方法,EnsureBucketAsync方法负责确保存储桶的存在和正确配置。方法首先检查指定的存储桶是否存在,如果不存在则创建新的存储桶。对于公共访问的存储桶(由配置中的PublicBucket指定),方法会额外调用SetPublicBucketPolicyAsync来设置适当的访问策略。这个方法保证了存储桶在使用前已经正确创建和配置。

SetPublicBucketPolicyAsync方法实现了为公共存储桶设置访问策略的功能。方法创建了一个符合AWS S3标准的策略JSON,允许所有用户对存储桶中的对象进行GetObject操作。策略使用JSON序列化,并通过MinIO客户端的SetPolicyAsync方法应用到存储桶。这个方法采用了容错设计,即使策略设置失败,也不会抛出异常而是记录错误日志,这样可以确保上传功能仍然可用,只是可能无法通过URL直接访问文件。

ValidateConfiguration方法用于验证MinIO配置的完整性和有效性。它检查了包括端点(Endpoint)、访问密钥(AccessKey)、密钥(SecretKey)以及公共和私有存储桶名称在内的所有必要配置项。如果任何配置项为空或未设置,方法会抛出ArgumentException异常,这种严格的配置验证确保了系统在启动时就能发现配置问题,避免运行时出现意外错误。

ProcessEndpoint方法处理MinIO服务端点URL的格式化。这个方法的存在是因为MinIO客户端需要特定格式的端点地址,它期望得到的是主机名和端口,而不是完整的URL。该方法会移除URL中的协议前缀("http://“或"https://”)以及末尾的斜杠,确保端点地址符合MinIO客户端的要求。

最后我们新建控制器FilesController,并新增Action Upload,代码如下,代码很简单,这里就不再多讲了。

/// <summary>
/// 上传文件
/// </summary>
/// <param name="file"></param>
/// <param name="isPublic"></param>
/// <returns></returns>
[HttpPost("upload")]
[Consumes("multipart/form-data")]
public async Task<ActionResult> Upload(IFormFile file, [FromQuery] bool isPublic = true)
{await _oss.UploadAsync(file,isPublic);return Ok();
}
2.2 获取文件URL

接下来我们实现获取文件URL的功能。对于公开的文件,我们将返回一个可以直接访问的URL;对于私有文件,我们将生成一个带有时效性的预签名URL。这样可以确保文件访问的安全性,同时为用户提供便捷的访问方式。让我们看看具体的实现代码:

// =================IOssService===================/// <summary>
/// 获取文件URL
/// </summary>
/// <param name="fileId">文件id</param>
Task<string> GetUrlAsync(long fileId);// =================MinioOssService===================/// <summary>
/// 获取文件URL
/// </summary>
/// <param name="fileId">文件id</param>
/// <returns></returns>
public async Task<string> GetUrlAsync(long fileId)
{// 查询文件信息Files? file = _dbContext.Files.FirstOrDefault(f=>f.Id==fileId && f.IsDeleted== false);if (file == null){throw new NotFoundException("文件不存在");}string bucket = "";string objectName = file.ObjectName;if (file.IsPublic){bucket = _options.Value.PublicBucket;// 公开桶:返回直链var baseUrl = _options.Value.PublicBaseUrl?.TrimEnd('/');if (!string.IsNullOrWhiteSpace(baseUrl)){return $"{baseUrl}/{bucket}/{Uri.EscapeDataString(objectName)}";}// 若未配置 PublicBaseUrl,则退回到 MinIO 原始地址var scheme = _options.Value.WithSSL ? "https" : "http";return $"{scheme}://{_options.Value.Endpoint.TrimEnd('/')}/{bucket}/{Uri.EscapeDataString(objectName)}";}else{// 私有桶:返回预签名bucket = _options.Value.PrivateBucket;var expirySeconds = _options.Value.PresignedExpireSeconds;var preArgs = new PresignedGetObjectArgs().WithBucket(bucket).WithObject(objectName).WithExpiry(expirySeconds);return await _client.PresignedGetObjectAsync(preArgs);}
}

GetUrlAsync方法是一个关键的文件访问URL生成功能。该方法通过文件ID从数据库查询文件信息,实现了对公开和私有文件的不同访问策略处理。方法首先会验证文件的存在性和有效性,如果找不到文件或文件已被标记为删除,会抛出NotFoundException异常来及时通知调用方。

在处理公开文件时,方法采用了灵活的URL生成策略。它优先使用配置中的PublicBaseUrl,这通常用于已配置CDN或自定义域名的场景。通过将PublicBaseUrl、存储桶名称和经过URL编码的对象名称组合,生成一个可直接访问的完整URL。如果没有配置PublicBaseUrl,方法会降级使用MinIO服务器的原始地址,根据WithSSL配置选择合适的协议(http或https),确保在任何情况下都能生成有效的访问地址。

对于私有文件的处理则采用了更安全的方式。方法使用MinIO客户端的PresignedGetObjectAsync功能生成带有访问凭证的预签名URL。这个URL具有时效性,其有效期由配置中的PresignedExpireSeconds控制。预签名URL包含了必要的认证信息,确保只有获得授权的用户在规定时间内能够访问文件,既保证了便捷访问,又不影响安全性。这种机制适合需要临时授权访问的场景,比如文件预览或限时下载。

Tip:Controller 中的调用方式很简单,就不再展示了,大家在项目的GitHub中查看完整代码。

2.3 删除文件

在资源管理中,删除功能是不可或缺的一部分。当用户不再需要某个文件时,我们需要同时清理MinIO存储中的实际文件和数据库中的文件记录。这个功能需要谨慎实现,因为文件删除是不可逆的操作。我们的实现会先验证文件的存在性,然后依次执行存储清理和数据库更新操作,确保整个删除过程的原子性和可靠性。让我们来看看具体的实现代码:

// =================IOssService===================/// <summary>
/// 删除文件
/// </summary>
/// <param name="fileId">文件id</param>
/// <param name="ct">取消令牌</param>
Task DeleteAsync(long fileId, CancellationToken ct = default);// =================MinioOssService==================/// <summary>
/// 删除文件
/// </summary>
/// <param name="fileId">文件id</param>
/// <param name="ct">取消令牌</param>
/// <returns></returns>
public async Task DeleteAsync(long fileId, CancellationToken ct = default)
{// 查询文件Files? fileInfo = _dbContext.Files.FirstOrDefault(f => f.IsDeleted == false && f.Id == fileId);if (fileInfo == null){throw new NotFoundException("文件不存在");}var bucket = fileInfo.IsPublic ? _options.Value.PublicBucket : _options.Value.PrivateBucket;await EnsureBucketAsync(bucket, ct);var rmArgs = new RemoveObjectArgs().WithBucket(bucket).WithObject(fileInfo.ObjectName);await _client.RemoveObjectAsync(rmArgs, ct);// 删除数据库记录SettingCommProperty.Delete(fileInfo);await _dbContext.SaveChangesAsync(fileInfo);
}

删除文件功能是资源管理中的重要组成部分。在IOssService接口中,我们定义了DeleteAsync方法,该方法接收文件ID和取消令牌作为参数。这个异步方法负责安全地删除存储在MinIO中的文件以及相关的数据库记录。

MinioOssService的具体实现中,DeleteAsync方法首先通过LINQ查询从数据库中获取文件信息。查询条件确保只查找未删除的文件(IsDeleted为false)且ID匹配的记录。如果找不到符合条件的文件记录,方法会抛出NotFoundException异常,及时通知调用方文件不存在。

获取到文件信息后,方法会根据文件的IsPublic属性决定从哪个存储桶中删除文件。通过_options.Value访问配置信息,如果是公开文件则使用PublicBucket,否则使用PrivateBucket。在执行删除操作前,方法会调用EnsureBucketAsync确保目标存储桶存在,这是一个额外的安全检查。接下来,方法使用MinIO客户端的RemoveObjectAsync方法执行实际的文件删除操作。RemoveObjectArgs通过链式调用配置了必要的参数,包括存储桶名称和文件的对象名称。这个操作会从MinIO存储中物理删除文件。最后我们调用_dbContext.SaveChangesAsync(ct);方法将文件标记为已删除。

2.4 获取前端直传凭证

在实际的文件上传场景中,我们经常需要考虑前端直接上传文件到对象存储服务的需求。这种直传方案可以有效减轻应用服务器的负载,提升上传性能和用户体验。为了实现这个功能,我们需要提供一个预签名的上传URL给前端使用。这个预签名URL本质上是一个临时的、带有授权信息的上传端点,它允许客户端在限定时间内直接向对象存储服务发起上传请求,而无需通过应用服务器中转文件数据。

预签名URL的工作机制是通过在URL中嵌入临时的访问凭证和必要的参数信息,使得客户端能够在有限时间内执行特定的操作(在这里是上传文件)。当前端获取到这个预签名URL后,就可以直接使用标准的HTTP PUT请求将文件上传到这个地址。这种方式不仅能显著提升上传效率,还能减少服务器的带宽消耗和处理负担。同时,由于预签名URL具有时效性和操作限制,也保证了上传操作的安全性。在大文件上传或高并发场景下,这种直传方案的优势尤为明显。我们来看一下代码实现:

// =================IOssService===================/// <summary>
/// 生成用于前端直传的预签名 PUT URL
/// </summary>
/// <param name="fileName">对象名称</param>
/// <param name="isPublic">是否公开桶</param>
/// <param name="ct">取消令牌</param>
Task<PresignedURLResponse> GetPresignedPutUrlAsync(string fileName, bool isPublic, CancellationToken ct = default);// =================MinioOssService==================/// <summary>
/// 生成前端直传的预签名 PUT URL
/// </summary>
/// <param name="fileName"></param>
/// <param name="isPublic"></param>
/// <param name="ct"></param>
public async Task<PresignedURLResponse> GetPresignedPutUrlAsync(string fileName, bool isPublic,CancellationToken ct = default)
{// 拼接日期路径和唯一标识string objectName = $"{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid():N}{Path.GetExtension(fileName)}";var bucket = isPublic ? _options.Value.PublicBucket : _options.Value.PrivateBucket;await EnsureBucketAsync(bucket, ct);int expirySeconds = _options.Value.UploadTokenExpireSeconds;var preArgs = new PresignedPutObjectArgs().WithBucket(bucket).WithObject(objectName).WithExpiry(expirySeconds);string uploadUrl = await _client.PresignedPutObjectAsync(preArgs);PresignedURLResponse presignedUrlResponse = new PresignedURLResponse(){UploadUrl = uploadUrl,ObjectName = objectName,OriginalFileName = fileName};return presignedUrlResponse;
}

IOssService接口中,我们定义了GetPresignedPutUrlAsync方法,该方法接收文件名、是否公开访问的标志以及取消令牌作为参数,返回一个包含预签名上传URLPresignedURLResponse对象。

MinioOssService的具体实现中,GetPresignedPutUrlAsync方法首先通过组合当前UTC时间的年月日路径格式和一个GUID,生成一个唯一的对象名称,并保留原始文件的扩展名。这种命名方式既保证了文件名的唯一性,又实现了按日期的文件组织结构。根据isPublic参数,方法会选择使用公开桶还是私有桶,并通过EnsureBucketAsync方法确保目标存储桶存在。

接下来,方法从配置中获取上传令牌的过期时间UploadTokenExpireSeconds,并使用MinIO客户端的PresignedPutObjectArgs创建预签名请求参数。这些参数包括存储桶名称、对象名称和过期时间。通过调用MinIO客户端的PresignedPutObjectAsync方法,生成一个带有认证信息的临时上传URL。

最后,方法将生成的上传URL、对象名称和原始文件名封装到PresignedURLResponse对象中返回。这个响应对象包含了客户端执行直传所需的所有信息。前端可以使用返回的预签名URL直接向MinIO发起PUT请求上传文件,既提高了上传效率,又减轻了应用服务器的负担。

2.5 确认文件上传成功

在前端完成文件上传到MinIO存储后,系统需要一个确认机制来验证上传是否成功并将文件信息持久化到数据库中。这个确认过程对于维护文件系统的完整性和一致性至关重要。前端会将文件的关键信息,包括对象名称、文件大小、内容类型、原始文件名等发送到服务端。服务端首先会通过MinIO的API验证文件是否确实存在于存储桶中,确保文件上传的完整性。验证通过后,服务端会在数据库中创建相应的文件记录,建立起文件元数据与实际存储对象之间的映射关系。这种双重确认机制不仅能够及时发现上传过程中的问题,还能确保系统中的文件记录始终与实际存储的文件保持同步,为后续的文件管理和访问提供可靠的基础。实现代码如下:

// =================IOssService===================/// <summary>
/// 文件上传确认
/// </summary>
/// <param name="request">上传确认请求</param>
/// <returns></returns>
Task ConfirmUploadAsync(ConfirmUploadRequest request, CancellationToken ct = default);// =================MinioOssService==================/// <summary>
/// 文件上传确认
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public async Task ConfirmUploadAsync(ConfirmUploadRequest request, CancellationToken ct = default)
{// 验证文件是否真的存在于 MinIO 中var bucket = request.IsPublic ? _options.Value.PublicBucket : _options.Value.PrivateBucket;try{var statArgs = new StatObjectArgs().WithBucket(bucket).WithObject(request.ObjectName);var objectStat = await _client.StatObjectAsync(statArgs, ct);// 创建文件记录var fileInfo = new Files{ObjectName = request.ObjectName,IsPublic = request.IsPublic,Size = request.FileSize,ContentType = request.ContentType,OriginalName = request.OriginalFileName};SettingCommProperty.Create(fileInfo);_dbContext.Files.Add(fileInfo);await _dbContext.SaveChangesAsync(ct);}catch (Exception ex){_logger.LogError(ex, "无法确认文件上传:{ObjectName}", request.ObjectName);throw new BadRequestException($"无法确认文件上传: {request.ObjectName}");}
}

文件上传确认功能是确保文件成功上传到MinIO存储系统的重要环节。在IOssService接口中,我们定义了ConfirmUploadAsync方法,该方法接收一个ConfirmUploadRequest类型的请求参数和一个可选的取消令牌参数。这个方法的主要职责是验证文件是否确实存在于MinIO存储中,并在确认成功后在数据库中创建相应的文件记录。

MinioOssService中的具体实现中,首先根据请求中的IsPublic属性决定使用公共存储桶还是私有存储桶。这种设计允许系统灵活处理不同访问权限的文件存储需求。接下来,通过创建StatObjectArgs对象并设置相应的存储桶和对象名称,使用MinIO客户端的StatObjectAsync方法来验证文件的存在性。这个操作会检查文件的元数据,如果文件不存在或无法访问,将会抛出异常。

如果文件验证成功,方法会创建一个新的Files实体对象,用于在数据库中记录文件信息。这个实体包含了文件的关键属性:对象名称、公共访问标志、文件大小、内容类型以及原始文件名。通过SettingCommProperty.Create方法设置通用属性后,将文件记录添加到数据库上下文中,并通过SaveChangesAsync保存更改。

为了确保系统的健壮性,整个操作被包装在try-catch块中。如果在验证或保存过程中发生任何异常,会记录详细的错误日志,并抛出一个BadRequestException异常,提供清晰的错误信息给调用方。

三、总结

本文详细介绍了在孢子记账项目中如何集成和使用MinIO对象存储服务来管理用户头像和账单图片等文件资源。文章首先深入讲解了MinIO的核心特性、优势以及在现代云原生应用中的广泛应用场景,并通过Docker方式演示了MinIO的安装部署过程。随后,文章重点展示了如何构建资源微服务,实现了包括文件上传和URL获取等在内的核心功能。在实现过程中,不仅考虑了公共访问和私有访问两种场景,还通过预签名URL机制确保了文件访问的安全性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/97789.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/97789.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ESP32三种主流的开发环境

ESP32三种主流的开发环境 1. ESP-IDF (Espressif IoT Development Framework) 这是乐鑫官方提供的专业开发框架&#xff0c;基于FreeRTOS实时操作系统。 特点&#xff1a; 功能最全面&#xff0c;性能最优支持所有ESP32硬件特性使用C/C编程专业级调试工具完整的组件库和API 适合…

HarmonyOS图形处理:Canvas绘制与动画开发实战

本文将全面介绍HarmonyOS 5中Canvas组件的使用方法和动画开发技巧&#xff0c;通过详细的代码示例和最佳实践&#xff0c;帮助您掌握图形绘制和动态效果实现的核心技能。 1. Canvas组件基础与核心API Canvas是HarmonyOS中用于2D图形绘制的重要组件&#xff0c;提供了丰富的绘图…

CCAFusion:用于红外与可见光图像融合的跨模态坐标注意力网络

CCAFusion&#xff1a;用于红外与可见光图像融合的跨模态坐标注意力网络 CCAFusion: Cross-Modal Coordinate Attention Network for Infrared and Visible Image Fusion 摘要 红外与可见光图像融合旨在生成一幅包含全面信息的图像&#xff0c;该图像既能保留丰富的纹理特征&a…

ESP32-P4小智编译历险记:从“编译失败“到“成功烧录“的奇幻之旅,xiaozhi智能聊天机器人编译避坑心得

🚀 ESP32-P4:AI小智编译历险记:从"编译失败"到"成功烧录"的奇幻之旅 要编译其他芯片esp32s3-s2-c3,遇到问题也可以在这里交流 “每一个编译错误都是成长的机会,每一次成功都是坚持的胜利!” —— 某位被编译器折磨的程序员 源码地址:https://githu…

DIFY 项目中通过 Makefile 调用 Dockerfile 并使用 sudo make build-web 命令构建 web 镜像的方法和注意事项

DIFY 项目中通过 Makefile 调用 Dockerfile 并使用 sudo make build-web 命令构建 web 镜像的场景,以下是具体方法和注意事项总结: 一、通过 sudo make build-web 构建 web 镜像的核心方法 1. 理解 Makefile 与 Dockerfile 的关联 Makefile 的作用:DIFY 的 Makefile 中定义…

重学前端015 --- 响应式网页设计 CSS变换

文章目录skew()transformcursortransition.arm .left {} 和 .arm.left {} 区别skew() skew 倾斜变换函数&#xff0c;该函数有两个参数。第一个是剪切x轴的角度&#xff0c;第二个是剪切y轴的角度。 transform: skew(0deg, 44deg);transform .arm.left {top: 35%;left: 5%;t…

【GMX v1实战】时序风险结算与资本成本:深度解析 GMX 永续合约的资金费率机制

在去中心化衍生品交易平台GMX中&#xff0c;当你准备开立杠杆合约仓位&#xff08;无论是做多还是做空某个资产&#xff09;时&#xff0c;系统会默默执行一个关键前置动作——调用名为 ​​updateCumulativeFundingRate​​ 的函数。这个看似普通的“准备工作”&#xff0c;实…

中宇联云计算SD-WAN的售后服务怎么样

前言在数字化转型浪潮中&#xff0c;企业选择SD-WAN服务商不仅关注技术性能&#xff0c;更看重售后服务的质量与可靠性。中宇联云计算作为行业领先者&#xff0c;其SD-WAN售后服务体系已成为行业标杆。随着全球数字化进程加速&#xff0c;企业对广域网&#xff08;WAN&#xff…

【Kubernetes】K8s 集群外服务配置 Service 访问

在 Kubernetes 集群中&#xff0c;内部服务可通过 Service-name 进行访问。那么&#xff0c;对于集群外的服务&#xff0c;Pod 应该如何通过 Service 进行访问呢&#xff1f;一起来看看吧&#xff01;此处举例以 Pod 访问集群外部的 Mysql 数据库1、创建 Service# 创建 Service…

Linux 开发工具(1)

从开始讲Linux&#xff0c;我们的目标绝不止于写几个命令这么简单。我们的目的是在Linux系统上做开发。因此学习Linux的开发工具也是必不可少的。本章将重点讲解&#xff1a;包管理器apt(CentOS叫yum&#xff0c;这里用ubuntu举例)&#xff0c;vim编辑器。一.包管理器apt1.安装…

redis面试点记录

1、主从复制psync-> runid->runid是&#xff1f;则是全量->返回fullresync和runid和复制进度->bgsave命令准备RDB文件->之后的命令写入replication_buffer->发送RDB->发送replication_buffer的信息repl_backlog_buffer环型缓冲区&#xff0c;主节点只有一…

Elastic APM 与 Elasticsearch 集成:构建完整可观测性栈

引言 Elastic APM 深度依赖 Elasticsearch 作为数据后端&#xff0c;但正确集成可以解锁更强大的功能&#xff0c;如自定义查询、聚合分析和与其它 Elastic 工具的协同。本文探讨 APM 与 Elasticsearch 的集成细节&#xff0c;包括数据流、索引管理以及高级用例&#xff0c;帮助…

开源模型应用落地-基于DPO的Qwen3-4B意图理解精准对齐实践(二十)

一、前言 在大模型技术蓬勃发展的今天,如何让AI真正“理解”用户意图,而非仅仅生成流畅文本,已成为落地应用的核心瓶颈。尤其是在客服、搜索、智能助手等场景中,模型对用户query的深层语义解析能力,直接决定了交互体验的成败。然而,经过标准SFT(监督微调)训练的模型,往…

23种设计模式案例

一、创建型模式 1. 单例模式 (Singleton Pattern) 应用场景: 全局状态管理、全局配置、共享资源访问 // 全局状态管理器 class Store {constructor() {if (Store.instance) return Store.instance;this.state {};Store.instance this;}getState(key) { return this.state[key…

ctfshow_web13-----------文件上传.user.ini

打开题目发现是一个文件上传题扫描后发现存在upload.php.bak.bak是备份文件拿到源码正则过滤了php&#xff0c;文件大小<24,文件名小于9经尝试&#xff0c;改后缀php5,ptml均不行&#xff0c;使用.htaccess文件也不成功上传上传.user.ini&#xff0c;在文件中写上auto_prepe…

图像拼接案例,抠图案例

目录 一.图像拼接案例 1.图像拼接项目介绍 2.核心步骤 ①计算图片特征点及描述符 ②匹配特征点&#xff0c;使用暴力匹配器 ③筛选有效匹配 ④计算透视变换矩阵 ⑤应用变换和拼接 二.抠图案例 1.缩放旋转处理 2.转化为灰度图并二值化 3.找出所有轮廓&#xff0c;并在…

【左程云算法笔记016】双端队列-双链表和固定数组实现

目录 1&#xff09;双端队列的介绍 2&#xff09;双端队列用双链表的实现代码演示 3&#xff09;双端队列用固定数组的实现 代码演示 视频 【算法讲解016【入门】双端队列-双链表和固定数组实现】 Leecode leecode641 设计循环双端队列 1&#xff09;双端队列的介绍 可以…

ffplay视频输出和尺寸变换

视频输出模块 视频输出初始化的主要流程 我们开始分析视频&#xff08;图像&#xff09;的显示。 因为使⽤了SDL&#xff0c;⽽video的显示也依赖SDL的窗⼝显示系统&#xff0c;所以先从main函数的SDL初始化看起&#xff08;节选&#xff09;&#xff1a; int main(int argc, c…

协议_https协议

http http协议是将数据以明文的形式在网络上传输。若是传输的数据中包含一些敏感信息比如银行卡信息等可能会被有心人攻击造成信息泄露或被篡改。 总结&#xff1a;http协议进行数据传输难以保证数据的隐私性以及数据完整性&#xff0c;为了保证数据的准确定引入了https这一协…

阿里云 腾讯云 API 自动化查询指南

文章目录一、核心思路与架构建议二、经验与核心建议三、技术方案选型建议四、API使用详解4.1 阿里云4.2 腾讯云五、进阶&#xff1a;与内部系统联动免费个人运维知识库&#xff0c;欢迎您的订阅&#xff1a;literator_ray.flowus.cn 一、核心思路与架构建议 自动化流程可以概括…