Files
pos-system/services/storage-service-net/docs/vi/README.md

20 KiB
Raw Blame History

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, Admin
  • password: Bảo vệ bằng mật khẩu (tùy chọn)
  • expiresAt: Ngày/giờ hết hạn
  • maxDownloads: 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 Admin hoặc SuperAdmin.

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:8080 sử dụng:

  • iam-service-net = Tên container Docker (không phải localhost)
  • 8080 = Port nội bộ container (không phải port expose 5001)

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