# Storage Service

> A .NET 10 microservice for file storage management supporting MinIO and Aliyun OSS.

## Features

- **Multi-provider Storage**: MinIO (S3-compatible) and Aliyun OSS
- **Runtime Provider Switching**: Switch between MinIO and Aliyun via environment variable
- **Complete File CRUD**: Upload, download, delete, list files
- **Pre-signed URLs**: Secure time-limited download/upload URLs
- **User Quotas**: Storage capacity and file count limits per user
- **Inter-service Communication**: JWT validation via IAM Service with caching

## Quick Start

### Prerequisites

- .NET 10 SDK
- Docker & Docker Compose
- PostgreSQL (or Neon)
- MinIO (or Aliyun OSS account)

### Run with Docker

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

Access: http://localhost:5002/swagger

### Run Locally

```bash
cd services/storage-service-net

# Install dependencies
dotnet restore

# Run migrations (first time)
dotnet ef database update --project src/StorageService.Infrastructure --startup-project src/StorageService.API

# Start the service
dotnet run --project src/StorageService.API
```

## Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `Storage__Provider` | Provider selection: `minio` or `aliyun` | `minio` |
| `Storage__DefaultBucket` | Default bucket name | `storage` |
| `Storage__MaxFileSizeBytes` | Maximum file size | `104857600` (100MB) |
| `Storage__PreSignedUrlExpirationSeconds` | Pre-signed URL expiration | `3600` |

### MinIO Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| `Storage__MinIO__Endpoint` | MinIO server endpoint | `localhost:9000` |
| `Storage__MinIO__AccessKey` | Access key | - |
| `Storage__MinIO__SecretKey` | Secret key | - |
| `Storage__MinIO__UseSSL` | Enable SSL | `false` |

### Aliyun OSS Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| `Storage__AliyunOSS__Endpoint` | OSS endpoint | - |
| `Storage__AliyunOSS__AccessKeyId` | Access key ID | - |
| `Storage__AliyunOSS__AccessKeySecret` | Access key secret | - |
| `Storage__AliyunOSS__Region` | OSS region | - |

### IAM Service Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| `IamService__BaseUrl` | IAM Service URL | `http://localhost:5001` |
| `IamService__ServiceName` | Service identifier | `storage-service` |
| `IamService__TimeoutSeconds` | Request timeout | `30` |
| `IamService__CacheDurationSeconds` | User info cache TTL | `300` |
| `IamService__HealthCheckCacheDurationSeconds` | Health check cache TTL | `60` |

## API Endpoints

### Files (Legacy - Proxy Upload)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/files/upload` | Upload a file via backend (max 100MB) |
| `GET` | `/api/v1/files` | List user's files with pagination |
| `GET` | `/api/v1/files/{id}` | Get file metadata by ID |
| `GET` | `/api/v1/files/{id}/download-url` | Get pre-signed download URL |
| `DELETE` | `/api/v1/files/{id}` | Delete a file (soft delete) |

### Direct Upload (Recommended for Scale)

> **Benefits of Direct Upload:**
> - Backend doesn't handle file binary → 90%+ load reduction
> - Can handle millions of concurrent uploads
> - Faster upload speed (no intermediary)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/storage/sign-upload` | Get pre-signed PUT URL for direct upload |
| `POST` | `/api/v1/storage/confirm-upload` | Confirm upload and save metadata |

### Quota

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/quota` | Get current user's storage quota |

### Folders

> **Important:** Folders are logical (database-only). File storage uses flat UUID keys. Rename/move operations are O(1).

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/folders` | Create new folder |
| `GET` | `/api/v1/folders` | List root folders |
| `GET` | `/api/v1/folders/{id}` | Get folder by ID with children |
| `PUT` | `/api/v1/folders/{id}` | Rename folder |
| `DELETE` | `/api/v1/folders/{id}` | Delete folder (soft delete) |

### File Versioning

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/files/{id}/versions` | List all versions of a file |
| `GET` | `/api/v1/files/{id}/versions/{versionNumber}/download` | Get download URL for specific version |
| `POST` | `/api/v1/files/{id}/versions/{versionNumber}/restore` | Restore file to specific version |

### File Sharing

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/storage/files/{id}/shares` | Create share link with options |
| `GET` | `/api/v1/storage/files/{id}/shares` | Get all shares for a file |
| `DELETE` | `/api/v1/storage/shares/{id}` | Revoke share link |
| `GET` | `/api/v1/storage/shares/public/{token}` | Access shared file (public, no auth) |

**Share Options:**
- `permission`: View, Download, Edit, Admin
- `password`: Optional password protection
- `expiresAt`: Expiration date/time
- `maxDownloads`: Maximum download count

### Admin API (Requires Role: Admin)

> **Note:** These endpoints require `Admin` or `SuperAdmin` role.

#### Quota Management

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/quotas` | Get all users' quotas |
| `GET` | `/api/v1/admin/quotas/{userId}` | Get quota for specific user |
| `PUT` | `/api/v1/admin/quotas/{userId}` | Update quota limits |

