Files
pos-system/services/storage-service-net/docs/en
Ho Ngoc Hai 5c8764f63a docs(architecture): Update documentation for direct upload architecture and API endpoints
- Enhanced the architecture documentation to recommend direct upload over legacy proxy upload for improved performance and scalability.
- Added detailed comparisons of upload patterns, including throughput, memory usage, and latency.
- Updated API endpoint documentation to reflect new direct upload methods and their benefits.
- Included examples for direct upload flow and bucket directory structure to aid developers in implementation.
2026-01-13 21:17:55 +07:00
..

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

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

Access: http://localhost:5002/swagger

Run Locally

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)

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

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

Step 2: Upload Directly to MinIO

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

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

Bucket Directory Structure

Files are organized by access level and user ID with the following format:

{bucket}/
├── private/{userId}/{date}/{fileId}_{filename}  → Owner access only (via pre-signed URL)
├── public/{userId}/{date}/{fileId}_{filename}   → Publicly accessible
└── shared/{userId}/{date}/{fileId}_{filename}   → Controlled by sharing rules

Object Key Format Details

Component Description Example
{bucket} Bucket name (from config) goodgo
{accessLevel} Access level prefix private, public, shared
{userId} Uploader's user ID user123
{date} Upload date (UTC) 20260113
{fileId} First 8 chars of GUID a1b2c3d4
{filename} Sanitized file name document.pdf

Real-World Example

goodgo/
├── private/
│   └── user123/
│       └── 20260113/
│           ├── a1b2c3d4_document.pdf
│           └── e5f6g7h8_image.jpg
├── public/
│   └── user456/
│       └── 20260113/
│           └── i9j0k1l2_avatar.png
└── shared/
    └── user789/
        └── 20260113/
            └── m3n4o5p6_presentation.pptx

Note

: The object key is returned in the /sign-upload response and must be sent back when calling /confirm-upload.

Legacy Upload Example (Via Backend)

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:

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

# 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

# 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

# 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