分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
進入官網後,可以獲得 Asp.net Core 對 MinIO 操作的 API 範例說明,官方文件
我們實際將代碼貼上後,可以發現會異常,參數不完整, MinIO 官方網站的文件更新速度較慢的關係。
建議直接從 MinIO 官方 Github Source Code 檢閱 https://github.com/minio
到頁面最下方輸入自己的開發語言 (本篇介紹 Asp.net Core)
找到相對應的代碼,複製到專案中,可以發現可以正常使用。
官方 Github 開發人員代碼比釋出的文件更即時
打開範例專案後,架構基本分成以下:
1. MinIO | : | 實現工廠模式, Signleton 提供 Web 與 MinIO Server 的實際互動 |
2. 假資料庫 | : | 為了方便說明,利用 Singleton 保存記憶體的資料,偽裝成教師個人資料的資料庫 |
3. Service | : | 這邊實現建立帳號、上傳檔案、下載檔案、刪除檔案的方式,並且製造假的資料庫互動 |
4. Web控制器 | : | 提供 Html 畫面取資料、與 API 接口進行 CRUD |
5. Html 畫面 | : | 教師上傳系統頁面,可以模擬管理者統一管理教師檔案 |
6. 初始化配置 | : | MinIO Server 連線設定值、依賴注入 |
整個 MinIo 資料夾分成 4 個部分
總共分成以下 4 個區塊:
1. Factory | : | 每個 Web 端操作當與 MinIO 操作時,已建立的 Client 物件會記錄,並且在每次呼叫時重複使用,並且網站結束時安全釋放資源 |
2. Model | : | 定義與 MinIO Server 連線基本屬性 |
3. Util | : | 提供取得共用的建立連線字串物件 |
4. MinIOClientInstance | : | Web 端 Service 實際互動的對象,提供目前教師系統定義的 MinIO 可操作功能 |
工廠方法,提供 Web 端呼叫後,建立此用戶的連線資訊,並且避免同個 Server 下異步操作同時進行鎖物件
在 Application 結束後,會安全釋放資源 Dispose()
public class MinIOClientFactory : IMinIOClientFactory, IDisposable
{
private static readonly ConcurrentDictionary<string, IMinioClient> _minIOClientDict = new();
private static readonly object _lockObject = new();
private static readonly ConcurrentDictionary<string, object> _lockObjectDict = new();
private readonly MinIOConnectionModel _ConnectionItem;
public MinIOClientFactory(IConfiguration configuration)
{
_ConnectionItem = MinIOUtil.GetConfigureSetting(configuration);
}
public IMinioClient CreateClient(MinIOConnectionModel param)
{
var key = $"{param.Host}_{param.Port}_{param.SecretKey}_{param.AccessKey}";
var minIOClientItem = GetMinIOClient(key);
return minIOClientItem;
// 取得 MinIO 客戶端
IMinioClient GetMinIOClient(string key)
{
if (!_minIOClientDict.TryGetValue(key, out var minIOClientItem))
{
var lockObject = GetLockObj(key);
lock (lockObject)
{
if (!_minIOClientDict.TryGetValue(key, out minIOClientItem))
{
minIOClientItem = new MinioClient()
.WithEndpoint(param.Host, param.Port)
.WithCredentials(param.AccessKey, param.SecretKey)
.Build();
_minIOClientDict[key] = minIOClientItem;
}
}
}
return minIOClientItem;
}
}
/// <summary>
/// 取得鎖物件
/// </summary>
private object GetLockObj(string key)
{
if (!_lockObjectDict.TryGetValue(key, out var obj) || obj == null)
{
lock (_lockObject)
{
if (!_lockObjectDict.TryGetValue(key, out obj) || obj == null)
{
obj = new object();
_lockObjectDict[key] = obj;
}
}
}
return obj;
}
public void Dispose()
{
foreach (var client in _minIOClientDict.Values)
{
client.Dispose();
}
_minIOClientDict.Clear();
}
}
MinIO 連線模型,缺一不可
/// <summary>
/// MinIO 連線模型
/// </summary>
public class MinIOConnectionModel
{
/// <summary>
/// 主機位置
/// </summary>
public string Host { get; set; } = string.Empty;
/// <summary>
/// Port 號
/// </summary>
public int Port { get; set; }
/// <summary>
/// 存取金鑰
/// </summary>
public string AccessKey { get; set; } = string.Empty;
/// <summary>
/// 密鑰
/// </summary>
public string SecretKey { get; set; } = string.Empty;
}
靜態共用方法,只要啟動 Asp.net Web 應用後,呼叫此方法可以依照 Appsettings.json 取得 MinIO 配置
public static MinIOConnectionModel GetConfigureSetting(IConfiguration configuration)
{
var result = new MinIOConnectionModel();
var minIOParam = configuration.GetSection("MinIO").Get<MinIOConnectionModel>();
result.Host = minIOParam?.Host ?? string.Empty;
result.Port = minIOParam?.Port ?? default(int);
result.AccessKey = minIOParam?.AccessKey ?? string.Empty;
result.SecretKey = minIOParam?.SecretKey ?? string.Empty;
return result;
}
Web 與 MinIO 伺服器操作的核心代碼,這邊實現 6 種方法(取檔、上傳、下載、刪除、檢查、取得 Bucket)
public class MinIOClientInstance : MinIOConnectionModel, IDisposable
{
private readonly IMinIOClientFactory _minioClientFactory;
private readonly IMinioClient _minIOClientSelf = null;
public MinIOClientInstance(MinIOConnectionModel param,
IConfiguration configuration,
IMinIOClientFactory minioClientFactory)
{
_minioClientFactory = minioClientFactory;
_minIOClientSelf = _minioClientFactory.CreateClient(MinIOUtil.GetConfigureSetting(configuration));
}
/// <summary>
/// 1. 建立 Bucket
/// </summary>
public async Task CreateBucket(string bucketName)
{
try
{
// 取得是否存在
var isExist = await IsExistBucket(bucketName);
// 不存在 - 才創建
if (!isExist)
{
await _minIOClientSelf.MakeBucketAsync(
new MakeBucketArgs()
.WithBucket(bucketName)
).ConfigureAwait(false);
}
}
catch (Exception e)
{
Console.WriteLine($"[CreateBucket] Exception: {e}");
}
}
/// <summary>
/// 2. 取得 Bucket 內的資料
/// </summary>
public async Task<FileModel> GetBucketList(string bucketName)
{
var result = new FileModel();
try
{
// 取得是否存在
var isExist = await IsExistBucket(bucketName);
// 不存在捨棄
if (!isExist)
return result;
// 使用 ListObjectsAsync 方法來列出所有的物件
var listArgs = new ListObjectsArgs()
.WithBucket(bucketName)
.WithRecursive(true); // 設為 true 可以列出所有物件,包括子目錄
var objects = _minIOClientSelf.ListObjectsEnumAsync(listArgs);
// 遍歷 bucket 中的所有物件
result.BucketName = bucketName;
await foreach (Minio.DataModel.Item obj in objects)
{
result.Files.Add(new FileItem()
{
FileName = obj.Key,
FileExtension = Path.GetExtension(obj.Key),
FileSize = obj.Size,
LastUpdateTime = obj.LastModifiedDateTime
});
}
}
catch (MinioException e)
{
Console.WriteLine($"[GetBucketList] Exception: {e}");
}
return result;
}
/// <summary>
/// 3. 下載檔案 - 記憶體資料
/// </summary>
public async Task<MemoryStream> GetObjectAsync(string fileName, string bucketName)
{
var memoryStream = new MemoryStream();
try
{
var getObjectArgs = new GetObjectArgs()
.WithBucket(bucketName)
.WithObject(fileName)
.WithCallbackStream((stream) =>
{
stream.CopyTo(memoryStream);
memoryStream.Position = 0;
});
await _minIOClientSelf.GetObjectAsync(getObjectArgs).ConfigureAwait(false);
return memoryStream; // 成功返回文件流
}
catch (Exception e)
{
Console.WriteLine($"[GetObjectAsync] Exception: {e}");
}
return memoryStream;
}
/// <summary>
/// 4. 上傳檔案
/// </summary>
public async Task UploadFile(IFormFile file, string bucketName)
{
try
{
// 取得是否存在
var isExist = await IsExistBucket(bucketName);
// 不存在捨棄
if (!isExist)
return;
// 取得上傳檔案的流
using (var fileStream = file.OpenReadStream())
{
var objectName = file.FileName; // 使用檔案名稱作為物件名稱
// 設置 PutObjectArgs
var putObjectArgs = new PutObjectArgs()
.WithBucket(bucketName)
.WithObject(objectName)
.WithStreamData(fileStream)
.WithObjectSize(fileStream.Length)
.WithContentType(file.ContentType); // 使用上傳檔案的 Content-Type
await _minIOClientSelf.PutObjectAsync(putObjectArgs);
}
}
catch (Exception e)
{
Console.WriteLine($"[UploadFile] Exception: {e}");
}
}
/// <summary>
/// 5. 完整刪除整個 Bucket
/// </summary>
public async Task DeleteBucket(string bucketName)
{
try
{
// 取得是否存在
var isExist = await IsExistBucket(bucketName);
// 不存在 - 退出
if (!isExist)
return;
// 列出並刪除所有物件
var listArgs = new ListObjectsArgs()
.WithBucket(bucketName)
.WithRecursive(true);
var objects = _minIOClientSelf.ListObjectsEnumAsync(listArgs);
await foreach (Minio.DataModel.Item obj in objects)
{
await _minIOClientSelf.RemoveObjectAsync(new RemoveObjectArgs()
.WithBucket(bucketName)
.WithObject(obj.Key));
}
// 物件內的資料都清除後才能刪除 Bucket
await _minIOClientSelf.RemoveBucketAsync(
new RemoveBucketArgs().WithBucket(bucketName)
).ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine($"[DeleteBucket] Exception: {e}");
}
}
/// <summary>
/// 6. 刪除單一檔案
/// </summary>
public async Task DeleteFile(string fileName, string bucketName)
{
try
{
// 取得是否存在
var isExist = await IsExistBucket(bucketName);
// 不存在 - 退出
if (!isExist)
return;
// 刪除指定物件
await _minIOClientSelf.RemoveObjectAsync(new RemoveObjectArgs()
.WithBucket(bucketName)
.WithObject(fileName));
}
catch (Exception e)
{
Console.WriteLine($"[DeleteFile] Exception: {e}");
}
}
/// <summary>
/// 7.是否存在指定 Bucket
/// </summary>
public async Task<bool> IsExistBucket(string bucketName)
{
try
{
// 取得是否存在
var getArgs = new BucketExistsArgs().WithBucket(bucketName);
var isExist = await _minIOClientSelf.BucketExistsAsync(getArgs).ConfigureAwait(false);
return isExist;
}
catch (Exception e)
{
Console.WriteLine($"[IsExistBucket] Exception: {e}");
return false;
}
}
#region 解構式 - 釋放資源
~MinIOClientInstance()
{
Dispose();
}
public void Dispose()
{
Dispose();
GC.SuppressFinalize(this);
}
#endregion
}
假資料,預設系統有 3 個教師,每個 Bucket 都是以教師 Id 自動建立。
新增帳號時會在本次記憶體操作中,增加一筆假資料(模擬資料庫操作)
public class FakeDataBase
{
private List<TeacherModel> _teachers = new List<TeacherModel>();
/// <summary>
/// 預設假資料
/// </summary>
public FakeDataBase()
{
_teachers.Add(new TeacherModel() { Id = 20240928001, Name = "張明宇" });
_teachers.Add(new TeacherModel() { Id = 20240928002, Name = "李曉峰" });
_teachers.Add(new TeacherModel() { Id = 20240928003, Name = "陳佳玲" });
}
/// <summary>
/// 取得教師資料
/// </summary>
public List<TeacherModel> GetTeachers()
{
return _teachers;
}
/// <summary>
/// 新增假資料庫的帳號 (名字隨機)
/// </summary>
public long CreateTeacher()
{
var fakeAutoIncretmentId = this._teachers.Max(item => item.Id) + 1;
var faker = new Faker("zh_CN");
var now = DateTime.Now;
_teachers.Add(new TeacherModel()
{
Id = fakeAutoIncretmentId,
Name = faker.Name.FullName()
});
return fakeAutoIncretmentId;
}
/// <summary>
/// 移除假資料庫的帳號
/// </summary>
public void DeleteTeacher(long id)
{
var getItem = _teachers.Where(item => item.Id == id).FirstOrDefault();
if (getItem != null)
{
_teachers.Remove(getItem);
}
}
}
教師上傳系統的實際業務邏輯,目前有以下 6 種,實現完整的 CRUD
1. 取得所有教師資料 |
2. 下載檔案 |
3. 上傳檔案 |
4. 刪除檔案 |
5. 建立帳號 |
6. 刪除帳號 |
public class TeacherManageService : ITeacherManageService
{
private readonly FakeDataBase _dataBase;
private readonly MinIOClientInstance _minIOClientInstance;
public TeacherManageService(FakeDataBase dataBase,
MinIOClientInstance minIOClientInstance)
{
_dataBase = dataBase;
_minIOClientInstance = minIOClientInstance;
}
/// <summary>
/// 取得所有教師資料
/// </summary>
public async Task<List<TeacherModel>> GetTeachers()
{
var result = new List<TeacherModel>();
result = await Task.Run(() =>
{
return _dataBase.GetTeachers();
});
foreach (var item in result)
{
var bucketName = $@"{item.Id}";
// 不存在就建立 Bucket
if (!await _minIOClientInstance.IsExistBucket(bucketName))
{
await _minIOClientInstance.CreateBucket(bucketName);
}
else// 存在就將檔案取回
{
var getFiles = await _minIOClientInstance.GetBucketList(bucketName);
item.MySelfFiles = getFiles;
}
}
return result;
}
/// <summary>
/// 下載檔案
/// </summary>
public async Task<MemoryStream> DownloadFile(string fileName, string bucketName)
{
return await _minIOClientInstance.GetObjectAsync(fileName, bucketName);
}
/// <summary>
/// 上傳檔案
/// </summary>
public async Task UploadFile(IFormFile file, string bucketName)
{
if (file == null ||
file.Length == 0 ||
string.IsNullOrEmpty(bucketName))
{
throw new Exception("參數異常");
}
await _minIOClientInstance.UploadFile(file, bucketName);
}
/// <summary>
/// 刪除檔案
/// </summary>
public async Task DeleteFile(string fileName, string bucketName)
{
if (string.IsNullOrEmpty(fileName) ||
string.IsNullOrEmpty(bucketName))
{
throw new Exception("參數異常");
}
await _minIOClientInstance.DeleteFile(fileName, bucketName);
}
/// <summary>
/// 建立帳號
/// </summary>
public async Task<long> CreateAccount()
{
var createdId = _dataBase.CreateTeacher();
var bucketName = $@"{createdId}";
// 不存在就建立 Bucket
if (!await _minIOClientInstance.IsExistBucket(bucketName))
{
await _minIOClientInstance.CreateBucket(bucketName);
}
return createdId;
}
/// <summary>
/// 刪除帳號
/// </summary>
public async Task DeleteAccount(long id)
{
_dataBase.DeleteTeacher(id);
var bucketName = $@"{id}";
await _minIOClientInstance.DeleteBucket(bucketName);
}
}
API 與 Service 對應功能,並且檢視控制器提供 Html 畫面
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ITeacherManageService _teacherManageService;
public HomeController(ILogger<HomeController> logger,
ITeacherManageService teacherManageService)
{
_logger = logger;
_teacherManageService = teacherManageService;
}
/// <summary>
/// 頁面資料
/// </summary>
public IActionResult Index()
{
var getResult = (_teacherManageService.GetTeachers()).Result;
return View(getResult);
}
/// <summary>
/// 上傳單一檔案
/// </summary>
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file, string bucketName)
{
await _teacherManageService.UploadFile(file, bucketName);
return Ok("上傳成功");
}
/// <summary>
/// 下載單一檔案
/// </summary>
[HttpGet]
public async Task<IActionResult> DownloadFile(string fileName, string bucketName)
{
var fileStream = await _teacherManageService.DownloadFile(fileName, bucketName);
if (fileStream != null)
{
return new FileStreamResult(fileStream, "application/octet-stream")
{
FileDownloadName = fileName
};
}
return BadRequest(new { message = $@"Cannot download file {fileName}." });
}
/// <summary>
/// 刪除單一檔案
/// </summary>
[HttpGet]
public async Task<IActionResult> DeleteFile(string fileName, string bucketName)
{
await _teacherManageService.DeleteFile(fileName, bucketName);
return Ok($@"{fileName} 已刪除");
}
/// <summary>
/// 刪除帳號
/// </summary>
[HttpGet]
public async Task<IActionResult> DeleteAccount(long Id)
{
await _teacherManageService.DeleteAccount(Id);
return Ok(new { message = $@"教師已刪除 ID:{Id}" });
}
/// <summary>
/// 新建帳號
/// </summary>
[HttpGet]
public async Task<IActionResult> CreateAccount()
{
var createdId = await _teacherManageService.CreateAccount();
return Ok(new { message = $@"教師帳號已建立 ID:{createdId}" });
}
}
與 Home/Index 檢視器互動,此頁面實現了所有前端 Javascript 呼叫 API 的工作。
@model IEnumerable<Example.Common.FakeDataBase.Model.TeacherModel>
@{
ViewData["Title"] = "MinIO CRUD Example Page";
}
<div id="teachersContainer">
@await Html.PartialAsync("_TeachersPartial", Model)
</div>
@section Scripts {
<script>
// 1. Upload File
document.querySelectorAll('.uploadForm').forEach(function (form)
{
form.addEventListener('submit', async function (event) {
event.preventDefault();
debugger;
const formData = new FormData(form);
const bucketName = form.getAttribute('data-bucket');
formData.append('bucketName', bucketName);
try {
const response = await fetch('/Home/UploadFile', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('上傳失敗');
}
alert("上傳成功");
location.reload();
} catch (error) {
const uploadResultDiv = form.nextElementSibling;
uploadResultDiv.innerText = `上傳時發生錯誤: ${error.message}`;
}
});
});
// 2. Download File
function downloadFile(bucketName, fileName) {
// 呼叫 API 下載文件
fetch(`/Home/downloadfile?fileName=${encodeURIComponent(fileName)}&bucketName=${encodeURIComponent(bucketName)}`)
.then(response => {
if (!response.ok) {
throw new Error('File not found or server error.');
}
return response.blob();// 轉換為 Blob 形式
})
.then(blob => {
// 創建一個隱藏的 <a> 元素來下載文件
const url = window.URL.createObjectURL(blob);
const aDom = document.createElement('a');
aDom.href = url;
aDom.download = fileName;// 設置下載的文件名
document.body.appendChild(aDom);
aDom.click();// 模擬點擊下載
aDom.remove();// 下載完成後移除 <a> 元素
})
.catch(error => {
console.error('Download error:', error);
alert('Error downloading file: ' + error.message);
});
}
// 3. Delete File
function deleteFile(bucketName, fileName) {
fetch(`/Home/DeleteFile?fileName=${encodeURIComponent(fileName)}&bucketName=${encodeURIComponent(bucketName)}`)
.then(response => {
if (!response.ok) {
throw new Error('File not found or server error.');
}
// 刷新頁面
alert(fileName + ' 刪除成功!');
location.reload();
})
.catch(error => {
console.error('Delete error:', error);
alert('Delete file Error: ' + error.message);
});
}
// 4. Delete Account
function deleteAccount(Id) {
fetch(`/Home/DeleteAccount?Id=${encodeURIComponent(Id)}`)
.then(response => {
return response.json();
})
.then(data => {
alert(data.message);
location.reload();
})
.catch(error => {
console.error('Delete Account error:', error);
alert('Delete Account Error: ' + error.message);
});
}
// 5. Create Account
function createAccount() {
fetch(`/Home/CreateAccount`)
.then(response => {
return response.json();
})
.then(data => {
alert(data.message);
location.reload();
})
.catch(error => {
console.error('Create Account error:', error);
alert('Create Account Error: ' + error.message);
});
}
</script>
}
與 Home/Index 檢視器互動,此頁面為首頁的一部分,會將取得的資料渲染
@model IEnumerable<Example.Common.FakeDataBase.Model.TeacherModel>
<div>
<div>
<button id="createAccountButton" onclick="createAccount()">新增帳號</button>
</div>
@foreach (var item in Model)
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>上傳操作</th>
<th>刪除帳號</th>
</tr>
</thead>
<tbody>
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>
<!-- 上傳表單 -->
<form class="uploadForm" enctype="multipart/form-data" data-bucket="@item.Id">
<input type="file" name="file" required />
<button type="submit">執行上傳</button>
</form>
<div class="uploadResult"></div>
</td>
<td>
<button id="deleteAccountButton_@item.Id" onclick="deleteAccount('@item.Id')">刪除帳號</button>
</td>
</tr>
<tr>
<td colspan="4">
<table class="table">
<thead>
<tr>
<th>檔案名稱</th>
<th>檔案大小</th>
<th>副檔名</th>
<th>最後更新時間</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var file in item.MySelfFiles.Files)
{
<tr>
<td>@file.FileName</td>
<td>@file.ShowSize</td>
<td>@file.FileExtension</td>
<td>@file.LastUpdateTime</td>
<td>
<!-- 下載按鈕 -->
<button id="downloadButton_@file.FileName" onclick="downloadFile('@item.Id', '@file.FileName')">下載</button>
<!-- 刪除按鈕 -->
<button id="deleteButton_@file.FileName" onclick="deleteFile('@item.Id', '@file.FileName')">刪除</button>
</td>
</tr>
}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<br/>
<br/>
<br/>
<br/>
}
</div>
初始化配置, MinIO 設定、依賴注入
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"MinIO": {
"Host": "192.168.51.188",
"Port": 9011,
"AccessKey": "uaub5kcqjFJovEpKZWr1",
"SecretKey": "fIsxDIHae8Zx8Sa7wJt6KgN1hCXE490cLX1YOPRL"
}
}
// 1. 注入相依 - MinIO 皆為單例
builder.Services.AddSingleton<IMinIOClientFactory, MinIOClientFactory>();
builder.Services.AddSingleton<FakeDataBase>();
builder.Services.AddSingleton<MinIOClientInstance>();
builder.Services.AddSingleton<MinIOConnectionModel>();
// 2. 注入相依 - Service 為 Scoped
builder.Services.AddScoped<ITeacherManageService, TeacherManageService>();
啟動範例專案後,預設可以看到如下畫面
這時檢閱 MinIO 伺服器可以看到會依照教師 ID 自動建立 Bucket,以便於保存教師的檔案
執行新增帳號按鈕
會自動刷新頁面,最下方會出現新建的教師資料,模擬新建的行為
模擬出新增帳號後,MinIO Server 會建立 Bucket ,來保存這個教師的檔案資料
執行選擇檔案按鈕,並且選擇一個測試檔案
執行執行上傳按鈕,並得到響應
頁面自動刷新後,出現檔案資訊
MinIo Server 檢查確實上傳成功
執行下載按鈕,也可以成功從 MinIO Server 取得此檔案
執行刪除按鈕,實際刪除此檔案
執行刪除帳號按鈕,將此教師移除
MinIo Server 此 Bucket 也會完整移除。