#### Files Management

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/files` | Get all files with filtering |
| `DELETE` | `/api/v1/admin/files/{id}` | Delete file for policy violation |

#### Shares Management

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/shares` | Get all shares |
| `DELETE` | `/api/v1/admin/shares/{id}` | Revoke share for violation |

#### Statistics

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/statistics` | Dashboard aggregated statistics |
| `GET` | `/api/v1/admin/statistics/users-near-limit` | Users near quota limit (>80%) |

## Direct Upload Example (Recommended)

### Step 1: Get 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"
  }
}
```

### Step 2: Upload Directly to MinIO

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

### Step 3: Confirm 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

The Storage Service generates different types of download URLs based on the file's `accessLevel`.

### Access Level Overview

| Access Level | Storage Path Prefix | Download URL Type | Use Case |
|--------------|---------------------|-------------------|----------|
| **Public** | `public/` | Direct URL (no signature) | Public assets, images, CDN |
| **Private** | `private/` | Pre-signed URL (AWS Signature V4) | User files, documents |
| **Shared** | `shared/` | Pre-signed URL (AWS Signature V4) | Shared files with link |

### Public Files

Public files can be accessed directly without authentication:

```
http://minio:9000/storage/public/2026/01/13/abc123.png
```

**Characteristics:**
- No signature required
- URL never expires
- Anyone with the URL can access
- Best for: avatars, thumbnails, public downloads

### Private/Shared Files (Pre-signed URLs)

Private and shared files require a **pre-signed URL** with 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...
```

**Signature Parameters Explained:**

| Parameter | Description | Example |
|-----------|-------------|---------|
| `X-Amz-Algorithm` | Signing algorithm | `AWS4-HMAC-SHA256` |
| `X-Amz-Credential` | Access key + date/region/service | `minioadmin/20260113/us-east-1/s3/aws4_request` |
| `X-Amz-Date` | Signature creation timestamp | `20260113T180024Z` |
| `X-Amz-Expires` | URL validity in seconds | `3600` (1 hour) |
| `X-Amz-SignedHeaders` | Headers included in signature | `host` |
| `X-Amz-Signature` | The cryptographic signature | `2ce827d3...` |

**Security Benefits:**
- URL expires after configured time (default: 1 hour)
- Signature prevents URL tampering
- No MinIO credentials exposed to client
- Each URL is unique and single-use in practice

### Get Download URL API

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

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

**Response for Private file:**
```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"
  }
}
```

### Configuration

| Variable | Description | Default |
|----------|-------------|---------|
| `Storage__PreSignedUrlExpirationSeconds` | Pre-signed URL validity period | `3600` (1 hour) |

> **Note:** For very large files or slow connections, consider increasing the expiration time.

## Logical Folder Architecture (Data Sovereignty)

> ⚠️ **IMPORTANT**: Following the **Data Sovereignty** principle in microservices, folders are a **logical concept in the Database**, NOT dependent on bucket structure.

### Design Principles

Storage Service owns its own data model:
- **Database** manages logical folder structure (hierarchy, paths, permissions)
- **Bucket** only stores file binaries with UUID keys (flat structure)

### ❌ Anti-pattern (DON'T DO THIS)

```
Bucket structure based on user paths:
goodgo/
├── users/john/documents/report.pdf     ← BAD
├── users/mary/images/photo.jpg         ← BAD
└── users/bob/work/presentation.pptx    ← BAD

PROBLEMS:
- Renaming folder = Moving millions of files (slow + risky)
- Predictable paths = Vulnerable to attacks
- Doesn't scale with millions of users
```

### ✅ Correct Architecture (Logical Separation)

#### 1️⃣ Database: Logical Structure

```sql
-- Folders table: Manages folder tree
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,    -- Example: /docs/work/2024
    created_at TIMESTAMP
);

-- Files table: Links to logical folder
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 are stored with **UUID-based keys** following pattern:

```
{prefix}/{year}/{month}/{day}/{uuid}.{ext}

Example:
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

NO user/folder structure in bucket!
All are flat UUID keys.
```

#### 3️⃣ Storage Key Generation

```csharp
// Automatically generate physical key (NOT based on 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}";
}
```

### Benefits

| Operation | Database (Logic) | Bucket (Physical) | Performance |
|-----------|------------------|-------------------|-------------|
| **Create folder** | INSERT 1 row | Nothing | Instant |
| **Rename folder** | UPDATE 1 row | Nothing | O(1) - Instant |
| **Move file** | UPDATE File.folder_id | Nothing | O(1) - Instant |
| **Delete folder** | Cascade delete | Queue cleanup | Background job |

