Files
pos-system/services/storage-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

26 KiB

StorageService - Service Documentation

Auto-generated from source code audit on 2026-03-13

Overview

StorageService is a file storage microservice providing S3-compatible object storage with support for direct uploads, multipart uploads, file sharing, file versioning, logical folders, and per-user storage quotas. It supports two storage backends: MinIO (primary) and Aliyun OSS.

  • Port: 5002 (Development)
  • Database: storage_service (PostgreSQL / Neon)
  • Storage Backend: MinIO (S3-compatible) at 167.114.174.113:9000
  • Cache: Redis at 167.114.174.113:6379
  • Default Bucket: goodgo
  • Max File Size: 100MB (configurable), validator limit 500MB
  • Auth: JWT Bearer via IAM Service
  • Framework: .NET 10.0, Clean Architecture + CQRS

API Endpoints

Files (/api/v1/files)

Method Route Auth Description
POST /api/v1/files/upload Bearer Upload a file (max 100MB)
GET /api/v1/files Bearer List user files (skip, take, search)
GET /api/v1/files/{fileId} Bearer Get file metadata by ID
GET /api/v1/files/{fileId}/download-url Bearer Get pre-signed download URL
GET /api/v1/files/{fileId}/cdn-url Bearer Get CDN URL (public) or fallback pre-signed URL
DELETE /api/v1/files/{fileId} Bearer Soft delete a file

Direct Upload - Pre-Signed URL (/api/v1/storage)

Method Route Auth Description
POST /api/v1/storage/sign-upload Bearer Get pre-signed PUT URL for direct upload to MinIO
POST /api/v1/storage/confirm-upload Bearer Confirm upload and save metadata after direct upload

Multipart Upload (/api/v1/files/multipart)

Method Route Auth Description
POST /api/v1/files/multipart/initiate Bearer Initiate multipart upload session
POST /api/v1/files/multipart/upload-part Bearer Upload a single part (max 20MB per part)
POST /api/v1/files/multipart/complete Bearer Complete and merge all parts
DELETE /api/v1/files/multipart/abort Bearer Abort and cleanup
GET /api/v1/files/multipart/{uploadId} Bearer Get upload progress

File Sharing (/api/v1/storage)

Method Route Auth Description
POST /api/v1/storage/files/{fileId}/shares Bearer Create share link (with optional password, expiry, max downloads)
GET /api/v1/storage/files/{fileId}/shares Bearer Get all shares for a file
DELETE /api/v1/storage/shares/{shareId} Bearer Revoke a share link
GET /api/v1/storage/shares/public/{token} Anonymous Access shared file by token (public endpoint)

File Versioning (/api/v1/storage/files/{fileId}/versions)

Method Route Auth Description
GET /api/v1/storage/files/{fileId}/versions Bearer Get all versions of a file
GET /api/v1/storage/files/{fileId}/versions/{versionId}/download Bearer Get download URL for specific version
POST /api/v1/storage/files/{fileId}/versions/{versionId}/restore Bearer Restore file to a specific version

Folders (/api/v1/storage/folders)

Method Route Auth Description
POST /api/v1/storage/folders Bearer Create folder (root or child)
GET /api/v1/storage/folders Bearer List folders (root or children of parentId)
GET /api/v1/storage/folders/{folderId} Bearer Get folder by ID
PUT /api/v1/storage/folders/{folderId} Bearer Rename folder (O(1) operation)
DELETE /api/v1/storage/folders/{folderId} Bearer Soft delete folder

Quota (/api/v1/quota)

Method Route Auth Description
GET /api/v1/quota Bearer Get current user's storage quota

Admin - Files (/api/v1/admin/files)

Method Route Auth Description
GET /api/v1/admin/files Admin/SuperAdmin Get all files with filtering and pagination
DELETE /api/v1/admin/files/{fileId} Admin/SuperAdmin Delete file (bypasses ownership, requires reason)

