26 KiB
Storage Service Architecture
Detailed architecture documentation for the Storage Service microservice.
Architecture Overview
graph TB
subgraph "API Layer"
C[Controllers]
CMD[Commands]
Q[Queries]
B[Behaviors]
end
subgraph "Domain Layer"
SF[StorageFile]
SQ[UserStorageQuota]
DE[Domain Events]
RI[Repository Interfaces]
end
subgraph "Infrastructure Layer"
SP[Storage Providers]
IAM[IAM Client]
R[Repositories]
CTX[DbContext]
end
subgraph "External Services"
MINIO[(MinIO)]
OSS[(Aliyun OSS)]
IAMS[IAM Service]
DB[(PostgreSQL)]
end
C --> CMD
C --> Q
CMD --> B --> SF
Q --> R
R --> CTX --> DB
SF --> DE
CMD --> SP
SP --> MINIO
SP --> OSS
C --> IAM --> IAMS
style C fill:#4a90d9,stroke:#2d5986,color:#fff
style SF fill:#50c878,stroke:#2d8659,color:#fff
style MINIO fill:#c73b3b,stroke:#922b2b,color:#fff
style OSS fill:#ff6b35,stroke:#cc5500,color:#fff
style IAMS fill:#9b59b6,stroke:#7d3c98,color:#fff
Layer Responsibilities
1. Domain Layer (StorageService.Domain)
The heart of the application containing pure business logic.
| Component | Purpose |
|---|---|
| StorageFile | Aggregate root for file metadata and lifecycle |
| UserStorageQuota | Aggregate root for user storage limits and usage |
| StorageProvider | Enum: MinIO, AliyunOSS |
| FileAccessLevel | Enum: Private, Public, Shared |
| Domain Events | FileUploadedDomainEvent, FileDeletedDomainEvent, UserQuotaUpdatedDomainEvent |
2. Infrastructure Layer (StorageService.Infrastructure)
Technical implementations and external integrations:
| Component | Purpose |
|---|---|
| MinioStorageProvider | MinIO S3-compatible storage operations |
| AliyunOssStorageProvider | Alibaba Cloud OSS operations |
| StorageProviderFactory | Runtime provider selection based on config |
| HttpIamServiceClient | Inter-service communication with IAM |
| FileRepository | EF Core repository for StorageFile |
| QuotaRepository | EF Core repository for UserStorageQuota |
3. API Layer (StorageService.API)
Application entry point and CQRS implementation:
| Component | Purpose |
|---|---|
| FilesController | File CRUD endpoints (legacy proxy upload) |
| SignedUrlController | Direct upload endpoints (recommended) |
| QuotaController | User quota endpoints |
| SignUploadCommand | Generate pre-signed upload URLs |
| ConfirmUploadCommand | Confirm direct uploads and save metadata |
| UploadFileCommand | Handle proxy file uploads (legacy) |
| DeleteFileCommand | Handle file deletions |
| Query Handlers | Handle read operations |
4. Admin Backoffice Layer
Controllers and Commands for Admin storage management:
| Component | Purpose |
|---|---|
| AdminQuotaController | Manage user quotas (GET all, PUT update) |
| AdminFilesController | View/delete all users' files |
| AdminSharesController | View/revoke violating file shares |
| AdminStatisticsController | Storage statistics dashboard |
| UpdateUserQuotaCommand | Admin update quota limits |
| AdminDeleteFileCommand | Admin delete file (bypass ownership) |
| AdminRevokeShareCommand | Admin revoke violating share |
| Admin Query Handlers | Queries with pagination and filtering |
Authorization: All Admin endpoints require
AdminorSuperAdminrole.
Direct Upload Architecture (Recommended)
For systems with millions of users, Direct Client Upload pattern is recommended over proxy upload.
Upload Patterns Comparison
| Aspect | Proxy Upload (Legacy) | Direct Upload (Recommended) |
|---|---|---|
| Throughput | ~100-500/sec | ~10,000+/sec |
| Memory per request | 100MB (file size) | ~10KB (metadata only) |
| Latency (100MB file) | 30-60s | 10-20s |
| Backend load | High | Minimal |
Direct Upload Flow
sequenceDiagram
participant Client
participant Storage_Service as Storage Service
participant MinIO
rect rgb(200, 230, 200)
Note over Client,Storage_Service: 1. Request Upload URL (lightweight)
Client->>Storage_Service: POST /api/v1/storage/sign-upload
Storage_Service->>Storage_Service: Validate JWT, Check Quota
Storage_Service-->>Client: Pre-signed PUT URL + ObjectKey
end
rect rgb(200, 200, 230)
Note over Client,MinIO: 2. Direct Upload (bypasses backend)
Client->>MinIO: PUT file binary to Pre-signed URL
MinIO-->>Client: 200 OK
end
rect rgb(230, 230, 200)
Note over Client,Storage_Service: 3. Confirm Upload (lightweight)
Client->>Storage_Service: POST /api/v1/storage/confirm-upload
Storage_Service->>MinIO: Verify file exists
Storage_Service->>Storage_Service: Save metadata, Update quota
Storage_Service-->>Client: File metadata
end
Logical Folder Architecture (Data Sovereignty)
⚠️ IMPORTANT: Following the Data Sovereignty principle in microservices, Storage Service must fully own its data model.
❌ Anti-pattern: Relying on Bucket Structure
BAD APPROACH - Folder structure reflected in bucket:
storage-bucket/
├── users/john/documents/report.pdf
├── users/john/images/photo.jpg
└── users/mary/work/presentation.pptx
PROBLEMS:
- Renaming folder "documents" → "docs" = Moving millions of files (O(n))
- Moving file = Copy + Delete on bucket (slow, risky)
- Predictable paths → Vulnerable to path traversal attacks
- Doesn't scale with millions of users
- Difficult to migrate to another storage provider
✅ Correct Approach: Logical Separation
Principles:
- Database = Logical structure (folders, hierarchy, permissions)
- Bucket = Physical storage (flat UUID keys)
graph TB
subgraph "Logical Layer - PostgreSQL Database"
F[Folders Table]
FL[Files Table]
F -->|parent_id| F
FL -->|folder_id| F
end
subgraph "Physical Layer - MinIO Bucket"
B[Flat UUID Structure]
B1[private/2026/01/13/uuid1.pdf]
B2[private/2026/01/13/uuid2.jpg]
B3[public/2026/01/14/uuid3.png]
end
FL -.->|storage_key| B
style F fill:#3498db,color:#fff
style FL fill:#2ecc71,color:#fff
style B fill:#e74c3c,color:#fff
Database Schema
-- Folders: Hierarchical tree structure
CREATE TABLE folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
path VARCHAR(1000) NOT NULL, -- Materialized path: /docs/work/2024
level INT NOT NULL DEFAULT 0, -- Tree depth
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (user_id, parent_id, name),
INDEX idx_user_path (user_id, path)
);
-- Files: Link to logical folders
CREATE TABLE storage_files (
id UUID PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
file_name VARCHAR(255) NOT NULL,
storage_key VARCHAR(500) UNIQUE, -- Physical UUID key in bucket
content_type VARCHAR(100),
file_size_bytes BIGINT,
access_level VARCHAR(20), -- Private, Public, Shared
bucket_name VARCHAR(255),
provider INT,
uploaded_at TIMESTAMP,
is_deleted BOOLEAN DEFAULT false,
INDEX idx_folder_id (folder_id),
INDEX idx_storage_key (storage_key)
);
Physical Key Generation Strategy
public class StorageKeyGenerator
{
/// <summary>
/// Generate UUID-based key (NOT based on folder path)
/// Pattern: {prefix}/{year}/{month}/{day}/{uuid}.{ext}
/// </summary>
public string GenerateKey(FileAccessLevel access, string fileName)
{
var now = DateTime.UtcNow;
var prefix = access == FileAccessLevel.Public ? "public" : "private";
var uuid = Guid.NewGuid().ToString("N"); // 32 chars, no hyphens
var ext = Path.GetExtension(fileName);
// Example: private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf
return $"{prefix}/{now:yyyy}/{now:MM}/{now:dd}/{uuid}{ext}";
}
}
Bucket Structure (Flat Physical)
goodgo/
├── private/
│ ├── 2026/
│ │ └── 01/
│ │ └── 13/
│ │ ├── d290f1ee6c544b0190e6d701748f0851.pdf
│ │ ├── a7f3b2c19d8e4f01bcd5e92f7a4d8b63.jpg
│ │ └── f9e4c1a2b7d36e5f8a0c4d9e2b1f7a8c.docx
├── public/
│ └── 2026/
│ └── 01/
│ └── 14/
│ └── c3d8f2e1a9b47c6e5d0f8a3b2e1c7d9f.png
└── shared/
└── 2026/
└── 01/
└── 15/
└── e1f2c3d4a5b6c7e8d9f0a1b2c3d4e5f6.xlsx
NO user/documents/... folders in bucket!
Workflows
1. Create Folder (Database Only)
sequenceDiagram
Client->>API: POST /api/v1/folders {name: "documents"}
API->>Database: INSERT INTO folders (name, path)
Database-->>API: folder_id
API-->>Client: {id, name, path: "/documents"}
Note over Client,API: Bucket is NOT touched
2. Upload File to Folder
sequenceDiagram
Client->>API: POST /sign-upload {fileName, folderId}
API->>Database: Validate folder ownership
API->>StorageKeyGen: Generate UUID key
StorageKeyGen-->>API: private/2026/01/13/{uuid}.pdf
API->>MinIO: Get pre-signed PUT URL
MinIO-->>API: pre-signed URL
API->>Database: INSERT file (folder_id, storage_key)
API-->>Client: {uploadUrl, objectKey}
Client->>MinIO: PUT file binary
Client->>API: POST /confirm-upload
3. List Files in Folder
-- Database query (fast, indexed)
SELECT * FROM storage_files
WHERE folder_id = '{folder-uuid}'
AND is_deleted = false
ORDER BY uploaded_at DESC;
-- Result returned to client:
{
"folder": {
"id": "folder-123",
"path": "/documents/work"
},
"files": [
{
"id": "file-456",
"name": "report.pdf",
"logicalPath": "/documents/work/report.pdf", -- Displayed to user
"storageKey": "private/2026/01/13/{uuid}.pdf" -- Physical key
}
]
}
4. Rename Folder
-- Only UPDATE database (instant, O(1))
UPDATE folders
SET name = 'docs',
path = '/docs' -- Update materialized path
WHERE id = '{folder-id}';
-- Update descendant paths
UPDATE folders
SET path = REPLACE(path, '/documents', '/docs')
WHERE path LIKE '/documents/%';
-- Files KEEP their storage_key - DON'T TOUCH bucket!
5. Move File Between Folders
-- Only UPDATE database (instant, O(1))
UPDATE storage_files
SET folder_id = '{new-folder-id}'
WHERE id = '{file-id}';
-- Physical file STAYS at old storage_key
-- No Copy/Delete needed in bucket
Performance Comparison
| Operation | Anti-pattern (Bucket-based) | Correct (Logical) | Improvement |
|---|---|---|---|
| Create folder | N/A (virtual) | INSERT to DB | Instant |
| Rename folder | Copy + Delete millions of files | UPDATE 1-N rows in DB | ~1000x faster |
| Move file | Copy + Delete 1 file | UPDATE 1 row in DB | ~100x faster |
| List files | Bucket prefix scan | Indexed DB query | ~50x faster |
| Delete folder | Delete millions of files | Cascade delete + background cleanup | Async |
Data Sovereignty Benefits
-
Performance:
- Rename folder: O(1) instead of O(n)
- Move file: O(1) instead of O(1) bucket copy
- List files: Indexed query instead of bucket scan
-
Security:
- UUID keys are unpredictable
- No path traversal vulnerabilities
- Access control in database
-
Scalability:
- Database handles metadata (fast)
- Bucket handles blobs (simple)
- Easy horizontal scaling
-
Flexibility:
- Easy to add features (sharing, versioning)
- Migration between storage providers
- Support multiple buckets/regions
-
Developer Experience:
- Client sees beautiful folder tree
- Backend works with simple UUIDs
- Clean separation of concerns
Direct Upload Components
| Component | Purpose |
|---|---|
| SignUploadCommand | Validate quota, generate object key with path prefix, create pre-signed PUT URL |
| SignUploadCommandHandler | Handle sign-upload requests |
| ConfirmUploadCommand | Verify file exists, save metadata, update quota |
| ConfirmUploadCommandHandler | Handle confirm-upload with idempotency |
| SignedUrlController | /sign-upload and /confirm-upload endpoints |
Download URL Architecture
The Storage Service generates different download URLs based on file access levels.
URL Generation Flow
sequenceDiagram
participant Client
participant API as Storage Service
participant MinIO
Client->>API: GET /api/v1/files/{id}/download-url
API->>API: Validate ownership
alt accessLevel = Public
API-->>Client: Direct URL (no signature)
else accessLevel = Private/Shared
API->>MinIO: GetPreSignedDownloadUrl()
MinIO-->>API: Pre-signed URL (AWS Signature V4)
API-->>Client: Pre-signed URL with expiration
end
Access Level URL Types
| Access Level | Storage Prefix | URL Type | Expiration |
|---|---|---|---|
| Public | public/ |
Direct URL | Never |
| Private | private/ |
Pre-signed URL | Configurable (default: 1 hour) |
| Shared | shared/ |
Pre-signed URL | Configurable (default: 1 hour) |
Pre-signed URL Structure
For private/shared files, URLs include AWS Signature Version 4 parameters:
http://minio:9000/bucket/private/2026/01/13/{uuid}.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential={accessKey}%2F{date}%2F{region}%2Fs3%2Faws4_request
&X-Amz-Date={timestamp}
&X-Amz-Expires={seconds}
&X-Amz-SignedHeaders=host
&X-Amz-Signature={signature}
| Parameter | Purpose |
|---|---|
X-Amz-Algorithm |
Signing algorithm (always AWS4-HMAC-SHA256) |
X-Amz-Credential |
Access key + scope (date/region/service) |
X-Amz-Date |
Timestamp when signature was created |
X-Amz-Expires |
URL validity in seconds |
X-Amz-SignedHeaders |
Headers included in signature calculation |
X-Amz-Signature |
Cryptographic signature to verify URL integrity |
Security Considerations
- Time-Limited Access: Pre-signed URLs expire after configured time
- Tamper-Proof: Any URL modification invalidates the signature
- Credential Protection: MinIO access keys never exposed to client
- Unique URLs: Each request generates a new signature
GetDownloadUrl Implementation
public async Task<string> GetDownloadUrlAsync(StorageFile file)
{
if (file.AccessLevel == FileAccessLevel.Public)
{
// Direct URL for public files
return $"{_settings.PublicEndpoint}/{file.BucketName}/{file.ObjectKey}";
}
// Pre-signed URL for private/shared files
return await _storageProvider.GetPreSignedDownloadUrlAsync(
file.BucketName,
file.ObjectKey,
_settings.PreSignedUrlExpirationSeconds
);
}
Multipart Upload Architecture (Large Files)
For files larger than 100MB, use Multipart Upload to upload in chunks.
Upload Methods Comparison
| Aspect | Direct Upload | Multipart Upload |
|---|---|---|
| File size | < 100MB | > 100MB (up to 5GB+) |
| Mechanism | Single PUT request | Multiple part uploads |
| Resume support | No | Yes (per part) |
| Progress tracking | No | Yes (per part) |
| Use case | Small/medium files | Large files, video, archives |
Multipart Upload Flow
sequenceDiagram
participant Client
participant API as Storage Service
participant DB as PostgreSQL
participant MinIO
rect rgb(200, 230, 200)
Note over Client,API: 1. Initiate Upload
Client->>API: POST /api/v1/files/multipart/initiate
API->>DB: Create MultipartUpload record
API->>MinIO: InitiateMultipartUpload
MinIO-->>API: Provider UploadId
API-->>Client: {uploadId, objectKey, totalChunks}
end
rect rgb(200, 200, 230)
Note over Client,MinIO: 2. Upload Parts (repeat for each chunk)
loop For each part 1..N
Client->>API: POST /api/v1/files/multipart/upload-part
API->>MinIO: UploadPart(partNumber, data)
MinIO-->>API: ETag
API->>DB: Save part info (partNumber, etag)
API-->>Client: {success, etag}
end
end
rect rgb(230, 200, 200)
Note over Client,API: 3. Optional: Check Progress
Client->>API: GET /api/v1/files/multipart/{uploadId}
API->>DB: Get upload + parts
API-->>Client: {progress: 75%, uploadedChunks: 3/4}
end
rect rgb(230, 230, 200)
Note over Client,API: 4. Complete Upload
Client->>API: POST /api/v1/files/multipart/complete
API->>MinIO: CompleteMultipartUpload(parts[])
MinIO-->>API: OK
API->>DB: Create StorageFile, Update quota
API-->>Client: {fileId, objectKey}
end
Multipart Upload Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/files/multipart/initiate |
Start upload session |
POST |
/api/v1/files/multipart/upload-part |
Upload 1 chunk |
POST |
/api/v1/files/multipart/complete |
Complete and merge parts |
DELETE |
/api/v1/files/multipart/abort |
Cancel upload, cleanup |
GET |
/api/v1/files/multipart/{uploadId} |
Check progress |
Multipart Upload Components
| Component | Purpose |
|---|---|
| InitiateMultipartUploadCommand | Create upload session, generate object key |
| UploadPartCommand | Upload 1 part to storage provider |
| CompleteMultipartUploadCommand | Merge parts, create StorageFile record |
| AbortMultipartUploadCommand | Cleanup parts, mark as aborted |
| GetMultipartUploadProgressQuery | Get upload progress |
| MultipartUploadController | API endpoints for multipart |
Storage Provider Architecture
graph TD
subgraph "Storage Provider Factory"
F[StorageProviderFactory]
C[Configuration]
end
subgraph "Providers"
MP[MinioStorageProvider]
AP[AliyunOssStorageProvider]
end
subgraph "Storage Backends"
MINIO[(MinIO)]
OSS[(Aliyun OSS)]
end
C --> |STORAGE_PROVIDER=minio| F
C --> |STORAGE_PROVIDER=aliyun| F
F --> |GetProvider| MP
F --> |GetProvider| AP
MP --> MINIO
AP --> OSS
style F fill:#4a90d9,stroke:#2d5986,color:#fff
style MP fill:#c73b3b,stroke:#922b2b,color:#fff
style AP fill:#ff6b35,stroke:#cc5500,color:#fff
Storage Provider Interface
public interface IStorageProvider
{
Task<UploadResult> UploadAsync(Stream stream, string objectKey, ...);
Task<Stream> DownloadAsync(string bucketName, string objectKey);
Task DeleteAsync(string bucketName, string objectKey);
Task<bool> ExistsAsync(string bucketName, string objectKey);
Task<string> GetPreSignedDownloadUrlAsync(string bucketName, string objectKey, int expirationSeconds);
Task<string> GetPreSignedUploadUrlAsync(string bucketName, string objectKey, int expirationSeconds);
}
Inter-Service Communication
sequenceDiagram
participant Client
participant Storage as Storage Service
participant Cache as In-Memory Cache
participant IAM as IAM Service
Client->>Storage: Upload File (JWT)
Storage->>Cache: Check user cache
alt Cache Hit
Cache-->>Storage: User info
else Cache Miss
Storage->>IAM: GET /api/v1/users/me
Note over Storage,IAM: Headers: Authorization, X-Service-Name
IAM-->>Storage: User info
Storage->>Cache: Store (5 min TTL)
end
Storage->>Storage: Validate quota
Storage->>Storage: Upload to provider
Storage-->>Client: Upload result
IAM Client Features
| Feature | Description |
|---|---|
| Caching | In-memory cache for user info (5 min TTL) |
| Health Check | Check IAM availability with caching (1 min TTL) |
| Polly Resilience | Retry (3x exponential) + Circuit Breaker |
| Permission Check | HasPermissionAsync, HasRoleAsync |
Available IAM Client Methods
// User Operations
Task<IamUserInfo?> ValidateUserAsync(string accessToken);
Task<IamUserInfo?> GetUserByIdAsync(string userId, string accessToken);
Task<bool> UserExistsAsync(string userId, string accessToken);
Task<IReadOnlyList<string>> GetUserRolesAsync(string userId, string accessToken);
Task<IReadOnlyList<string>> GetUserPermissionsAsync(string userId, string accessToken);
Task<bool> HasPermissionAsync(string userId, string permission, string accessToken);
Task<bool> HasRoleAsync(string userId, string role, string accessToken);
// Health Check
Task<IamHealthStatus> CheckHealthAsync();
Task<bool> IsAvailableAsync();
// Cache Management
void InvalidateUserCache(string userId);
void ClearCache();
Database Schema
erDiagram
storage_files {
uuid id PK
varchar file_name
varchar bucket_name
varchar object_key
varchar content_type
bigint file_size_bytes
varchar user_id
varchar tenant_id
int provider
int access_level
timestamp uploaded_at
timestamp expires_at
varchar checksum
boolean is_deleted
timestamp deleted_at
}
user_storage_quotas {
uuid id PK
varchar user_id UK
bigint max_storage_bytes
bigint used_storage_bytes
int max_file_count
int current_file_count
varchar quota_tier
timestamp last_updated_at
timestamp created_at
}
API Endpoints
Files (Legacy - Proxy Upload)
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/files/upload |
Upload file via backend (max 100MB) |
GET |
/api/v1/files |
List user files with pagination |
GET |
/api/v1/files/{id} |
Get file metadata |
GET |
/api/v1/files/{id}/download-url |
Get pre-signed download URL |
DELETE |
/api/v1/files/{id} |
Delete file (soft delete) |
Direct Upload (Recommended)
| 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 user storage quota |
Admin API (Requires Role: Admin)
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%) |
Health Checks
graph TD
HC[Health Check Endpoint]
HC --> |/health/live| L[Liveness]
HC --> |/health/ready| R[Readiness]
R --> DB[(PostgreSQL)]
R --> MINIO[(MinIO)]
R --> IAM[IAM Service]
style HC fill:#3498db,stroke:#2980b9,color:#fff
style L fill:#2ecc71,stroke:#27ae60,color:#fff
style R fill:#f39c12,stroke:#d68910,color:#fff
Deployment Architecture
Docker Compose (Local)
services:
storage-api:
build: .
ports: ["5002:8080"]
depends_on:
- postgres
- redis
- minio
environment:
- Storage__Provider=minio
- Storage__MinIO__Endpoint=minio:9000
minio:
image: minio/minio:latest
ports: ["9000:9000", "9001:9001"]
Traefik Integration
labels:
- "traefik.enable=true"
- "traefik.http.routers.storage-service.rule=PathPrefix(`/api/v1/files`) || PathPrefix(`/api/v1/quota`)"
- "traefik.http.services.storage-service.loadbalancer.server.port=8080"
Security Considerations
- Authentication: JWT Bearer validation via IAM Service
- Authorization: User ownership check on files
- Input Validation: File size limits, content type validation
- Pre-signed URLs: Time-limited access to files
- Soft Delete: Files are marked deleted, not immediately removed
Performance Optimization
- Caching: In-memory cache for IAM user info
- Pre-signed URLs: Direct client-to-storage downloads
- Streaming Upload: Stream-based file handling
- Async Operations: All I/O operations are async
- Connection Pooling: HTTP client with Polly policies