2026-02-14 18:27:30 +01:00
# 01 - Internal Architecture
OxiCloud follows a **hexagonal (ports & adapters) architecture** organized in four layers:
```
Domain → Application → Infrastructure → Interfaces
```
All cross-layer dependencies point inward via trait-based ports. The DI container (**AppServiceFactory**) wires concrete implementations at startup.
---
## Storage Model: 100% Blob Storage
OxiCloud uses a **100% blob storage model** where:
- **File metadata** (name, folder, size, user, timestamps, trash status) is stored in **PostgreSQL** (`storage.files` table).
- **File content** is stored as content-addressed blobs via **DedupService** at `.blobs/{prefix}/{hash}.blob` .
- **Folder structure** is purely virtual — represented as rows in `storage.folders` (no filesystem directories per user).
- **Trash** is a soft-delete flag (`is_trashed` , `trashed_at` ) on files and folders, exposed via `storage.trash_items` VIEW.
There are no filesystem-based ID mappings, no `folder_ids.json` /`file_ids.json` , and no storage mediator.
---
## Dependency Injection Container
### AppServiceFactory
**File:** `src/common/di.rs`
```rust
pub struct AppServiceFactory {
storage_path : PathBuf ,
locales_path : PathBuf ,
config : AppConfig ,
}
```
Initialization order in `build_app_state()` :
1. **Core services** — path, content cache, thumbnail, chunked upload, transcode, dedup, compression
2. **Repository services** — `FolderDbRepository` , `FileBlobReadRepository` , `FileBlobWriteRepository` , `TrashDbRepository` (all PgPool-backed)
3. **Trash service** (if **enable_trash** enabled)
4. **Application services** — folder, file upload/retrieval/management, search, i18n
5. **Share service** (if **enable_file_sharing** enabled)
6. **DB-dependent services** — favorites, recent, storage usage, auth (via **auth_factory** )
7. **Preload** translations
8. **ZIP service** (needs file retrieval + folder service, wired last)
9. **Assemble AppState** + admin settings + CalDAV/CardDAV
### AppState (Global State)
```rust
pub struct AppState {
pub core : CoreServices ,
pub repositories : RepositoryServices ,
pub applications : ApplicationServices ,
pub db_pool : Option < Arc < PgPool >> ,
pub auth_service : Option < AuthServices > ,
pub admin_settings_service : Option < Arc < AdminSettingsService >> ,
pub trash_service : Option < Arc < dyn TrashUseCase >> ,
pub share_service : Option < Arc < dyn ShareUseCase >> ,
pub favorites_service : Option < Arc < dyn FavoritesUseCase >> ,
pub recent_service : Option < Arc < dyn RecentItemsUseCase >> ,
pub storage_usage_service : Option < Arc < dyn StorageUsagePort >> ,
pub calendar_service : Option < Arc < dyn StorageUseCase >> ,
pub contact_service : Option < Arc < dyn StorageUseCase >> ,
pub calendar_use_case : Option < Arc < dyn CalendarUseCase >> ,
pub addressbook_use_case : Option < Arc < dyn AddressBookUseCase >> ,
pub contact_use_case : Option < Arc < dyn ContactUseCase >> ,
}
```
Builder pattern: `new()` → `with_database()` → `with_auth_services()` → `with_trash_service()` → ... → `for_routing()` . The `Default` impl uses stubs from `crate::common::stubs` .
### Service Groups
```rust
pub struct CoreServices {
pub path_service : Arc < PathService > ,
pub file_content_cache : Arc < dyn ContentCachePort > ,
pub thumbnail_service : Arc < dyn ThumbnailPort > ,
pub chunked_upload_service : Arc < dyn ChunkedUploadPort > ,
pub image_transcode_service : Arc < dyn ImageTranscodePort > ,
pub dedup_service : Arc < dyn DedupPort > ,
pub compression_service : Arc < dyn CompressionPort > ,
pub zip_service : Arc < dyn ZipPort > ,
pub config : AppConfig ,
}
pub struct RepositoryServices {
pub folder_repository : Arc < dyn FolderStoragePort > ,
pub folder_repo_concrete : Arc < FolderDbRepository > ,
pub file_read_repository : Arc < dyn FileReadPort > ,
pub file_write_repository : Arc < dyn FileWritePort > ,
pub i18n_repository : Arc < dyn I18nService > ,
pub trash_repository : Option < Arc < dyn TrashRepository >> ,
}
pub struct ApplicationServices {
pub folder_service_concrete : Arc < FolderService > ,
pub folder_service : Arc < dyn FolderUseCase > ,
pub file_upload_service : Arc < dyn FileUploadUseCase > ,
pub file_retrieval_service : Arc < dyn FileRetrievalUseCase > ,
pub file_management_service : Arc < dyn FileManagementUseCase > ,
pub file_use_case_factory : Arc < dyn FileUseCaseFactory > ,
pub i18n_service : Arc < I18nApplicationService > ,
pub trash_service : Option < Arc < dyn TrashUseCase >> ,
pub search_service : Option < Arc < dyn SearchUseCase >> ,
pub share_service : Option < Arc < dyn ShareUseCase >> ,
pub favorites_service : Option < Arc < dyn FavoritesUseCase >> ,
pub recent_service : Option < Arc < dyn RecentItemsUseCase >> ,
}
pub struct AuthServices {
pub token_service : Arc < dyn TokenServicePort > ,
pub auth_application_service : Arc < AuthApplicationService > ,
}
```
---
## Database Schema (Storage)
All file and folder metadata lives in the `storage` PostgreSQL schema:
```sql
CREATE SCHEMA IF NOT EXISTS storage ;
CREATE TABLE storage . folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name TEXT NOT NULL ,
parent_id UUID REFERENCES storage . folders ( id ) ON DELETE CASCADE ,
user_id VARCHAR ( 36 ) NOT NULL REFERENCES auth . users ( id ),
is_trashed BOOLEAN NOT NULL DEFAULT FALSE ,
trashed_at TIMESTAMPTZ ,
original_parent_id UUID ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW ()
);
CREATE TABLE storage . files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
name TEXT NOT NULL ,
folder_id UUID NOT NULL REFERENCES storage . folders ( id ) ON DELETE CASCADE ,
user_id VARCHAR ( 36 ) NOT NULL REFERENCES auth . users ( id ),
blob_hash TEXT NOT NULL ,
size BIGINT NOT NULL DEFAULT 0 ,
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream' ,
is_trashed BOOLEAN NOT NULL DEFAULT FALSE ,
trashed_at TIMESTAMPTZ ,
original_folder_id UUID ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW ()
);
CREATE OR REPLACE VIEW storage . trash_items AS
SELECT id , name , 'file' AS item_type , folder_id AS parent_id ,
user_id , size , mime_type , trashed_at , created_at
FROM storage . files WHERE is_trashed = TRUE
UNION ALL
SELECT id , name , 'folder' AS item_type , parent_id ,
user_id , 0 AS size , NULL AS mime_type , trashed_at , created_at
FROM storage . folders WHERE is_trashed = TRUE ;
```
---
## Repository Layer (Infrastructure)
All repositories use **PgPool** for metadata and **DedupService** for blob content.
### FolderDbRepository
**File:** `src/infrastructure/repositories/pg/folder_db_repository.rs`
```rust
pub struct FolderDbRepository {
pool : Option < Arc < PgPool >> ,
}
```
Implements `FolderRepository` . Uses recursive CTEs for path building, unique constraints for name dedup within parent, and soft-delete flags for trash operations.
Key methods: `create_folder` , `get_folder` , `get_folder_by_path` , `list_folders` , `rename_folder` , `move_folder` , `delete_folder` , `move_to_trash` , `restore_from_trash` , `create_home_folder` , `get_folder_user_id` .
`new_stub()` creates a pool-less instance for `AppState::default()` .
### FileBlobReadRepository
**File:** `src/infrastructure/repositories/pg/file_blob_read_repository.rs`
```rust
pub struct FileBlobReadRepository {
pool : Arc < PgPool > ,
dedup : Arc < dyn DedupPort > ,
folder_repo : Arc < FolderDbRepository > ,
}
```
Implements `FileReadPort` . Reads metadata from `storage.files` and content from blob store via `dedup.read_blob()` / `read_blob_bytes()` .
Key methods: `get_file` , `list_files` , `get_file_content` , `get_file_stream` , `get_file_range_stream` , `get_file_mmap` , `get_file_path` , `get_parent_folder_id` .
### FileBlobWriteRepository
**File:** `src/infrastructure/repositories/pg/file_blob_write_repository.rs`
```rust
pub struct FileBlobWriteRepository {
pool : Arc < PgPool > ,
dedup : Arc < dyn DedupPort > ,
folder_repo : Arc < FolderDbRepository > ,
}
```
Implements `FileWritePort` . Stores content via `dedup.store_bytes()` (returns hash), then INSERTs metadata into `storage.files` .
Key methods: `save_file` , `save_file_from_stream` , `move_file` , `rename_file` , `delete_file` , `update_file_content` , `move_to_trash` , `restore_from_trash` , `delete_file_permanently` .
### TrashDbRepository
**File:** `src/infrastructure/repositories/pg/trash_db_repository.rs`
```rust
pub struct TrashDbRepository {
pool : Arc < PgPool > ,
retention_days : u32 ,
}
```
Implements `TrashRepository` . Reads from `storage.trash_items` VIEW. `clear_trash` DELETEs rows where `is_trashed = TRUE` . `get_expired_items` checks `trashed_at` against the configured retention period.
---
## Path Service
**File:** `src/infrastructure/services/path_service.rs`
```rust
pub struct PathService {
root_path : PathBuf , // e.g., ./storage
}
```
Used for resolving storage root paths (blob storage directory, thumbnail paths, etc.). Not used for per-user folder resolution — that is handled by `FolderDbRepository` via PostgreSQL.
### StoragePath (Domain Value Object)
**File:** `src/domain/services/path_service.rs`
```rust
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoragePath {
segments : Vec < String > ,
}
```
| Method | Description |
|---|---|
| `root()` | Empty path (storage root) |
| `from_string(path)` | Parse from `/` -delimited string |
| `join(segment)` | Append a segment |
| `file_name()` | Last segment |
| `parent()` | All segments except last |
| `to_string()` | Join segments with `/` |
### Trait Implementations
- **StoragePort** — `resolve_path()` , `ensure_directory()` , `file_exists()` , `directory_exists()`
---
## Session Management
### Session Entity
**File:** `src/domain/entities/session.rs`
```rust
pub struct Session {
id : String , // UUID v4
user_id : String ,
refresh_token : String ,
expires_at : DateTime < Utc > ,
ip_address : Option < String > ,
user_agent : Option < String > ,
created_at : DateTime < Utc > ,
revoked : bool ,
}
```
Constructors:
- `Session::new(user_id, refresh_token, ip_address, user_agent, expires_in_days)` — generates UUID, panics if **user_id** or **refresh_token** empty
- `Session::from_raw(...)` — for DB reconstruction
### SessionRepository (Domain Port)
**File:** `src/domain/repositories/session_repository.rs`
```rust
#[async_trait]
pub trait SessionRepository : Send + Sync + 'static {
async fn create_session ( & self , session : Session ) -> SessionRepositoryResult < Session > ;
async fn get_session_by_id ( & self , id : & str ) -> SessionRepositoryResult < Session > ;
async fn get_session_by_refresh_token ( & self , token : & str ) -> SessionRepositoryResult < Session > ;
async fn get_sessions_by_user_id ( & self , user_id : & str ) -> SessionRepositoryResult < Vec < Session >> ;
async fn revoke_session ( & self , session_id : & str ) -> SessionRepositoryResult < () > ;
async fn revoke_all_user_sessions ( & self , user_id : & str ) -> SessionRepositoryResult < u64 > ;
async fn delete_expired_sessions ( & self ) -> SessionRepositoryResult < u64 > ;
}
```
### SessionStoragePort (Application Port)
**File:** `src/application/ports/auth_ports.rs`
```rust
#[async_trait]
pub trait SessionStoragePort : Send + Sync + 'static {
async fn create_session ( & self , session : Session ) -> Result < Session , DomainError > ;
async fn get_session_by_refresh_token ( & self , token : & str ) -> Result < Session , DomainError > ;
async fn revoke_session ( & self , session_id : & str ) -> Result < (), DomainError > ;
async fn revoke_all_user_sessions ( & self , user_id : & str ) -> Result < u64 , DomainError > ;
}
```
### SessionPgRepository (Infrastructure)
**File:** `src/infrastructure/repositories/pg/session_pg_repository.rs`
```rust
pub struct SessionPgRepository {
pool : Arc < PgPool > ,
}
```
Implements both **SessionRepository** and **SessionStoragePort** . Uses `with_transaction()` helper for write operations. `create_session` also updates `auth.users.last_login_at` within the same transaction.
### Database Schema
```sql
CREATE TABLE IF NOT EXISTS auth . sessions (
id VARCHAR ( 36 ) PRIMARY KEY ,
user_id VARCHAR ( 36 ) NOT NULL REFERENCES auth . users ( id ) ON DELETE CASCADE ,
refresh_token TEXT NOT NULL UNIQUE ,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL ,
ip_address TEXT ,
user_agent TEXT ,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ,
revoked BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX idx_sessions_user_id ON auth . sessions ( user_id );
CREATE INDEX idx_sessions_refresh_token ON auth . sessions ( refresh_token );
CREATE INDEX idx_sessions_expires_at ON auth . sessions ( expires_at );
CREATE INDEX idx_sessions_active ON auth . sessions ( user_id , revoked )
WHERE NOT revoked AND is_session_active ( expires_at );
```
### Auth Service
**File:** `src/application/services/auth_application_service.rs`
**AuthApplicationService** orchestrates authentication using:
- **UserStoragePort** — user CRUD
- **SessionStoragePort** — session lifecycle
- **PasswordHasherPort** — Argon2id hashing
- **TokenServicePort** — JWT generation/validation
- `RwLock<OidcState>` — hot-reloadable OIDC configuration
- `Mutex<HashMap<String, PendingOidcFlow>>` — in-flight OIDC login states
Wired by `auth_factory.rs` : **UserPgRepository** + **SessionPgRepository** + **Argon2PasswordHasher** + **JwtTokenService** → **AuthApplicationService** .
---
## File Use Case Factory
**File:** `src/application/services/file_use_case_factory.rs`
```rust
pub trait FileUseCaseFactory : Send + Sync + 'static {
fn create_file_upload_use_case ( & self ) -> Arc < dyn FileUploadUseCase > ;
fn create_file_retrieval_use_case ( & self ) -> Arc < dyn FileRetrievalUseCase > ;
fn create_file_management_use_case ( & self ) -> Arc < dyn FileManagementUseCase > ;
}
```
**AppFileUseCaseFactory** creates lightweight service instances with only **FileReadPort** / **FileWritePort** .
### File Operation Port Hierarchy
| Port | Key Methods |
|---|---|
| **FileUploadUseCase** | `upload_file()` , `smart_upload()` (returns **UploadStrategy** : `Buffered` <1MB, `Streaming` ≥1MB), `create_file()` , `update_file()` |
| **FileRetrievalUseCase** | `get_file()` , `get_file_content()` , `get_file_stream()` , `get_file_optimized()` (content-cache → WebP transcode → mmap → streaming), `get_file_range_stream()` |
| **FileManagementUseCase** | `move_file()` , `rename_file()` , `delete_file()` , `delete_with_cleanup()` (trash-first with dedup reference cleanup) |
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Interfaces Layer │
│ Axum Router → API Routes + Middleware (Auth, Compress) │
└─────────────────────┬───────────────────────────────────────┘
│ Arc<AppState>
┌─────────────────────▼───────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ FileUpload │ │ FolderService│ │ AuthApplication │ │
│ │ FileRetrieval│ │ SearchService│ │ AdminSettings │ │
│ │ FileMgmt │ │ I18nService │ │ TrashService │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬──────────┘ │
│ │ Ports (traits) │ │ │
└─────────┼────────────────┼─────────────────────┼────────────┘
│ │ │
┌─────────▼────────────────▼─────────────────────▼────────────┐
│ Infrastructure Layer │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ FileBlobRead │ │ PathService │ │ SessionPg │ │
│ │ FileBlobWrite │ │ DedupService │ │ UserPg │ │
│ │ FolderDb │ │ Thumbnail │ │ JwtTokenService │ │
│ │ TrashDb │ │ Transcode │ │ Argon2Hasher │ │
│ └────────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ContentCache │ │ Compression │ │ ChunkedUpload │ │
│ │ BufferPool │ │ ZipService │ │ ShareFsRepo │ │
│ └────────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Domain Layer │
│ Entities: File, Folder, Session, User, Calendar, Contact │
│ Value Objects: StoragePath │
│ Repository Traits: FolderRepository, TrashRepository, ... │
│ Domain Errors │
└─────────────────────────────────────────────────────────────┘
```
### Data Flow: File Upload
```
HTTP Request (multipart)
→ FileUploadService.smart_upload()
→ FileBlobWriteRepository.save_file() / save_file_from_stream()
→ DedupService.store_bytes() → .blobs/{prefix}/{hash}.blob
→ INSERT INTO storage.files (name, folder_id, blob_hash, size, ...)
→ 201 Created (FileDto)
```
### Data Flow: File Download
```
HTTP Request (GET /api/files/{id}/download)
→ FileRetrievalService.get_file_optimized()
→ ContentCache hit? → serve from RAM
→ FileBlobReadRepository.get_file_content() / get_file_stream()
→ SELECT blob_hash FROM storage.files WHERE id = $1
→ DedupService.read_blob(hash) → bytes from .blobs/
→ Optional WebP transcode → response
```
### Data Flow: Folder Operations
```
HTTP Request (POST /api/folders)
→ FolderService.create_folder()
→ FolderDbRepository.create_folder()
→ INSERT INTO storage.folders (name, parent_id, user_id, ...)
→ 201 Created (FolderDto)
```