Admin - Quotas (/api/v1/admin/quotas)

Method Route Auth Description
GET /api/v1/admin/quotas Admin/SuperAdmin Get all users' quotas with filtering
GET /api/v1/admin/quotas/{userId} Admin/SuperAdmin Get quota for specific user
PUT /api/v1/admin/quotas/{userId} Admin/SuperAdmin Update user quota limits

Admin - Shares (/api/v1/admin/shares)

Method Route Auth Description
GET /api/v1/admin/shares Admin/SuperAdmin Get all file shares with filtering
DELETE /api/v1/admin/shares/{shareId} Admin/SuperAdmin Revoke share (requires reason)

Admin - Statistics (/api/v1/admin/statistics)

Method Route Auth Description
GET /api/v1/admin/statistics Admin/SuperAdmin Get aggregated storage statistics
GET /api/v1/admin/statistics/users-near-limit Admin/SuperAdmin Get users with usage >= 80%

Health Checks

Method Route Auth Description
GET /health None Full health check (incl. PostgreSQL)
GET /health/live None Liveness probe (app running)
GET /health/ready None Readiness probe

Commands

UploadFileCommand

  • Input: Stream FileStream, string FileName, string ContentType, long FileSizeBytes, string UserId, string? TenantId, FileAccessLevel AccessLevel
  • Logic: Check file size limit -> Check user quota -> Generate object key ({accessLevel}/{userId}/{date}/{uniqueId}_{filename}) -> Upload to storage provider -> Save StorageFile entity -> Update quota -> SaveEntities
  • Validator: FileName (required, max 255, no .. / \), ContentType (required, max 100), FileSizeBytes (> 0, <= 500MB), UserId (required, max 128), TenantId (max 128)
  • Result: UploadFileResult(bool Success, Guid? FileId, string? ObjectKey, string? Error)

SignUploadCommand

  • Input: string UserId, string FileName, string ContentType, long FileSizeBytes, FileAccessLevel AccessLevel, string? TenantId
  • Logic: Validate file size -> Check user quota -> Generate object key -> Ensure bucket exists -> Generate pre-signed PUT URL
  • Validator: Same rules as UploadFileCommand
  • Result: SignUploadResult(bool Success, string? UploadUrl, string? ObjectKey, DateTime? ExpiresAt, string? Error)

ConfirmUploadCommand

  • Input: string UserId, string ObjectKey, string FileName, string ContentType, long FileSizeBytes, FileAccessLevel AccessLevel, string? TenantId
  • Logic: Validate object key ownership (userId in path) -> Verify file exists in storage -> Idempotency check (existing file by objectKey) -> Double-check quota -> Create StorageFile entity -> Update quota -> Invalidate caches
  • Validator: UserId (required, max 128), ObjectKey (required, max 1024), FileName (required, max 255), ContentType (required, max 100), FileSizeBytes (> 0)
  • Result: ConfirmUploadResult(bool Success, Guid? FileId, FileDto? Metadata, string? Error)

DeleteFileCommand

  • Input: Guid FileId, string UserId
  • Logic: Get file -> Check ownership -> Delete from storage provider (best-effort) -> Soft delete record -> Update quota -> Invalidate caches
  • Validator: FileId (required), UserId (required, max 128)
  • Result: DeleteFileResult(bool Success, string? Error)

CreateFileShareCommand

  • Input: Guid FileId, string UserId, SharePermission Permission, string? SharedWith, string? Password, DateTime? ExpiresAt, int? MaxDownloads
  • Logic: Verify file exists + ownership -> Create FileShare entity (generates random token, hashes password with PBKDF2) -> Save -> Return share URL
  • Validator: FileId (required), UserId (required, max 128), SharedWith (max 256), Password (min 4, max 100), MaxDownloads (> 0, <= 10000), ExpiresAt (must be future)
  • Result: CreateFileShareResult(bool Success, Guid? ShareId, string? ShareToken, string? ShareUrl, string? Error)

