Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
namespace RobotNet10.StorageManager;
|
||||
|
||||
public interface IStorageManager: IDisposable
|
||||
{
|
||||
Task UploadAsync(string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken);
|
||||
Task<string> GetUrlAsync(string path, string objectName, CancellationToken cancellationToken);
|
||||
Task<Stream> GetFileAsync(string path, string objectName, CancellationToken cancellationToken);
|
||||
Task DeleteAsync(string path, string objectName, CancellationToken cancellationToken);
|
||||
Task<bool> ExistsAsync(string path, string objectName, CancellationToken cancellationToken);
|
||||
Task<List<string>> ListAsync(string path, bool recursive, CancellationToken cancellationToken);
|
||||
Task CopyAsync(string sourcePath, string destPath, string objectName, CancellationToken cancellationToken);
|
||||
Task<StorageMetadata> GetMetadataAsync(string path, string objectName, CancellationToken cancellationToken);
|
||||
}
|
||||
621
srcs/RobotNet10/Commons/RobotNet10.StorageManager/README.md
Normal file
621
srcs/RobotNet10/Commons/RobotNet10.StorageManager/README.md
Normal file
@@ -0,0 +1,621 @@
|
||||
# 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](#tính-năng)
|
||||
- [Cài đặt](#cài-đặt)
|
||||
- [Cấu hình](#cấu-hình)
|
||||
- [Cách sử dụng](#cách-sử-dụng)
|
||||
- [API Reference](#api-reference)
|
||||
- [Xử lý lỗi](#xử-lý-lỗi)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## ✨ 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:
|
||||
|
||||
```xml
|
||||
<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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
// 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` → `.png`
|
||||
- `image/jpeg` → `.jpg`
|
||||
- `application/pdf` → `.pdf`
|
||||
- `application/json` → `.json`
|
||||
- Và nhiều format khác...
|
||||
|
||||
### Get URL
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
// 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ề `MemoryStream` chứa toàn bộ nội dung file đã download
|
||||
- **Quan trọng**: Luôn sử dụng `using` statement để dispose stream sau khi sử dụng
|
||||
|
||||
### Delete File
|
||||
|
||||
```csharp
|
||||
await storageManager.DeleteAsync(
|
||||
path: "maps/level1",
|
||||
objectName: "map_image",
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
```
|
||||
|
||||
### Check File Exists
|
||||
|
||||
```csharp
|
||||
var exists = await storageManager.ExistsAsync(
|
||||
path: "maps/level1",
|
||||
objectName: "map_image",
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
if (exists)
|
||||
{
|
||||
Console.WriteLine("File exists!");
|
||||
}
|
||||
```
|
||||
|
||||
### List Files
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
await storageManager.CopyAsync(
|
||||
sourcePath: "maps/level1",
|
||||
destPath: "maps/backup",
|
||||
objectName: "map_image",
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
```
|
||||
|
||||
### Get File Metadata
|
||||
|
||||
```csharp
|
||||
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 file
|
||||
- `objectName`: Tên file/object
|
||||
- `data`: Stream chứa dữ liệu file
|
||||
- `size`: Kích thước file (bytes)
|
||||
- `contentType`: MIME type của file (ví dụ: "image/png", "application/pdf")
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentNullException`: Khi `data` hoặc `config` là null
|
||||
- `ArgumentException`: Khi `path`, `objectName`, `contentType` invalid hoặc `size < 0`
|
||||
- `ArgumentException`: 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ục
|
||||
- `objectName`: 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`: Khi `path` hoặc `objectName` invalid
|
||||
|
||||
---
|
||||
|
||||
#### `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ục
|
||||
- `objectName`: Tên file/object
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Returns:**
|
||||
- Local storage: `FileStream` đọc trực tiếp từ file system
|
||||
- MinIO storage: `MemoryStream` chứa toàn bộ nội dung file đã download
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `path` hoặc `objectName` invalid
|
||||
- `FileNotFoundException`: 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ục
|
||||
- `objectName`: Tên file/object
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `path` hoặc `objectName` invalid
|
||||
|
||||
---
|
||||
|
||||
#### `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ục
|
||||
- `objectName`: Tên file/object
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Returns:**
|
||||
- `true`: File tồn tại
|
||||
- `false`: File không tồn tại
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `path` hoặc `objectName` invalid
|
||||
|
||||
---
|
||||
|
||||
#### `Task<List<string>> ListAsync(string path, bool recursive, CancellationToken cancellationToken)`
|
||||
|
||||
Liệt kê files trong path.
|
||||
|
||||
**Parameters:**
|
||||
- `path`: Đường dẫn/thư mục
|
||||
- `recursive`: `true` = bao gồm subfolders, `false` = chỉ files trong path hiện tại
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Returns:**
|
||||
- List các file paths (relative paths)
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `path` invalid
|
||||
|
||||
---
|
||||
|
||||
#### `Task CopyAsync(string sourcePath, string destPath, string objectName, CancellationToken cancellationToken)`
|
||||
|
||||
Copy file từ source path sang dest path.
|
||||
|
||||
**Parameters:**
|
||||
- `sourcePath`: Đường dẫn nguồn
|
||||
- `destPath`: Đường dẫn đích
|
||||
- `objectName`: Tên file/object
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `sourcePath`, `destPath`, hoặc `objectName` invalid
|
||||
|
||||
---
|
||||
|
||||
#### `Task<StorageMetadata> GetMetadataAsync(string path, string objectName, CancellationToken cancellationToken)`
|
||||
|
||||
Lấy metadata của file.
|
||||
|
||||
**Parameters:**
|
||||
- `path`: Đường dẫn/thư mục
|
||||
- `objectName`: Tên file/object
|
||||
- `cancellationToken`: Cancellation token
|
||||
|
||||
**Returns:**
|
||||
- `StorageMetadata` object chứa thông tin file
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException`: Khi `path` hoặc `objectName` invalid
|
||||
- `FileNotFoundException`: Khi file không tồn tại
|
||||
|
||||
---
|
||||
|
||||
### StorageMetadata Class
|
||||
|
||||
```csharp
|
||||
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ý:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
using var storageManager = new StorageManager(config);
|
||||
// StorageManager sẽ tự động dispose khi out of scope
|
||||
```
|
||||
|
||||
### 2. Validate input trước khi upload
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
await storageManager.UploadAsync(path, objectName, data, size, contentType, cts.Token);
|
||||
```
|
||||
|
||||
### 4. Xử lý large files
|
||||
|
||||
```csharp
|
||||
// Đố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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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ó]
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Minio;
|
||||
|
||||
namespace RobotNet10.StorageManager;
|
||||
|
||||
public class MinioConfig
|
||||
{
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
public string User { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public bool EnableSSL { get; set; }
|
||||
}
|
||||
|
||||
public class StorageConfig
|
||||
{
|
||||
public bool UsingLocal { get; set; }
|
||||
public string LocalFolder { get; set; } = "Images";
|
||||
public string Bucket { get; set; } = string.Empty;
|
||||
public MinioConfig? MinioConfig { get; set; }
|
||||
public int RetryCount { get; set; } = 3;
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
using Minio;
|
||||
using Minio.DataModel;
|
||||
using Minio.DataModel.Args;
|
||||
|
||||
namespace RobotNet10.StorageManager;
|
||||
|
||||
public class StorageManager : IStorageManager
|
||||
{
|
||||
private readonly IMinioClient? MinioClient;
|
||||
private readonly StorageConfig StorageConfig;
|
||||
|
||||
public StorageManager(StorageConfig config)
|
||||
{
|
||||
StorageConfig = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
if (!StorageConfig.UsingLocal)
|
||||
{
|
||||
if (StorageConfig.MinioConfig == null)
|
||||
throw new ArgumentException("MinioConfig is required when UsingLocal is false", nameof(config));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(StorageConfig.MinioConfig.Endpoint))
|
||||
throw new ArgumentException("MinioConfig.Endpoint cannot be null or empty", nameof(config));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(StorageConfig.Bucket))
|
||||
throw new ArgumentException("Bucket cannot be null or empty when UsingLocal is false", nameof(config));
|
||||
|
||||
MinioClient = new MinioClient()
|
||||
.WithEndpoint(StorageConfig.MinioConfig.Endpoint)
|
||||
.WithCredentials(StorageConfig.MinioConfig.User, StorageConfig.MinioConfig.Password)
|
||||
.WithSSL(StorageConfig.MinioConfig.EnableSSL)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePath(string path)
|
||||
{
|
||||
if (path.Contains(".."))
|
||||
throw new ArgumentException("Path cannot contain '..'", nameof(path));
|
||||
|
||||
var invalidChars = Path.GetInvalidPathChars();
|
||||
if (path.Any(c => invalidChars.Contains(c)))
|
||||
throw new ArgumentException("Path contains invalid characters", nameof(path));
|
||||
}
|
||||
|
||||
private static void ValidateObjectName(string objectName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(objectName))
|
||||
throw new ArgumentException("ObjectName cannot be null or empty", nameof(objectName));
|
||||
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
if (objectName.Any(c => invalidChars.Contains(c)))
|
||||
throw new ArgumentException("ObjectName contains invalid characters", nameof(objectName));
|
||||
}
|
||||
|
||||
private static string GetFileExtension(string contentType)
|
||||
{
|
||||
return contentType.ToLowerInvariant() switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" => ".jpg",
|
||||
"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",
|
||||
_ => ".bin"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, CancellationToken cancellationToken)
|
||||
{
|
||||
int maxRetries = StorageConfig.RetryCount;
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await operation();
|
||||
}
|
||||
catch (Exception) when (i < maxRetries - 1)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)), cancellationToken);
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException($"Operation failed after {maxRetries} retries");
|
||||
}
|
||||
|
||||
private async Task UploadLocal(string path, string objectName, Stream data, string contentType, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = GetFileExtension(contentType);
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
if (!Directory.Exists(localFolder)) Directory.CreateDirectory(localFolder);
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
|
||||
var pathLocal = Path.Combine(folderPath, $"{objectName}{extension}");
|
||||
if (File.Exists($"{pathLocal}.bk")) File.Delete($"{pathLocal}.bk");
|
||||
if (File.Exists(pathLocal)) File.Move(pathLocal, $"{pathLocal}.bk");
|
||||
using Stream fileStream = new FileStream(pathLocal, FileMode.Create);
|
||||
await data.CopyToAsync(fileStream, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task UploadMinio(IMinioClient minioClient, string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken)
|
||||
{
|
||||
await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var beArgs = new BucketExistsArgs().WithBucket(StorageConfig.Bucket);
|
||||
bool found = await minioClient.BucketExistsAsync(beArgs, cancellationToken).ConfigureAwait(false);
|
||||
if (!found)
|
||||
{
|
||||
var mbArgs = new MakeBucketArgs()
|
||||
.WithBucket(StorageConfig.Bucket);
|
||||
await minioClient.MakeBucketAsync(mbArgs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
var putObjectArgs = new PutObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}")
|
||||
.WithObjectSize(size)
|
||||
.WithStreamData(data)
|
||||
.WithContentType(contentType);
|
||||
await minioClient.PutObjectAsync(putObjectArgs, cancellationToken).ConfigureAwait(false);
|
||||
return Task.CompletedTask;
|
||||
}, cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
public async Task UploadAsync(string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
if (data != null)
|
||||
{
|
||||
if (size < 0)
|
||||
throw new ArgumentException("Size cannot be negative", nameof(size));
|
||||
if (string.IsNullOrWhiteSpace(contentType))
|
||||
throw new ArgumentException("ContentType cannot be null or empty", nameof(contentType));
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
await UploadMinio(MinioClient, path, objectName, data, size, contentType, cancellationToken);
|
||||
else
|
||||
await UploadLocal(path, objectName, data, contentType, cancellationToken);
|
||||
}
|
||||
else throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
private async Task<string> GetUrlLocal(string path, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
if (Directory.Exists(localFolder))
|
||||
{
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
// Try common extensions
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var pathLocal = Path.Combine(folderPath, $"{objectName}{ext}");
|
||||
if (File.Exists(pathLocal))
|
||||
{
|
||||
return pathLocal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private async Task<string> GetUrlMinio(IMinioClient minioClient, string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var presignedGetObjectArgs = new PresignedGetObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}")
|
||||
.WithExpiry(60 * 60 * 24);
|
||||
return await minioClient.PresignedGetObjectAsync(presignedGetObjectArgs);
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> GetUrlAsync(string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
return await GetUrlMinio(MinioClient, path, objectName, cancellationToken);
|
||||
else
|
||||
return await GetUrlLocal(path, objectName);
|
||||
}
|
||||
|
||||
private async Task<Stream> GetFileLocal(string path, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
if (Directory.Exists(localFolder))
|
||||
{
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
// Try common extensions
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var pathLocal = Path.Combine(folderPath, $"{objectName}{ext}");
|
||||
if (File.Exists(pathLocal))
|
||||
{
|
||||
return new FileStream(pathLocal, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new FileNotFoundException($"File not found: {path}/{objectName}");
|
||||
}
|
||||
|
||||
private async Task<Stream> GetFileMinio(IMinioClient minioClient, string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
var getObjectArgs = new GetObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}")
|
||||
.WithCallbackStream(async (stream, token) =>
|
||||
{
|
||||
await stream.CopyToAsync(memoryStream, token);
|
||||
});
|
||||
|
||||
await minioClient.GetObjectAsync(getObjectArgs, cancellationToken);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
return memoryStream;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Stream> GetFileAsync(string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
return await GetFileMinio(MinioClient, path, objectName, cancellationToken);
|
||||
else
|
||||
return await GetFileLocal(path, objectName);
|
||||
}
|
||||
|
||||
private async Task DeleteLocal(string path, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
if (Directory.Exists(localFolder))
|
||||
{
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
// Try common extensions
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var pathLocal = Path.Combine(folderPath, $"{objectName}{ext}");
|
||||
if (File.Exists(pathLocal))
|
||||
{
|
||||
File.Delete(pathLocal);
|
||||
if (File.Exists($"{pathLocal}.bk"))
|
||||
File.Delete($"{pathLocal}.bk");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeleteMinio(IMinioClient minioClient, string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var removeObjectArgs = new RemoveObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}")
|
||||
.WithVersionId("null");
|
||||
await minioClient.RemoveObjectAsync(removeObjectArgs, cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
await DeleteMinio(MinioClient, path, objectName, cancellationToken);
|
||||
else
|
||||
await DeleteLocal(path, objectName);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
return await ExistsMinio(MinioClient, path, objectName, cancellationToken);
|
||||
else
|
||||
return await ExistsLocal(path, objectName);
|
||||
}
|
||||
|
||||
private async Task<bool> ExistsLocal(string path, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
if (Directory.Exists(localFolder))
|
||||
{
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var pathLocal = Path.Combine(folderPath, $"{objectName}{ext}");
|
||||
if (File.Exists(pathLocal))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> ExistsMinio(IMinioClient minioClient, string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var statObjectArgs = new StatObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}");
|
||||
await minioClient.StatObjectAsync(statObjectArgs, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Minio.Exceptions.ObjectNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<string>> ListAsync(string path, bool recursive, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
return await ListMinio(MinioClient, path, recursive, cancellationToken);
|
||||
else
|
||||
return await ListLocal(path, recursive);
|
||||
}
|
||||
|
||||
private async Task<List<string>> ListLocal(string path, bool recursive)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
|
||||
if (!Directory.Exists(folderPath))
|
||||
return result;
|
||||
|
||||
if (recursive)
|
||||
{
|
||||
var allFiles = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories);
|
||||
var basePath = Path.GetFullPath(folderPath);
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(basePath, file);
|
||||
var relativePathNormalized = relativePath.Replace('\\', '/');
|
||||
result.Add(relativePathNormalized);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var files = Directory.GetFiles(folderPath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
result.Add(Path.GetFileName(file));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ListMinio(IMinioClient minioClient, string path, bool recursive, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var result = new List<string>();
|
||||
var listArgs = new ListObjectsArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithPrefix($"{path}/")
|
||||
.WithRecursive(recursive);
|
||||
|
||||
await foreach (var item in minioClient.ListObjectsEnumAsync(listArgs, cancellationToken))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Key))
|
||||
{
|
||||
var relativePath = item.Key.StartsWith($"{path}/")
|
||||
? item.Key[(path.Length + 1)..]
|
||||
: item.Key;
|
||||
result.Add(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task CopyAsync(string sourcePath, string destPath, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourcePath))
|
||||
throw new ArgumentException("SourcePath cannot be null or empty", nameof(sourcePath));
|
||||
if (string.IsNullOrWhiteSpace(destPath))
|
||||
throw new ArgumentException("DestPath cannot be null or empty", nameof(destPath));
|
||||
ValidatePath(sourcePath);
|
||||
ValidatePath(destPath);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
await CopyMinio(MinioClient, sourcePath, destPath, objectName, cancellationToken);
|
||||
else
|
||||
await CopyLocal(sourcePath, destPath, objectName);
|
||||
}
|
||||
|
||||
private async Task CopyLocal(string sourcePath, string destPath, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
var sourceFolder = Path.Combine(localFolder, sourcePath);
|
||||
var destFolder = Path.Combine(localFolder, destPath);
|
||||
|
||||
if (!Directory.Exists(destFolder))
|
||||
Directory.CreateDirectory(destFolder);
|
||||
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var sourceFile = Path.Combine(sourceFolder, $"{objectName}{ext}");
|
||||
if (File.Exists(sourceFile))
|
||||
{
|
||||
var destFile = Path.Combine(destFolder, $"{objectName}{ext}");
|
||||
File.Copy(sourceFile, destFile, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyMinio(IMinioClient minioClient, string sourcePath, string destPath, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var copySourceArgs = new CopySourceObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{sourcePath}/{objectName}");
|
||||
|
||||
var copyObjectArgs = new CopyObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{destPath}/{objectName}")
|
||||
.WithCopyObjectSource(copySourceArgs);
|
||||
|
||||
await minioClient.CopyObjectAsync(copyObjectArgs, cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<StorageMetadata> GetMetadataAsync(string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
||||
ValidatePath(path);
|
||||
ValidateObjectName(objectName);
|
||||
|
||||
if (!StorageConfig.UsingLocal && MinioClient != null)
|
||||
return await GetMetadataMinio(MinioClient, path, objectName, cancellationToken);
|
||||
else
|
||||
return await GetMetadataLocal(path, objectName);
|
||||
}
|
||||
|
||||
private async Task<StorageMetadata> GetMetadataLocal(string path, string objectName)
|
||||
{
|
||||
var localFolder = StorageConfig.LocalFolder;
|
||||
var folderPath = Path.Combine(localFolder, path);
|
||||
|
||||
var extensions = new[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".json", ".txt", ".bin" };
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var filePath = Path.Combine(folderPath, $"{objectName}{ext}");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return new StorageMetadata
|
||||
{
|
||||
Path = path,
|
||||
ObjectName = objectName,
|
||||
Size = fileInfo.Length,
|
||||
ContentType = GetContentTypeFromExtension(ext),
|
||||
LastModified = fileInfo.LastWriteTime,
|
||||
ETag = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"File not found: {path}/{objectName}");
|
||||
}
|
||||
|
||||
private async Task<StorageMetadata> GetMetadataMinio(IMinioClient minioClient, string path, string objectName, CancellationToken cancellationToken)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var statObjectArgs = new StatObjectArgs()
|
||||
.WithBucket(StorageConfig.Bucket)
|
||||
.WithObject($"{path}/{objectName}");
|
||||
|
||||
var stat = await minioClient.StatObjectAsync(statObjectArgs, cancellationToken);
|
||||
|
||||
return new StorageMetadata
|
||||
{
|
||||
Path = path,
|
||||
ObjectName = objectName,
|
||||
Size = stat.Size,
|
||||
ContentType = stat.ContentType ?? string.Empty,
|
||||
LastModified = stat.LastModified,
|
||||
ETag = stat.ETag
|
||||
};
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private static string GetContentTypeFromExtension(string extension)
|
||||
{
|
||||
return extension.ToLowerInvariant() switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".jpg" => "image/jpeg",
|
||||
".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
".bmp" => "image/bmp",
|
||||
".webp" => "image/webp",
|
||||
".pdf" => "application/pdf",
|
||||
".json" => "application/json",
|
||||
".txt" => "text/plain",
|
||||
".csv" => "text/csv",
|
||||
".xml" => "application/xml",
|
||||
".zip" => "application/zip",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (MinioClient is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace RobotNet10.StorageManager;
|
||||
|
||||
public class StorageMetadata
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string ObjectName { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public DateTime? LastModified { get; set; }
|
||||
public string? ETag { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user