# 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 ``` ### 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 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 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 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> 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 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ó]