RevokeFileShareCommand

  • Input: Guid ShareId, string UserId
  • Logic: Get share -> Verify ownership -> Revoke (idempotent) -> Save
  • Result: RevokeFileShareResult(bool Success, string? Error)

InitiateMultipartUploadCommand

  • Input: string FileName, long FileSizeBytes, string ContentType, string UserId, int? ChunkSizeBytes, FileAccessLevel AccessLevel, string? TenantId
  • Logic: Validate file size -> Check quota -> Determine chunk size (default 5MB, min 5MB, max 100MB) -> Generate object key -> Initiate multipart upload at provider -> Create MultipartUpload entity (expiration: 24h) -> Save
  • Result: InitiateMultipartUploadResult(... UploadId, ProviderUploadId, ObjectKey, BucketName, TotalChunks, ChunkSizeBytes, ExpiresAt ...)

UploadPartCommand

  • Input: Guid UploadId, int PartNumber, Stream DataStream, string UserId
  • Logic: Get upload with parts -> Verify ownership -> Check status (InProgress) -> Check expiration -> Upload part to provider -> Add part to domain entity -> Save
  • Result: UploadPartResult(bool Success, string? ETag, string? Error)

CompleteMultipartUploadCommand

  • Input: Guid UploadId, string UserId
  • Logic: Get upload with parts -> Verify ownership -> Domain validation (all parts uploaded) -> Complete at provider -> Create StorageFile entity -> Update quota -> Save
  • Result: CompleteMultipartUploadResult(bool Success, Guid? FileId, string? ObjectKey, string? Error)

AbortMultipartUploadCommand

  • Input: Guid UploadId, string UserId
  • Logic: Get upload -> Verify ownership -> Abort at provider (best-effort) -> Mark as aborted in DB -> Save
  • Result: AbortMultipartUploadResult(bool Success, string? Error)

AdminDeleteFileCommand

  • Input: Guid FileId, string AdminUserId, string Reason
  • Logic: Same as DeleteFileCommand but bypasses ownership check. Logs admin action with reason.
  • Result: AdminDeleteFileResult(bool Success, string? DeletedFileName, string? FileOwnerUserId, string? Error)

AdminRevokeShareCommand

  • Input: Guid ShareId, string AdminUserId, string Reason
  • Logic: Get share -> Revoke -> Log admin action with reason -> Save
  • Result: AdminRevokeShareResult(bool Success, Guid? FileId, string? ShareOwnerUserId, string? Error)

UpdateUserQuotaCommand

  • Input: string TargetUserId, long MaxStorageBytes, int MaxFileCount, string? QuotaTier
  • Logic: Get or create quota -> Update limits (validates max >= current usage) -> Invalidate cache -> Save
  • Validator: TargetUserId (required, max 128), MaxStorageBytes (> 0), MaxFileCount (> 0), QuotaTier (max 50)
  • Result: UpdateUserQuotaResult(bool Success, QuotaUpdatedDto? Data, string? Error)

Queries

GetFileQuery

  • Input: Guid FileId, string UserId
  • Logic: Cache-aside pattern (10min TTL) -> Get from DB -> Check ownership
  • Result: FileDto?

GetUserFilesQuery

  • Input: string UserId, int Skip, int Take, string? SearchTerm
  • Logic: If search term: search by filename; otherwise: list by userId -> Count total -> Map to DTOs
  • Result: UserFilesResult(IReadOnlyList<FileDto> Files, int TotalCount)

GetUserQuotaQuery

  • Input: string UserId
  • Logic: Cache-aside pattern (5min TTL) -> GetOrCreate quota -> Map to DTO
  • Result: QuotaDto?

GetDownloadUrlQuery

  • Input: Guid FileId, string UserId, int ExpirationSeconds
  • Logic: Get file -> Check access (private files: owner only) -> Mark accessed -> Generate pre-signed download URL
  • Result: DownloadUrlResult(bool Success, string? Url, int? ExpiresInSeconds, string? Error)

