17 KiB
RobotNet10.StorageManager
Thư viện quản lý lưu trữ file hỗ trợ cả Local File System và MinIO Object Storage. Được thiết kế để lưu trữ và quản lý các file đa dạng (ảnh, PDF, JSON, v.v.) với khả năng tự động detect file extension từ Content-Type.
📋 Mục lục
✨ Tính năng
- ✅ Dual Storage Support: Hỗ trợ cả Local File System và MinIO Object Storage
- ✅ Multi-format Support: Tự động detect và hỗ trợ nhiều file types (PNG, JPG, PDF, JSON, TXT, v.v.)
- ✅ Retry Logic: Tự động retry với exponential backoff cho MinIO operations
- ✅ Path Security: Validate paths để tránh path traversal attacks
- ✅ Additional Operations: Exists, List, Copy, GetMetadata
- ✅ Backup Support: Tự động backup file cũ khi upload file mới (Local storage)
- ✅ Presigned URLs: Hỗ trợ presigned URLs cho MinIO (24h expiry)
📦 Cài đặt
Thêm Project Reference
Thêm reference vào project của bạn:
<ProjectReference Include="..\Commons\RobotNet10.StorageManager\RobotNet10.StorageManager.csproj" />
Dependencies
Project này sử dụng:
- Minio (Version 7.0.0) - Cho MinIO object storage support
- .NET 10.0
⚙️ Cấu hình
StorageConfig
Tạo instance của StorageConfig để cấu hình storage:
using RobotNet10.StorageManager;
// Cấu hình cho Local Storage
var localConfig = new StorageConfig
{
UsingLocal = true,
LocalFolder = "MapImages", // Thư mục lưu trữ local
RetryCount = 3 // Số lần retry (mặc định: 3)
};
// Cấu hình cho MinIO Storage
var minioConfig = new StorageConfig
{
UsingLocal = false,
Bucket = "my-bucket", // Tên bucket
RetryCount = 3,
MinioConfig = new MinioConfig
{
Endpoint = "localhost:9000",
User = "minioadmin",
Password = "minioadmin",
EnableSSL = false
}
};
MinioConfig Properties
| Property | Type | Mô tả |
|---|---|---|
Endpoint |
string | MinIO server endpoint (ví dụ: "localhost:9000") |
User |
string | Access key / Username |
Password |
string | Secret key / Password |
EnableSSL |
bool | Bật/tắt SSL connection |
StorageConfig Properties
| Property | Type | Mô tả |
|---|---|---|
UsingLocal |
bool | true = Local storage, false = MinIO storage |
LocalFolder |
string | Tên thư mục cho local storage (mặc định: "Images") |
Bucket |
string | Tên bucket cho MinIO (required khi UsingLocal = false) |
MinioConfig |
MinioConfig? | Cấu hình MinIO (required khi UsingLocal = false) |
RetryCount |
int | Số lần retry cho MinIO operations (mặc định: 3) |
🚀 Cách sử dụng
Khởi tạo StorageManager
using RobotNet10.StorageManager;
// Tạo config
var config = new StorageConfig
{
UsingLocal = true,
LocalFolder = "MapImages"
};
// Tạo StorageManager instance
var storageManager = new StorageManager(config);
// Nhớ dispose khi không dùng nữa
storageManager.Dispose();
Upload File
// Upload file từ Stream
var fileData = new MemoryStream(File.ReadAllBytes("map.png"));
await storageManager.UploadAsync(
path: "maps/level1",
objectName: "map_image",
data: fileData,
size: fileData.Length,
contentType: "image/png",
cancellationToken: CancellationToken.None
);
// Upload file từ FileStream
using var fileStream = new FileStream("document.pdf", FileMode.Open);
await storageManager.UploadAsync(
path: "documents",
objectName: "manual",
data: fileStream,
size: fileStream.Length,
contentType: "application/pdf",
cancellationToken: CancellationToken.None
);
Lưu ý: File extension sẽ được tự động detect từ contentType:
image/png→.pngimage/jpeg→.jpgapplication/pdf→.pdfapplication/json→.json- Và nhiều format khác...
Get URL
// Local storage: Trả về file path
var url = await storageManager.GetUrlAsync("maps/level1", "map_image");
// Kết quả: "C:\...\MapImages\maps\level1\map_image.png"
// MinIO storage: Trả về presigned URL (24h expiry)
var url = await storageManager.GetUrlAsync("maps/level1", "map_image");
// Kết quả: "https://minio-server:9000/bucket/maps/level1/map_image?X-Amz-Algorithm=..."
Get File
// Lấy file dưới dạng Stream
using var fileStream = await storageManager.GetFileAsync(
path: "maps/level1",
objectName: "map_image",
cancellationToken: CancellationToken.None
);
// Đọc dữ liệu từ stream
var buffer = new byte[fileStream.Length];
await fileStream.ReadAsync(buffer, 0, (int)fileStream.Length);
// Hoặc copy sang file khác
using var outputFile = new FileStream("output.png", FileMode.Create);
await fileStream.CopyToAsync(outputFile);
Lưu ý:
- Local storage: Trả về
FileStreamđọc trực tiếp từ file system - MinIO storage: Trả về
MemoryStreamchứa toàn bộ nội dung file đã download - Quan trọng: Luôn sử dụng
usingstatement để dispose stream sau khi sử dụng
Delete File
await storageManager.DeleteAsync(
path: "maps/level1",
objectName: "map_image",
cancellationToken: CancellationToken.None
);
Check File Exists
var exists = await storageManager.ExistsAsync(
path: "maps/level1",
objectName: "map_image",
cancellationToken: CancellationToken.None
);
if (exists)
{
Console.WriteLine("File exists!");
}
List Files
// List files trong path (non-recursive)
var files = await storageManager.ListAsync(
path: "maps",
recursive: false,
cancellationToken: CancellationToken.None
);
// Kết quả: ["level1/map_image.png", "level2/map_image.png"]
// List files recursive (bao gồm subfolders)
var allFiles = await storageManager.ListAsync(
path: "maps",
recursive: true,
cancellationToken: CancellationToken.None
);
// Kết quả: ["level1/map_image.png", "level1/subfolder/other.png", "level2/map_image.png"]
Copy File
await storageManager.CopyAsync(
sourcePath: "maps/level1",
destPath: "maps/backup",
objectName: "map_image",
cancellationToken: CancellationToken.None
);
Get File Metadata
var metadata = await storageManager.GetMetadataAsync(
path: "maps/level1",
objectName: "map_image",
cancellationToken: CancellationToken.None
);
Console.WriteLine($"Size: {metadata.Size} bytes");
Console.WriteLine($"ContentType: {metadata.ContentType}");
Console.WriteLine($"LastModified: {metadata.LastModified}");
Console.WriteLine($"ETag: {metadata.ETag}"); // Chỉ có cho MinIO
📚 API Reference
IStorageManager Interface
Task UploadAsync(string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken)
Upload file vào storage.
Parameters:
path: Đường dẫn/thư mục để lưu fileobjectName: Tên file/objectdata: Stream chứa dữ liệu filesize: Kích thước file (bytes)contentType: MIME type của file (ví dụ: "image/png", "application/pdf")cancellationToken: Cancellation token
Throws:
ArgumentNullException: Khidatahoặcconfiglà nullArgumentException: Khipath,objectName,contentTypeinvalid hoặcsize < 0ArgumentException: Khi path chứa..hoặc invalid characters
Task<string> GetUrlAsync(string path, string objectName)
Lấy URL/path của file.
Parameters:
path: Đường dẫn/thư mụcobjectName: Tên file/object
Returns:
- Local storage: File path (string)
- MinIO storage: Presigned URL (string, 24h expiry)
- Không tìm thấy: Empty string
Throws:
ArgumentException: KhipathhoặcobjectNameinvalid
Task<Stream> GetFileAsync(string path, string objectName, CancellationToken cancellationToken)
Lấy file dưới dạng Stream để đọc nội dung.
Parameters:
path: Đường dẫn/thư mụcobjectName: Tên file/objectcancellationToken: Cancellation token
Returns:
- Local storage:
FileStreamđọc trực tiếp từ file system - MinIO storage:
MemoryStreamchứa toàn bộ nội dung file đã download
Throws:
ArgumentException: KhipathhoặcobjectNameinvalidFileNotFoundException: Khi file không tồn tại
Lưu ý: Luôn sử dụng using statement để dispose stream sau khi sử dụng xong.
Task DeleteAsync(string path, string objectName, CancellationToken cancellationToken)
Xóa file từ storage.
Parameters:
path: Đường dẫn/thư mụcobjectName: Tên file/objectcancellationToken: Cancellation token
Throws:
ArgumentException: KhipathhoặcobjectNameinvalid
Task<bool> ExistsAsync(string path, string objectName, CancellationToken cancellationToken)
Kiểm tra file có tồn tại không.
Parameters:
path: Đường dẫn/thư mụcobjectName: Tên file/objectcancellationToken: Cancellation token
Returns:
true: File tồn tạifalse: File không tồn tại
Throws:
ArgumentException: KhipathhoặcobjectNameinvalid
Task<List<string>> ListAsync(string path, bool recursive, CancellationToken cancellationToken)
Liệt kê files trong path.
Parameters:
path: Đường dẫn/thư mụcrecursive:true= bao gồm subfolders,false= chỉ files trong path hiện tạicancellationToken: Cancellation token
Returns:
- List các file paths (relative paths)
Throws:
ArgumentException: Khipathinvalid
Task CopyAsync(string sourcePath, string destPath, string objectName, CancellationToken cancellationToken)
Copy file từ source path sang dest path.
Parameters:
sourcePath: Đường dẫn nguồndestPath: Đường dẫn đíchobjectName: Tên file/objectcancellationToken: Cancellation token
Throws:
ArgumentException: KhisourcePath,destPath, hoặcobjectNameinvalid
Task<StorageMetadata> GetMetadataAsync(string path, string objectName, CancellationToken cancellationToken)
Lấy metadata của file.
Parameters:
path: Đường dẫn/thư mụcobjectName: Tên file/objectcancellationToken: Cancellation token
Returns:
StorageMetadataobject chứa thông tin file
Throws:
ArgumentException: KhipathhoặcobjectNameinvalidFileNotFoundException: Khi file không tồn tại
StorageMetadata Class
public class StorageMetadata
{
public string Path { get; set; } // Full path của file
public string ObjectName { get; set; } // Tên file/object
public long Size { get; set; } // Kích thước file (bytes)
public string ContentType { get; set; } // MIME type
public DateTime? LastModified { get; set; } // Thời gian sửa đổi cuối
public string? ETag { get; set; } // ETag (chỉ có cho MinIO)
}
⚠️ Xử lý lỗi
StorageManager không catch exceptions - tất cả exceptions sẽ bubble up để caller xử lý:
try
{
await storageManager.UploadAsync(path, objectName, data, size, contentType, ct);
}
catch (ArgumentNullException ex)
{
// Handle null argument
Console.WriteLine($"Null argument: {ex.Message}");
}
catch (ArgumentException ex)
{
// Handle invalid argument
Console.WriteLine($"Invalid argument: {ex.Message}");
}
catch (IOException ex)
{
// Handle I/O errors (local storage)
Console.WriteLine($"I/O error: {ex.Message}");
}
catch (Minio.Exceptions.MinioException ex)
{
// Handle MinIO errors
Console.WriteLine($"MinIO error: {ex.Message}");
}
Common Exceptions
| Exception | Nguyên nhân |
|---|---|
ArgumentNullException |
Required parameter là null |
ArgumentException |
Invalid path, objectName, hoặc config |
FileNotFoundException |
File không tồn tại (GetMetadataAsync) |
IOException |
I/O errors (local storage) |
Minio.Exceptions.MinioException |
MinIO operation errors |
InvalidOperationException |
Retry failed sau nhiều lần thử |
💡 Best Practices
1. Sử dụng using statement
using var storageManager = new StorageManager(config);
// StorageManager sẽ tự động dispose khi out of scope
2. Validate input trước khi upload
if (string.IsNullOrWhiteSpace(fileName))
throw new ArgumentException("FileName cannot be empty");
if (fileStream.Length == 0)
throw new ArgumentException("File cannot be empty");
3. Sử dụng CancellationToken
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await storageManager.UploadAsync(path, objectName, data, size, contentType, cts.Token);
4. Xử lý large files
// Đối với large files, nên stream trực tiếp từ file
using var fileStream = new FileStream("large-file.pdf", FileMode.Open, FileAccess.Read);
await storageManager.UploadAsync(
path: "documents",
objectName: "large-file",
data: fileStream,
size: fileStream.Length,
contentType: "application/pdf",
cancellationToken: CancellationToken.None
);
5. Check file exists trước khi operations
if (await storageManager.ExistsAsync(path, objectName, ct))
{
var metadata = await storageManager.GetMetadataAsync(path, objectName, ct);
Console.WriteLine($"File size: {metadata.Size} bytes");
}
6. Sử dụng ListAsync với recursive cẩn thận
// Với large directories, recursive listing có thể chậm
// Nên sử dụng non-recursive nếu có thể
var files = await storageManager.ListAsync(path, recursive: false, ct);
7. Configure RetryCount phù hợp
var config = new StorageConfig
{
UsingLocal = false,
Bucket = "my-bucket",
RetryCount = 5, // Tăng retry count cho unreliable networks
MinioConfig = new MinioConfig { ... }
};
📝 Ví dụ hoàn chỉnh
using RobotNet10.StorageManager;
using System.IO;
// 1. Cấu hình
var config = new StorageConfig
{
UsingLocal = true,
LocalFolder = "MapImages",
RetryCount = 3
};
// 2. Tạo StorageManager
using var storageManager = new StorageManager(config);
// 3. Upload file
var imageData = File.ReadAllBytes("map.png");
using var imageStream = new MemoryStream(imageData);
await storageManager.UploadAsync(
path: "maps/level1",
objectName: "map_image",
data: imageStream,
size: imageData.Length,
contentType: "image/png",
cancellationToken: CancellationToken.None
);
// 4. Check file exists
if (await storageManager.ExistsAsync("maps/level1", "map_image", CancellationToken.None))
{
// 5. Get metadata
var metadata = await storageManager.GetMetadataAsync(
"maps/level1",
"map_image",
CancellationToken.None
);
Console.WriteLine($"Uploaded: {metadata.Size} bytes");
// 6. Get URL
var url = await storageManager.GetUrlAsync("maps/level1", "map_image");
Console.WriteLine($"File URL: {url}");
// 6.5. Get File
using var fileStream = await storageManager.GetFileAsync(
"maps/level1",
"map_image",
CancellationToken.None
);
var fileData = new byte[fileStream.Length];
await fileStream.ReadAsync(fileData, 0, (int)fileStream.Length);
Console.WriteLine($"File data length: {fileData.Length} bytes");
// 7. List files
var files = await storageManager.ListAsync("maps", recursive: false, CancellationToken.None);
Console.WriteLine($"Total files: {files.Count}");
// 8. Copy file
await storageManager.CopyAsync(
"maps/level1",
"maps/backup",
"map_image",
CancellationToken.None
);
// 9. Delete file
await storageManager.DeleteAsync("maps/level1", "map_image", CancellationToken.None);
}
🔧 Supported Content Types
StorageManager tự động detect file extension từ các Content-Type sau:
| Content-Type | Extension |
|---|---|
image/png |
.png |
image/jpeg, image/jpg |
.jpg |
image/gif |
.gif |
image/bmp |
.bmp |
image/webp |
.webp |
application/pdf |
.pdf |
application/json |
.json |
text/plain |
.txt |
text/csv |
.csv |
application/xml |
.xml |
application/zip |
.zip |
| Others | .bin (default) |
📄 License
[Thêm license information nếu có]
🤝 Contributing
[Thêm contributing guidelines nếu có]