20 KiB
Storage Service
Microservice .NET 10 để quản lý lưu trữ file hỗ trợ MinIO và Aliyun OSS.
Tính Năng
- Multi-provider Storage: MinIO (tương thích S3) và Aliyun OSS
- Chuyển đổi Provider Runtime: Chuyển đổi giữa MinIO và Aliyun qua biến môi trường
- CRUD File Đầy Đủ: Upload, download, delete, list files
- Pre-signed URLs: URL download/upload an toàn có thời hạn
- Quota User: Giới hạn dung lượng và số file cho mỗi user
- Giao tiếp Inter-service: Xác thực JWT qua IAM Service với caching
Bắt Đầu Nhanh
Yêu Cầu
- .NET 10 SDK
- Docker & Docker Compose
- PostgreSQL (hoặc Neon)
- MinIO (hoặc tài khoản Aliyun OSS)
Chạy với Docker
cd services/storage-service-net
docker-compose up -d
Truy cập: http://localhost:5002/swagger
Chạy Local
cd services/storage-service-net
# Cài đặt dependencies
dotnet restore
# Chạy migrations (lần đầu)
dotnet ef database update --project src/StorageService.Infrastructure --startup-project src/StorageService.API
# Khởi động service
dotnet run --project src/StorageService.API
Cấu Hình
Biến Môi Trường
| Biến | Mô tả | Mặc định |
|---|---|---|
Storage__Provider |
Chọn provider: minio hoặc aliyun |
minio |
Storage__DefaultBucket |
Tên bucket mặc định | storage |
Storage__MaxFileSizeBytes |
Kích thước file tối đa | 104857600 (100MB) |
Storage__PreSignedUrlExpirationSeconds |
Thời hạn pre-signed URL | 3600 |
Cấu Hình MinIO
| Biến | Mô tả | Mặc định |
|---|---|---|
Storage__MinIO__Endpoint |
Endpoint server MinIO | localhost:9000 |
Storage__MinIO__AccessKey |
Access key | - |
Storage__MinIO__SecretKey |
Secret key | - |
Storage__MinIO__UseSSL |
Bật SSL | false |
Cấu Hình Aliyun OSS
| Biến | Mô tả | Mặc định |
|---|---|---|
Storage__AliyunOSS__Endpoint |
OSS endpoint | - |
Storage__AliyunOSS__AccessKeyId |
Access key ID | - |
Storage__AliyunOSS__AccessKeySecret |
Access key secret | - |
Storage__AliyunOSS__Region |
OSS region | - |
Cấu Hình IAM Service
| Biến | Mô tả | Mặc định |
|---|---|---|
IamService__BaseUrl |
URL IAM Service | http://localhost:5001 |
IamService__ServiceName |
Định danh service | storage-service |
IamService__TimeoutSeconds |
Timeout request | 30 |
IamService__CacheDurationSeconds |
TTL cache user info | 300 |
IamService__HealthCheckCacheDurationSeconds |
TTL cache health check | 60 |
API Endpoints
Files (Legacy - Proxy Upload)
| Method | Endpoint | Mô tả |
|---|---|---|
POST |
/api/v1/files/upload |
Upload file qua backend (tối đa 100MB) |
GET |
/api/v1/files |
Danh sách files với phân trang |
GET |
/api/v1/files/{id} |
Lấy metadata file theo ID |
GET |
/api/v1/files/{id}/download-url |
Lấy pre-signed download URL |
DELETE |
/api/v1/files/{id} |
Xóa file (soft delete) |
Direct Upload (Khuyến nghị cho Scale lớn)
Lợi ích của Direct Upload:
- Backend không xử lý file binary → Giảm 90%+ tải
- Có thể xử lý hàng triệu uploads đồng thời
- Tốc độ upload nhanh hơn (không qua trung gian)
| Method | Endpoint | Mô tả |
|---|---|---|
POST |
/api/v1/storage/sign-upload |
Lấy pre-signed PUT URL để upload trực tiếp |
POST |
/api/v1/storage/confirm-upload |
Xác nhận upload và lưu metadata |
Quota
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/quota |
Lấy quota storage của user hiện tại |
Folders
Quan trọng: Folders chỉ là logic (trong database). File storage dùng flat UUID keys. Đổi tên/di chuyển là O(1).
| Method | Endpoint | Mô tả |
|---|---|---|
POST |
/api/v1/folders |
Tạo folder mới |
GET |
/api/v1/folders |
Danh sách root folders |
GET |
/api/v1/folders/{id} |
Lấy folder theo ID với children |
PUT |
/api/v1/folders/{id} |
Đổi tên folder |
DELETE |
/api/v1/folders/{id} |
Xóa folder (soft delete) |
File Versioning
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/files/{id}/versions |
Danh sách tất cả versions của file |
GET |
/api/v1/files/{id}/versions/{versionNumber}/download |
Lấy URL download cho version cụ thể |
POST |
/api/v1/files/{id}/versions/{versionNumber}/restore |
Khôi phục file về version cụ thể |
File Sharing
| Method | Endpoint | Mô tả |
|---|---|---|
POST |
/api/v1/storage/files/{id}/shares |
Tạo link chia sẻ với options |
GET |
/api/v1/storage/files/{id}/shares |
Lấy tất cả shares của file |
DELETE |
/api/v1/storage/shares/{id} |
Thu hồi link chia sẻ |
GET |
/api/v1/storage/shares/public/{token} |
Truy cập file chia sẻ (công khai, không cần auth) |
Tùy chọn Share:
permission: View, Download, Edit, Adminpassword: Bảo vệ bằng mật khẩu (tùy chọn)expiresAt: Ngày/giờ hết hạnmaxDownloads: Số lần download tối đa
Admin API (Yêu cầu Role: Admin)
Lưu ý: Các endpoints này yêu cầu role
AdminhoặcSuperAdmin.
Quản lý Quota
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/admin/quotas |
Lấy danh sách quota tất cả users |
GET |
/api/v1/admin/quotas/{userId} |
Lấy quota của user cụ thể |
PUT |
/api/v1/admin/quotas/{userId} |
Cập nhật quota limits |
Quản lý Files
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/admin/files |
Xem tất cả files với filter |
DELETE |
/api/v1/admin/files/{id} |
Xóa file vi phạm |
Quản lý Shares
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/admin/shares |
Xem tất cả shares |
DELETE |
/api/v1/admin/shares/{id} |
Revoke share vi phạm |
Thống kê
| Method | Endpoint | Mô tả |
|---|---|---|
GET |
/api/v1/admin/statistics |
Dashboard thống kê tổng hợp |
GET |
/api/v1/admin/statistics/users-near-limit |
Users gần hết quota (>80%) |
Ví Dụ Direct Upload (Khuyến nghị)
Bước 1: Lấy Pre-signed URL
curl -X POST "http://localhost:5002/api/v1/storage/sign-upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fileName": "document.pdf",
"fileSizeBytes": 1048576,
"contentType": "application/pdf",
"accessLevel": "private"
}'
Response:
{
"success": true,
"data": {
"uploadUrl": "http://minio:9000/storage/private/user123/20260113/abc12345_document.pdf?X-Amz-...",
"objectKey": "private/user123/20260113/abc12345_document.pdf",
"expiresAt": "2026-01-13T21:49:33Z"
}
}
Bước 2: Upload trực tiếp lên MinIO
curl -X PUT "${uploadUrl}" \
-H "Content-Type: application/pdf" \
--data-binary @document.pdf
Bước 3: Xác nhận Upload
curl -X POST "http://localhost:5002/api/v1/storage/confirm-upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"objectKey": "private/user123/20260113/abc12345_document.pdf",
"fileName": "document.pdf",
"fileSizeBytes": 1048576,
"contentType": "application/pdf",
"accessLevel": "private"
}'
Response:
{
"success": true,
"data": {
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"metadata": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"fileName": "document.pdf",
"contentType": "application/pdf",
"fileSizeBytes": 1048576,
"provider": "MinIO",
"accessLevel": "Private",
"uploadedAt": "2026-01-13T20:49:33Z"
}
}
}
Pre-signed URLs & Access Levels
Storage Service tạo các loại download URL khác nhau dựa trên accessLevel của file.
Tổng Quan Access Level
| Access Level | Storage Path Prefix | Loại Download URL | Use Case |
|---|---|---|---|
| Public | public/ |
Direct URL (không có signature) | Assets công khai, hình ảnh, CDN |
| Private | private/ |
Pre-signed URL (AWS Signature V4) | Files user, documents |
| Shared | shared/ |
Pre-signed URL (AWS Signature V4) | Files chia sẻ qua link |
Files Public
Files public có thể truy cập trực tiếp không cần authentication:
http://minio:9000/storage/public/2026/01/13/abc123.png
Đặc điểm:
- Không cần signature
- URL không bao giờ hết hạn
- Bất kỳ ai có URL đều có thể truy cập
- Phù hợp cho: avatars, thumbnails, downloads công khai
Files Private/Shared (Pre-signed URLs)
Files private và shared yêu cầu pre-signed URL với AWS Signature Version 4:
http://minio:9000/storage/private/2026/01/13/xyz789.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=minioadmin%2F20260113%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20260113T180024Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=2ce827d357d105fc1cf88240dee407e5ea72...
Giải Thích Các Tham Số Signature:
| Tham số | Mô tả | Ví dụ |
|---|---|---|
X-Amz-Algorithm |
Thuật toán ký | AWS4-HMAC-SHA256 |
X-Amz-Credential |
Access key + date/region/service | minioadmin/20260113/us-east-1/s3/aws4_request |
X-Amz-Date |
Thời điểm tạo signature | 20260113T180024Z |
X-Amz-Expires |
Thời hạn URL (giây) | 3600 (1 giờ) |
X-Amz-SignedHeaders |
Headers được ký | host |
X-Amz-Signature |
Chữ ký mã hóa | 2ce827d3... |
Lợi Ích Bảo Mật:
- URL hết hạn sau thời gian cấu hình (mặc định: 1 giờ)
- Signature ngăn chỉnh sửa URL
- Không lộ credentials MinIO cho client
- Mỗi URL là duy nhất và sử dụng một lần trong thực tế
API Lấy Download URL
curl -X GET "http://localhost:5002/api/v1/files/{id}/download-url" \
-H "Authorization: Bearer YOUR_TOKEN"
Response cho file Public:
{
"success": true,
"data": {
"downloadUrl": "http://minio:9000/storage/public/2026/01/13/abc123.png",
"expiresAt": null
}
}
Response cho file Private:
{
"success": true,
"data": {
"downloadUrl": "http://minio:9000/storage/private/2026/01/13/xyz789.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...",
"expiresAt": "2026-01-13T19:00:24Z"
}
}
Cấu Hình
| Biến | Mô tả | Mặc định |
|---|---|---|
Storage__PreSignedUrlExpirationSeconds |
Thời hạn pre-signed URL | 3600 (1 giờ) |
Lưu ý: Với files rất lớn hoặc kết nối chậm, hãy cân nhắc tăng thời hạn.
Kiến Trúc Logical Folder (Data Sovereignty)
⚠️ QUAN TRỌNG: Theo nguyên tắc Data Sovereignty trong microservices, folder là logical concept trong Database, KHÔNG phụ thuộc vào bucket structure.
Nguyên Tắc Thiết Kế
Storage Service sở hữu mô hình dữ liệu riêng:
- Database quản lý cấu trúc folder logic (hierarchy, paths, permissions)
- Bucket chỉ lưu trữ file binary với UUID keys (flat structure)
❌ Anti-pattern (KHÔNG LÀM)
Bucket structure dựa vào user path:
goodgo/
├── users/john/documents/report.pdf ← BAD
├── users/mary/images/photo.jpg ← BAD
└── users/bob/work/presentation.pptx ← BAD
VẤN ĐỀ:
- Đổi tên folder = Move hàng triệu files (chậm + rủi ro)
- Path predictable = Dễ bị attack
- Không scale với hàng triệu users
✅ Kiến Trúc Đúng (Logical Separation)
1️⃣ Database: Logical Structure
-- Folders table: Quản lý cây thư mục
CREATE TABLE folders (
id UUID PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
parent_id UUID REFERENCES folders(id),
name VARCHAR(255) NOT NULL,
path VARCHAR(1000) NOT NULL, -- VD: /docs/work/2024
created_at TIMESTAMP
);
-- Files table: Link đến folder logic
CREATE TABLE files (
id UUID PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
folder_id UUID REFERENCES folders(id), -- Logical folder
file_name VARCHAR(255) NOT NULL,
storage_key VARCHAR(500) UNIQUE, -- Physical key (UUID)
size_bytes BIGINT,
content_type VARCHAR(100),
access_level VARCHAR(20),
created_at TIMESTAMP
);
2️⃣ Bucket: Flat Physical Storage
Files được lưu với UUID-based keys theo pattern:
{prefix}/{year}/{month}/{day}/{uuid}.{ext}
Ví dụ:
goodgo/
├── private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf
├── private/2026/01/13/a7f3b2c19d8e4f01bcd5e92f7a4d8b63.jpg
├── public/2026/01/14/f9e4c1a2b7d36e5f8a0c4d9e2b1f7a8c.png
└── shared/2026/01/15/c3d8f2e1a9b47c6e5d0f8a3b2e1c7d9f.docx
KHÔNG CÓ cấu trúc user/folder trong bucket!
Tất cả là flat UUID keys.
3️⃣ Storage Key Generation
// Tự động generate physical key (KHÔNG dựa vào folder)
public string GenerateStorageKey(FileAccessLevel access, string fileName)
{
var now = DateTime.UtcNow;
var prefix = access == FileAccessLevel.Public ? "public" : "private";
var uuid = Guid.NewGuid().ToString("N");
var ext = Path.GetExtension(fileName);
// Pattern: {prefix}/{year}/{month}/{day}/{uuid}{ext}
return $"{prefix}/{now:yyyy}/{now:MM}/{now:dd}/{uuid}{ext}";
}
Lợi Ích
| Thao tác | Database (Logic) | Bucket (Physical) | Hiệu năng |
|---|---|---|---|
| Tạo folder | INSERT 1 row | Không làm gì | Instant |
| Đổi tên folder | UPDATE 1 row | Không làm gì | O(1) - Instant |
| Move file | UPDATE File.folder_id | Không làm gì | O(1) - Instant |
| Delete folder | Cascade delete | Queue cleanup | Background job |
So sánh với Anti-pattern:
- Đổi tên folder: O(1) vs O(n) - nhanh hơn 1000x
- Bảo mật: UUID keys không đoán được
- Migration: Dễ dàng chuyển storage provider
Workflow Ví Dụ
Upload file vào folder:
# 1. Tạo folder (chỉ trong Database)
POST /api/v1/folders
{
"name": "documents",
"parentId": null
}
# 2. Upload file vào folder
POST /api/v1/storage/sign-upload
{
"fileName": "report.pdf",
"folderId": "folder-uuid-123", # Logical folder
"fileSizeBytes": 1048576
}
# Response: Physical key KHÔNG chứa folder name
{
"uploadUrl": "...",
"objectKey": "private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf"
}
List files trong folder:
GET /api/v1/folders/{folderId}/files
# Database query: SELECT * FROM files WHERE folder_id = {folderId}
# Client nhìn thấy: /documents/report.pdf (logical path)
# Bucket lưu: private/2026/01/13/{uuid}.pdf (physical key)
📚 Chi tiết kỹ thuật: Xem ARCHITECTURE.md để hiểu sâu hơn về pattern này.
Ví Dụ Legacy Upload (Qua Backend)
curl -X POST "http://localhost:5002/api/v1/files/upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf"
Giao Tiếp Inter-Service
Service giao tiếp với IAM Service để xác thực user. Phần này giải thích cấu hình cần thiết cho giao tiếp inter-service trong môi trường Docker.
Cấu Hình Docker Network
Khi chạy trong Docker Compose, các services giao tiếp qua Docker network sử dụng tên container:
# docker-compose.yml
storage-service:
environment:
# QUAN TRỌNG: Sử dụng tên container cho giao tiếp inter-service
- IamService__BaseUrl=http://iam-service-net:8080
- IamService__ServiceName=storage-service
Lưu ý: URL
http://iam-service-net:8080sử dụng:
iam-service-net= Tên container Docker (không phảilocalhost)8080= Port nội bộ container (không phải port expose5001)
Cấu Hình JWT Token Issuer
Để đảm bảo JWT tokens hoạt động giữa các services, IAM Service phải sử dụng issuer URI cố định:
# IAM Service docker-compose.yml
iam-service-net:
environment:
- IdentityServer__IssuerUri=http://iam-service
Điều này đảm bảo tokens được issue bởi IAM Service có claim iss nhất quán bất kể client truy cập như thế nào.
Luồng Giao Tiếp
┌─────────────────┐ JWT Token (iss: http://iam-service) ┌─────────────────┐
│ Storage │ ──────────────────────────────────────────► │ IAM Service │
│ Service │ ◄────────────────────────────────────────── │ │
│ :8080 │ Thông tin user + Roles/Permissions │ :8080 │
└─────────────────┘ └─────────────────┘
│ Docker Network: goodgo-network │
└──────────────────────────────────────────────────────────────┘
Headers & Caching
- Headers:
Authorization: Bearer <token>,X-Service-Name: storage-service - Cache User Info: 5 phút (300 giây)
- Cache Health Check: 1 phút (60 giây)
- Resilience: Polly retry (3x) + circuit breaker
Các Phương Thức (IIamServiceClient)
| Phương thức | Mô tả |
|---|---|
ValidateUserAsync |
Xác thực JWT token và lấy thông tin user |
GetUserByIdAsync |
Lấy thông tin user theo ID |
GetUserRolesAsync |
Lấy danh sách roles của user |
GetUserPermissionsAsync |
Lấy danh sách permissions của user |
HasPermissionAsync |
Kiểm tra user có permission cụ thể |
HasRoleAsync |
Kiểm tra user có role cụ thể |
CheckHealthAsync |
Kiểm tra trạng thái health của IAM Service |
Xử Lý Sự Cố
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
401 Unauthorized |
JWT issuer không khớp | Set IdentityServer__IssuerUri trong IAM Service |
Connection refused |
URL sai hoặc container chưa chạy | Dùng tên container, không phải localhost |
Name resolution failed |
Container không cùng network | Đảm bảo cả hai services dùng goodgo-network |
Timeout |
IAM Service chậm/unhealthy | Kiểm tra: curl http://iam-service-net:8080/health |
Database Migrations
# Tạo migration mới
dotnet ef migrations add TenMigration \
--project src/StorageService.Infrastructure \
--startup-project src/StorageService.API
# Áp dụng migrations
dotnet ef database update \
--project src/StorageService.Infrastructure \
--startup-project src/StorageService.API
Testing
# Chạy tất cả tests
dotnet test
# Chạy với coverage
dotnet test --collect:"XPlat Code Coverage"
Cấu Trúc Dự Án
services/storage-service-net/
├── src/
│ ├── StorageService.API/ # Controllers, Commands, Queries
│ ├── StorageService.Domain/ # Entities, Repository interfaces
│ └── StorageService.Infrastructure/# Providers, DbContext, Repositories
├── tests/
│ ├── StorageService.UnitTests/
│ └── StorageService.FunctionalTests/
├── docs/
│ ├── en/ # Tài liệu tiếng Anh
│ └── vi/ # Tài liệu tiếng Việt
├── docker-compose.yml
├── Dockerfile
└── README.md
License
MIT