GetFileSharesQuery

  • Input: Guid FileId, string UserId
  • Logic: Verify file ownership -> Get all shares for file -> Map to DTOs
  • Result: IEnumerable<FileShareDto>

AccessSharedFileQuery

  • Input: string Token, string? Password
  • Logic: Get share by token -> Validate (not expired/revoked/limit reached) -> Validate password (PBKDF2) -> Get file -> Generate pre-signed download URL -> Increment download count -> Save
  • Result: SharedFileAccessResult(bool Success, string? DownloadUrl, string? FileName, ...)

GetMultipartUploadProgressQuery

  • Input: Guid UploadId, string UserId
  • Logic: Get upload with parts -> Verify ownership -> Calculate progress
  • Result: MultipartUploadProgressResult?

AdminGetFilesQuery

  • Input: int PageNumber, int PageSize, string? UserId, string? AccessLevel, string? ContentType, DateTime? UploadedAfter/Before, string? SortBy, bool Descending
  • Logic: IgnoreQueryFilters (includes soft-deleted) -> Apply filters -> Sort -> Paginate
  • Result: AdminFilesResult(IReadOnlyList<AdminFileDto> Items, int TotalCount, int PageNumber, int PageSize, int TotalPages)

GetAllUsersQuotaQuery

  • Input: int PageNumber, int PageSize, string? QuotaTier, double? MinUsagePercentage, string? SortBy, bool Descending
  • Logic: Filter by tier, min usage% -> Sort -> Paginate
  • Result: AllUsersQuotaResult(IReadOnlyList<AdminQuotaDto> Items, ...)

GetStorageStatisticsQuery

  • Input: (none)
  • Logic: Aggregate stats: total users, total storage used/allocated, total files, avg usage%, users by tier, users near limit (>80%), users over limit (>=100%)
  • Result: StorageStatisticsDto

AdminGetSharesQuery

  • Input: int PageNumber, int PageSize, string? Status, string? SharedBy
  • Logic: Filter by status, sharedBy -> Paginate -> Order by CreatedAt desc
  • Result: AdminSharesResult(IReadOnlyList<AdminShareDto> Items, ...)

Domain Model

StorageFile (Aggregate Root)

  • Table: storage_files
  • Fields: Id, FileName, BucketName, ObjectKey, ContentType, FileSizeBytes, UserId, TenantId, Provider (enum->string), AccessLevel (enum->string), UploadedAt, LastAccessedAt, ExpiresAt, Checksum, IsDeleted, DeletedAt
  • Behavior Methods: MarkAccessed(), UpdateAccessLevel(level), Delete() (soft), SetExpiration(dt), UpdateFromVersion(objectKey, size, contentType)
  • Domain Events: FileUploadedDomainEvent (on create), FileDeletedDomainEvent (on delete)
  • Query Filter: !IsDeleted (global)

UserStorageQuota (Aggregate Root)

  • Table: user_storage_quotas
  • Fields: Id, UserId, MaxStorageBytes (default 1GB), UsedStorageBytes, MaxFileCount (default 1000), CurrentFileCount, QuotaTier (default "free"), LastUpdatedAt, CreatedAt
  • Computed: RemainingStorageBytes, RemainingFileCount, UsagePercentage
  • Behavior Methods: CanUpload(size), AddUsage(size, count), RemoveUsage(size, count), UpdateLimits(max, maxFiles, tier), RecalculateUsage(totalBytes, totalFiles)
  • Domain Events: UserQuotaUpdatedDomainEvent (on AddUsage/RemoveUsage)
  • Validation: UpdateLimits throws if new max < current usage