**Compared to Anti-pattern:**
- Rename folder: **O(1)** vs O(n) - 1000x faster
- Security: UUID keys are unpredictable
- Migration: Easy to switch storage provider

### Workflow Examples

**Upload file to folder:**
```bash
# 1. Create folder (Database only)
POST /api/v1/folders
{
  "name": "documents",
  "parentId": null
}

# 2. Upload file to folder
POST /api/v1/storage/sign-upload
{
  "fileName": "report.pdf",
  "folderId": "folder-uuid-123",    # Logical folder
  "fileSizeBytes": 1048576
}

# Response: Physical key DOES NOT contain folder name
{
  "uploadUrl": "...",
  "objectKey": "private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf"
}
```

**List files in folder:**
```bash
GET /api/v1/folders/{folderId}/files

# Database query: SELECT * FROM files WHERE folder_id = {folderId}
# Client sees: /documents/report.pdf (logical path)
# Bucket stores: private/2026/01/13/{uuid}.pdf (physical key)
```

> 📚 **Technical Details:** See [ARCHITECTURE.md](./ARCHITECTURE.md) for deeper understanding of this pattern.

## Legacy Upload Example (Via Backend)

```bash
curl -X POST "http://localhost:5002/api/v1/files/upload" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@document.pdf"
```


## Inter-Service Communication

The service communicates with IAM Service for user validation. This section explains the configuration required for proper inter-service communication in Docker environments.

### Docker Network Configuration

When running in Docker Compose, services communicate via Docker network using container names:

```yaml
# docker-compose.yml
storage-service:
  environment:
    # IMPORTANT: Use container name for inter-service communication
    - IamService__BaseUrl=http://iam-service-net:8080
    - IamService__ServiceName=storage-service
```

> **Note**: The URL `http://iam-service-net:8080` uses:
> - `iam-service-net` = Docker container name (not `localhost`)
> - `8080` = Internal container port (not exposed port `5001`)

### JWT Token Issuer Configuration

To ensure JWT tokens work across services, IAM Service must use a fixed issuer URI:

```yaml
# IAM Service docker-compose.yml
iam-service-net:
  environment:
    - IdentityServer__IssuerUri=http://iam-service
```

This ensures tokens issued by IAM Service have a consistent `iss` claim regardless of how the client accesses it.

### Communication Flow

```
┌─────────────────┐     JWT Token (iss: http://iam-service)     ┌─────────────────┐
│  Storage        │ ──────────────────────────────────────────► │  IAM Service    │
│  Service        │ ◄────────────────────────────────────────── │                 │
│  :8080          │        User info + Roles/Permissions        │  :8080          │
└─────────────────┘                                             └─────────────────┘
        │                      Docker Network: goodgo-network          │
        └──────────────────────────────────────────────────────────────┘
```

### Headers & Caching

- **Headers**: `Authorization: Bearer <token>`, `X-Service-Name: storage-service`
- **User Info Cache**: 5 minutes (300 seconds)
- **Health Check Cache**: 1 minute (60 seconds)
- **Resilience**: Polly retry (3x) + circuit breaker

### Available Methods (IIamServiceClient)

| Method | Description |
|--------|-------------|
| `ValidateUserAsync` | Validate JWT token and get user info |
| `GetUserByIdAsync` | Get user by ID |
| `GetUserRolesAsync` | Get user's assigned roles |
| `GetUserPermissionsAsync` | Get user's permissions |
| `HasPermissionAsync` | Check if user has specific permission |
| `HasRoleAsync` | Check if user has specific role |
| `CheckHealthAsync` | Check IAM Service health status |

### Troubleshooting

| Issue | Cause | Solution |
|-------|-------|----------|
| `401 Unauthorized` | JWT issuer mismatch | Set `IdentityServer__IssuerUri` in IAM Service |
| `Connection refused` | Wrong URL or container not running | Use container name, not `localhost` |
| `Name resolution failed` | Container not in same network | Ensure both services use `goodgo-network` |
| `Timeout` | IAM Service slow/unhealthy | Check IAM health: `curl http://iam-service-net:8080/health` |

## Database Migrations

```bash
# Create new migration
dotnet ef migrations add MigrationName \
  --project src/StorageService.Infrastructure \
  --startup-project src/StorageService.API

# Apply migrations
dotnet ef database update \
  --project src/StorageService.Infrastructure \
  --startup-project src/StorageService.API
```

## Testing

```bash
# Run all tests
dotnet test

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
```

## Project Structure

```
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/                           # English documentation
│   └── vi/                           # Vietnamese documentation
├── docker-compose.yml
├── Dockerfile
└── README.md
```

## License

MIT
