# 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

```bash
cd services/storage-service-net
docker-compose up -d
```

Truy cập: http://localhost:5002/swagger

### Chạy Local

```bash
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

```bash
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:
```json
{
  "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

```bash
curl -X PUT "${uploadUrl}" \
  -H "Content-Type: application/pdf" \
  --data-binary @document.pdf
```

### Bước 3: Xác nhận Upload

```bash
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:
```json
{
  "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

```bash
curl -X GET "http://localhost:5002/api/v1/files/{id}/download-url" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response cho file Public:**
```json
{
  "success": true,
  "data": {
    "downloadUrl": "http://minio:9000/storage/public/2026/01/13/abc123.png",
    "expiresAt": null
  }
}
```

**Response cho file Private:**
```json
{
  "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

```sql
-- 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

```csharp
// 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:**
```bash
# 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:**
```bash
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](./ARCHITECTURE.md) để hiểu sâu hơn về pattern này.

## Ví Dụ Legacy Upload (Qua Backend)

```bash
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:

```yaml
# 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:

```yaml
# 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

```bash
# 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

```bash
# 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