FileShare (Aggregate Root)

  • Table: file_shares
  • Fields: Id, FileId (FK -> storage_files), SharedBy, SharedWith, Permission (enum->string), ShareToken (unique, 32 random bytes base64), PasswordHash (PBKDF2-SHA256, 100K iterations), ExpiresAt, MaxDownloads, DownloadCount, Status (enum->string), CreatedAt, RevokedAt
  • Behavior Methods: IsValid() (checks status, expiry, download limit), ValidatePassword(pwd), IncrementDownloadCount(), Revoke()
  • Enums: SharePermission (View=0, Download=1, Edit=2, Admin=3), FileShareStatus (Active=0, Expired=1, Revoked=2, LimitReached=3)

FileVersion (Entity)

  • Table: file_versions
  • Fields: Id, FileId (FK -> storage_files), VersionNumber, ObjectKey, SizeBytes, ContentType, Checksum, CreatedAt, CreatedBy, Comment, IsCurrent
  • Behavior Methods: MarkAsCurrent(), UnmarkAsCurrent()

Folder (Aggregate Root)

  • Table: folders
  • Fields: Id, UserId, ParentId (FK self-referencing, restrict delete), Name, Path (materialized path e.g. /parent/child/), Level (0=root), CreatedAt, UpdatedAt, IsDeleted, DeletedAt
  • Behavior Methods: CreateRoot(userId, name) (static), CreateChild(name), Rename(newName), MoveTo(newParent), Delete() (soft)
  • Design Note: Folders are LOGICAL only. Storage uses flat UUID keys. Rename/move is O(1) -- database-only.
  • Query Filter: !IsDeleted (global)

MultipartUpload (Aggregate Root)

  • Table: multipart_uploads
  • Fields: Id, UserId, FileName, TotalSizeBytes, ChunkSizeBytes, TotalChunks (computed), UploadedChunks, ProviderUploadId, BucketName, ObjectKey, ContentType, Status (enum->string), CreatedAt, CompletedAt, ExpiresAt
  • Behavior Methods: AddPart(partNum, etag, size), Complete() (validates all parts uploaded), Abort(), MarkFailed(), IsExpired(), GetProgressPercentage()
  • Domain Events: MultipartUploadInitiatedDomainEvent, MultipartUploadCompletedDomainEvent, MultipartUploadAbortedDomainEvent
  • Child Entity: MultipartUploadPart (Id, MultipartUploadId, PartNumber, ETag, SizeBytes, UploadedAt)
  • Enums: MultipartUploadStatus (InProgress=0, Completed=1, Aborted=2, Failed=3, Expired=4)

Database Schema

Table: storage_files

Column Type Constraints
id uuid PK
file_name varchar(255) NOT NULL
bucket_name varchar(100) NOT NULL
object_key varchar(500) NOT NULL, UNIQUE
content_type varchar(100) NOT NULL
file_size_bytes bigint NOT NULL
user_id varchar(100) NOT NULL
tenant_id varchar(100) nullable
provider varchar (string enum) NOT NULL
access_level varchar (string enum) NOT NULL
uploaded_at timestamp NOT NULL
last_accessed_at timestamp nullable
expires_at timestamp nullable
checksum varchar(100) nullable
is_deleted boolean NOT NULL, default false
deleted_at timestamp nullable

Indexes: object_key (unique), user_id, tenant_id, uploaded_at, is_deleted Query Filter: is_deleted = false

Table: user_storage_quotas

Column Type Constraints
id uuid PK
user_id varchar(100) NOT NULL, UNIQUE
max_storage_bytes bigint NOT NULL
used_storage_bytes bigint NOT NULL
max_file_count int NOT NULL
current_file_count int NOT NULL
quota_tier varchar(50) nullable
created_at timestamp NOT NULL
last_updated_at timestamp NOT NULL

Indexes: user_id (unique)

Table: file_shares

