# 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>, pub auth_service: Option, pub admin_settings_service: Option>, pub trash_service: Option>, pub share_service: Option>, pub favorites_service: Option>, pub recent_service: Option>, pub storage_usage_service: Option>, pub calendar_service: Option>, pub contact_service: Option>, pub calendar_use_case: Option>, pub addressbook_use_case: Option>, pub contact_use_case: Option>, } ``` 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, pub file_content_cache: Arc, pub thumbnail_service: Arc, pub chunked_upload_service: Arc, pub image_transcode_service: Arc, pub dedup_service: Arc, pub compression_service: Arc, pub zip_service: Arc, pub config: AppConfig, } pub struct RepositoryServices { pub folder_repository: Arc, pub folder_repo_concrete: Arc, pub file_read_repository: Arc, pub file_write_repository: Arc, pub i18n_repository: Arc, pub trash_repository: Option>, } pub struct ApplicationServices { pub folder_service_concrete: Arc, pub folder_service: Arc, pub file_upload_service: Arc, pub file_retrieval_service: Arc, pub file_management_service: Arc, pub file_use_case_factory: Arc, pub i18n_service: Arc, pub trash_service: Option>, pub search_service: Option>, pub share_service: Option>, pub favorites_service: Option>, pub recent_service: Option>, } pub struct AuthServices { pub token_service: Arc, pub auth_application_service: Arc, } ``` --- ## 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>, } ``` 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, dedup: Arc, folder_repo: Arc, } ``` 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, dedup: Arc, folder_repo: Arc, } ``` 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, 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, } ``` | 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, ip_address: Option, user_agent: Option, created_at: DateTime, 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; async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult; async fn get_session_by_refresh_token(&self, token: &str) -> SessionRepositoryResult; async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult>; async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>; async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult; async fn delete_expired_sessions(&self) -> SessionRepositoryResult; } ``` ### 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; async fn get_session_by_refresh_token(&self, token: &str) -> Result; async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>; async fn revoke_all_user_sessions(&self, user_id: &str) -> Result; } ``` ### SessionPgRepository (Infrastructure) **File:** `src/infrastructure/repositories/pg/session_pg_repository.rs` ```rust pub struct SessionPgRepository { pool: Arc, } ``` 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` — hot-reloadable OIDC configuration - `Mutex>` — 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; fn create_file_retrieval_use_case(&self) -> Arc; fn create_file_management_use_case(&self) -> Arc; } ``` **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 ┌─────────────────────▼───────────────────────────────────────┐ │ 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) ```