在孢子記賬中我們需要存儲用戶的頭像、賬單的圖片等文件,這些文件的存儲我們可以使用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/data
和minio/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
設置通用屬性(如創建時間、創建者等)。最后將實體添加到數據庫上下文并保存更改。
在代碼中我們調用了ProcessEndpoint
、ValidateConfiguration
、EnsureBucketAsync
三個方法,它們都是私有方法,用來輔助文件上傳功能,我們來看一下這三個方法的代碼:
/// <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
方法,該方法接收文件名、是否公開訪問的標志以及取消令牌作為參數,返回一個包含預簽名上傳URL
的PresignedURLResponse
對象。
在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機制確保了文件訪問的安全性。