Column Type Constraints
id uuid PK
file_id uuid NOT NULL, FK -> storage_files (CASCADE)
shared_by varchar(255) NOT NULL
shared_with varchar(255) nullable
permission varchar(50) (string enum) NOT NULL
share_token varchar(255) NOT NULL, UNIQUE
password_hash varchar(255) nullable
expires_at timestamp nullable
max_downloads int nullable
download_count int NOT NULL, default 0
status varchar(50) (string enum) NOT NULL
created_at timestamp NOT NULL
revoked_at timestamp nullable

Indexes: file_id, share_token (unique), shared_with, shared_by

Table: file_versions

Column Type Constraints
id uuid PK
file_id uuid NOT NULL, FK -> storage_files (CASCADE)
version_number int NOT NULL
object_key varchar(500) NOT NULL
size_bytes bigint NOT NULL
content_type varchar(100) NOT NULL
checksum varchar(100) nullable
created_at timestamp NOT NULL
created_by varchar(255) NOT NULL
comment varchar(500) nullable
is_current boolean default false

Indexes: file_id, (file_id + version_number) unique, (file_id + is_current)

Table: folders

Column Type Constraints
id uuid PK
user_id varchar(255) NOT NULL
parent_id uuid nullable, FK self (RESTRICT)
name varchar(255) NOT NULL
path varchar(1000) NOT NULL
level int NOT NULL
created_at timestamp NOT NULL
updated_at timestamp NOT NULL
is_deleted boolean NOT NULL, default false
deleted_at timestamp nullable

Indexes: user_id, parent_id, (user_id + parent_id + name) unique filtered is_deleted = false, path Query Filter: is_deleted = false

Table: multipart_uploads

Column Type Constraints
id uuid PK
user_id varchar(255) NOT NULL
file_name varchar(255) NOT NULL
total_size_bytes bigint NOT NULL
chunk_size_bytes int NOT NULL
total_chunks int NOT NULL
uploaded_chunks int NOT NULL
upload_id varchar(255) NOT NULL (provider upload ID)
bucket_name varchar(255) NOT NULL
object_key varchar(500) NOT NULL
content_type varchar(100) NOT NULL
status varchar(50) (string enum) NOT NULL
created_at timestamp NOT NULL
completed_at timestamp nullable
expires_at timestamp NOT NULL

Indexes: (user_id + status), upload_id, created_at

Table: multipart_upload_parts

Column Type Constraints
id uuid PK
multipart_upload_id uuid NOT NULL, FK -> multipart_uploads (CASCADE)
part_number int NOT NULL
etag varchar(255) NOT NULL
size_bytes bigint NOT NULL
uploaded_at timestamp NOT NULL

Indexes: (multipart_upload_id + part_number) unique

Migrations

  1. 20260112185402_InitialCreate - storage_files, user_storage_quotas
  2. 20260113155939_AddMultipartUploadTables - multipart_uploads, multipart_upload_parts
  3. 20260113170635_AddFileSharing - file_shares
  4. 20260113171238_AddFileVersioning - file_versions
  5. 20260113171612_AddFolders - folders

Dependencies

NuGet Packages

API Layer:

  • MediatR 12.4.1
  • FluentValidation 11.11.0
  • Microsoft.AspNetCore.Authentication.JwtBearer 10.0.1
  • Swashbuckle.AspNetCore 7.2.0
  • Asp.Versioning.Mvc 8.1.0
  • AspNetCore.HealthChecks.NpgSql 8.0.2
  • AspNetCore.HealthChecks.Redis 8.0.1
  • Hellang.Middleware.ProblemDetails 6.5.1
  • Serilog.AspNetCore 8.0.3

Domain Layer:

  • MediatR.Contracts 2.0.1

Infrastructure Layer:

  • Microsoft.EntityFrameworkCore 10.0.0
  • Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
  • MediatR 12.4.1
  • Dapper 2.1.35
  • Polly 8.5.0
  • StackExchange.Redis 2.8.16
  • Minio 6.0.4
  • AWSSDK.S3 3.7.400 (for low-level multipart upload)
  • Aliyun.OSS.SDK.NetCore 2.14.1

External Services

  • IAM Service (http://localhost:5001) - JWT token validation
  • MinIO (167.114.174.113:9000) - Object storage backend
  • Redis (167.114.174.113:6379) - Caching (quota, file metadata, user files)
  • PostgreSQL (Neon) - Metadata persistence

Configuration

appsettings.json

Section Key Default Description
ConnectionStrings DefaultConnection Neon PostgreSQL Database connection
Storage Provider minio Active provider (minio or aliyun)
Storage DefaultBucket goodgo Default S3 bucket
Storage PreSignedUrlExpirationSeconds 3600 Pre-signed URL TTL
Storage MaxFileSizeBytes 104857600 (100MB) Max upload size
Storage:MinIO Endpoint 167.114.174.113:9000 MinIO endpoint
Storage:MinIO AccessKey minioadmin MinIO access key
Storage:MinIO UseSSL false SSL for MinIO
Storage:MinIO Region us-east-1 MinIO region
Storage:AliyunOSS Endpoint (empty) Aliyun OSS endpoint
Redis Host 167.114.174.113 Redis host
Redis Port 6379 Redis port
Redis Database 0 Redis database index
Jwt Secret goodgo-iam-service-secret-key-32chars! JWT secret
Jwt Issuer goodgo-platform JWT issuer
Jwt AccessTokenExpiryMinutes 15 Token TTL
CDN Enabled false CDN integration toggle
CDN BaseUrl (empty) CDN base URL
IamService BaseUrl http://localhost:5001 IAM service URL
IamService TimeoutSeconds 30 HTTP timeout

Architecture Notes

Upload Patterns

  1. Server-Side Upload (POST /files/upload): File goes through the API, uploaded to MinIO by the handler. Simple but doesn't scale for millions of concurrent uploads.
  2. Direct Client Upload (POST /storage/sign-upload + POST /storage/confirm-upload): Client gets pre-signed PUT URL, uploads directly to MinIO, then confirms. Bypasses backend for file transfer -- highly scalable.
  3. Multipart Upload (POST /files/multipart/initiate -> upload parts -> POST /files/multipart/complete): For large files, uploaded in 5MB chunks. 24-hour expiration.

Caching Strategy

  • File metadata: Redis cache-aside, 10min TTL
  • User quota: Redis cache-aside, 5min TTL
  • Cache invalidation on: file delete, upload confirm, quota update

Security

  • All user-facing endpoints require JWT Bearer auth
  • File ownership validation on all operations (except admin endpoints)
  • Share passwords hashed with PBKDF2-SHA256 (100K iterations, 16-byte salt)
  • Share tokens: 32 random bytes, base64url-encoded
  • Admin endpoints require Admin or SuperAdmin role
  • Object key ownership validation via path structure ({access}/{userId}/...)

Object Key Structure

{accessLevel}/{userId}/{yyyyMMdd}/{8-char-uuid}_{sanitized-filename}
  • accessLevel: public, private, or shared
  • File names sanitized (invalid chars removed, max 100 chars + extension)

MediatR Pipeline

LoggingBehavior -> ValidatorBehavior -> TransactionBehavior -> Handler

Resilience

  • PostgreSQL: retry on failure (5 retries, 30s max delay)
  • IAM HTTP client: Polly retry (3 retries, exponential backoff) + circuit breaker (5 failures, 30s break)
  • Storage delete: best-effort (continues with soft delete even if provider delete fails)
  • Multipart upload abort: best-effort provider cleanup

Tests

  • Unit Tests: StorageFile, FileShare, Folder domain entity tests; UploadFileCommandHandler, DeleteFileCommandHandler, SignUploadCommandHandler, FileShareCommandHandler tests
  • Functional Tests: FilesApi, FileSharingApi, SignedUrlApi tests with CustomWebApplicationFactory (InMemory DB)