>(path: P) -> Result<(), IoError>
-
-/// Rename with directory sync
-pub async fn rename_with_sync(from: P, to: Q) -> Result<(), IoError>
-
-/// Delete with directory sync
-pub async fn remove_file_with_sync>(path: P) -> Result<(), IoError>
-```
-
-### fsync Guarantees
-
-- `sync_all()` on written files ensures data and metadata reach the physical storage device
-- Directory entries are synced after create/rename/delete operations
-- Prevents data loss during crashes or power failures between OS buffer flush and disk write
-
----
-
-## Transaction Flow: File Upload
-
-```
-1. DedupService.store_bytes(content)
- → Compute SHA-256 hash
- → Check if blob exists (dedup hit → increment ref, return hash)
- → Write to .blobs/{prefix}/{hash}.blob.tmp
- → fsync + rename → .blobs/{prefix}/{hash}.blob
-
-2. FileBlobWriteRepository.save_file()
- → BEGIN TRANSACTION
- → INSERT INTO storage.files (name, folder_id, blob_hash, size, ...)
- → COMMIT
-```
-
-If step 1 fails, no metadata is written. If step 2 fails, the blob exists but is unreferenced (cleaned up by garbage collection). Data is never in an inconsistent state.
-
-## Transaction Flow: File Deletion
-
-```
-1. FileBlobWriteRepository.delete_file_permanently()
- → BEGIN TRANSACTION
- → DELETE FROM storage.files WHERE id = $1 (captures blob_hash first)
- → COMMIT
-
-2. DedupService.decrement_ref(blob_hash)
- → Decrement reference counter
- → If counter reaches 0, delete the blob file
-```
-
-If step 2 fails, an unreferenced blob may remain on disk (occupies space but is not a correctness issue). Future garbage collection can clean these up.
-
----
-
-## Benefits
-
-1. **ACID transactions** — metadata operations are atomic, consistent, isolated, and durable
-2. **Content-addressable storage** — identical content is stored once, referenced by hash
-3. **Crash resilience** — atomic blob writes + PostgreSQL WAL ensure recovery
-4. **No partial writes** — temp file + rename pattern guarantees all-or-nothing
-5. **Referential integrity** — foreign keys prevent orphaned metadata
-
----
-
-## Performance Considerations
-
-- PostgreSQL connection pooling (`sqlx::PgPool`) amortizes connection overhead
-- Dedup hash computation is CPU-bound but avoids unnecessary disk writes for duplicate content
-- Blob fsync adds latency vs. buffered writes, but ensures durability for critical user data
-- Content cache (in-memory LRU) serves repeat reads without disk or DB access
diff --git a/doc/i18n.md b/doc/i18n.md
deleted file mode 100644
index f012f00f..00000000
--- a/doc/i18n.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# 16 - Internationalization
-
-JSON-based translation system. Translations are loaded from static files, cached in memory, and served via a public REST API (no auth required).
-
-## Supported Languages
-
-| Locale | Code | File |
-|---|---|---|
-| English | `en` | `static/locales/en.json` |
-| Spanish | `es` | `static/locales/es.json` |
-| French | `fr` | `static/locales/fr.json` |
-| German | `de` | `static/locales/de.json` |
-| Portuguese | `pt` | `static/locales/pt.json` |
-| Persian | `fa` | `static/locales/fa.json` |
-| Chinese | `zh` | `static/locales/zh.json` |
-| Dutch | `nl` | `static/locales/nl.json` |
-
-Default locale: **en** (English).
-
-## Architecture
-
-| Layer | Component | File |
-|---|---|---|
-| Domain Port | **I18nService** trait, **Locale** enum | `src/domain/services/i18n_service.rs` |
-| Application Service | **I18nApplicationService** | `src/application/services/i18n_application_service.rs` |
-| Application DTOs | **LocaleDto**, **TranslationRequestDto**, etc. | `src/application/dtos/i18n_dto.rs` |
-| Infrastructure | **FileSystemI18nService** | `src/infrastructure/services/file_system_i18n_service.rs` |
-| Interfaces | **I18nHandler** | `src/interfaces/api/handlers/i18n_handler.rs` |
-
-## REST API
-
-Public endpoints (no authentication), under `/api/i18n`:
-
-| Method | Path | Handler | Description |
-|---|---|---|---|
-| `GET` | `/api/i18n/locales` | `get_locales` | List available locales |
-| `GET` | `/api/i18n/translate` | `translate` | Translate a key (`?key=...&locale=...`) |
-| `GET` | `/api/i18n/locales/{locale_code}` | `get_translations_by_locale` | Get all translations for a locale |
-
-### Examples
-
-```bash
-# List available locales
-curl "https://oxicloud.example.com/api/i18n/locales"
-# [{"code":"en","name":"English"},{"code":"es","name":"Spanish"}, ...]
-
-# Translate a key
-curl "https://oxicloud.example.com/api/i18n/translate?key=app.title&locale=es"
-# {"key":"app.title","locale":"es","text":"OxiCloud"}
-
-# Get all translations for a locale
-curl "https://oxicloud.example.com/api/i18n/locales/en"
-# { "app": { "title": "OxiCloud", ... }, "nav": { ... }, ... }
-```
-
-## Translation File Format
-
-Nested JSON with dot-delimited key lookups:
-
-```json
-{
- "app": {
- "title": "OxiCloud",
- "description": "Your personal cloud storage"
- },
- "nav": {
- "files": "Files",
- "shared": "Shared",
- "recent": "Recent",
- "favorites": "Favorites",
- "trash": "Trash"
- },
- "actions": {
- "search": "Search files...",
- "new_folder": "New folder",
- "upload": "Upload",
- "download": "Download",
- "delete": "Delete"
- },
- "share": { ... },
- "user_menu": { ... }
-}
-```
-
-Key lookup: `"nav.files"` resolves to `"Files"`.
-
-## Fallback Behavior
-
-If a key is missing in the requested locale, the system falls back to English (`en`). If still not found, returns an `I18nError::KeyNotFound`.
-
-## Caching
-
-Translations are cached in-memory via `RwLock>`. Loaded lazily on first request per locale.
-
-## Frontend Integration
-
-The frontend uses `static/js/i18n.js` and `static/js/languageSelector.js` to:
-1. Detect the user's preferred language
-2. Load translations via `/api/i18n/locales/{code}`
-3. Apply translations to DOM elements
diff --git a/doc/important-delta-sync-implementation.md b/doc/important-delta-sync-implementation.md
deleted file mode 100644
index 374e0016..00000000
--- a/doc/important-delta-sync-implementation.md
+++ /dev/null
@@ -1,1152 +0,0 @@
-# 20 - Delta Sync Implementation
-
-Delta sync transfers only the modified parts of a file instead of the whole thing. Based on the rsync algorithm, it can save 90-99% bandwidth in common scenarios.
-
-**Status**: pending implementation
-**Priority**: medium
-**Estimated savings**: 10-100x less data transfer
-
-## Contents
-
-1. [Problem Statement](#problem-statement)
-2. [How It Works](#how-it-works)
-3. [Key Algorithms](#key-algorithms)
-4. [Proposed Architecture](#proposed-architecture)
-5. [Data Structures](#data-structures)
-6. [API Endpoints](#api-endpoints)
-7. [Step-by-Step Implementation](#step-by-step-implementation)
-8. [Integration with Existing System](#integration-with-existing-system)
-9. [Use Cases and Effectiveness](#use-cases-and-effectiveness)
-10. [Performance Considerations](#performance-considerations)
-11. [Testing](#testing)
-12. [Required Dependencies](#required-dependencies)
-
----
-
-## Key Benefits
-
-| Metric | Without Delta Sync | With Delta Sync |
-|---------|----------------|----------------|
-| Edit 1 line in 100MB | 100MB transferred | ~1KB transferred |
-| Sync time (slow connection) | 4+ minutes | <1 second |
-| Bandwidth consumption | 100% | 0.1-10% |
-
----
-
-## Problem Statement
-
-### Current scenario (no delta sync)
-
-```
-User has document.docx (50MB) on OxiCloud
- │
- ▼
-Downloads full file (50MB) ──────────────────────► 50MB ↓
- │
- ▼
-Edits one word
- │
- ▼
-Uploads full file again (50MB) ──────────────────► 50MB ↑
- │
- ▼
-TOTAL: 100MB transferred to change one word
-```
-
-### Target scenario (with delta sync)
-
-```
-User has document.docx (50MB) on OxiCloud
- │
- ▼
-Downloads full file (50MB) ──────────────────────► 50MB ↓ (first time)
- │
- ▼
-Edits one word
- │
- ▼
-Uploads ONLY modified blocks ───────────────────► ~50KB ↑
- │
- ▼
-TOTAL: 50.05MB (99.9% savings on upload)
-```
-
----
-
-## How It Works
-
-### Block (chunk) concept
-
-The file gets divided into fixed-size blocks (typically 4KB-64KB):
-
-```
-Original file (server):
-┌────────┬────────┬────────┬────────┬────────┐
-│ Bloque │ Bloque │ Bloque │ Bloque │ Bloque │
-│ 0 │ 1 │ 2 │ 3 │ 4 │
-│ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │
-│ │ │ │ │ │
-│ weak:A │ weak:B │ weak:C │ weak:D │ weak:E │
-│ sha:X1 │ sha:X2 │ sha:X3 │ sha:X4 │ sha:X5 │
-└────────┴────────┴────────┴────────┴────────┘
-
-Modified file (client):
-┌────────┬────────┬────────┬────────┬────────┐
-│ Bloque │ Bloque │ Bloque │ Bloque │ Bloque │
-│ 0 │ 1 │ 2 │ 3 │ 4 │
-│ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │
-│ │ │ │ │ │
-│ weak:A │ weak:B │ weak:F │ weak:D │ weak:E │ ← Block 2 changed
-│ sha:X1 │ sha:X2 │ sha:Y3 │ sha:X4 │ sha:X5 │
-└────────┴────────┴───▲────┴────────┴────────┘
- │
- ONLY THIS ONE GETS TRANSFERRED
-```
-
-### Sync flow
-
-```
-┌─────────────────────────────────────────────────────────────────────┐
-│ DELTA SYNC FLOW │
-├─────────────────────────────────────────────────────────────────────┤
-│ │
-│ CLIENT SERVER │
-│ │
-│ 1. Has modified 1. Has original file │
-│ file + block index │
-│ │
-│ 2. Requests signatures ─────────────► │
-│ GET /files/{id}/signatures │
-│ │
-│ ◄────────────── 3. Returns signature list │
-│ [(weak, strong), ...] │
-│ │
-│ 4. Compares local blocks │
-│ against server │
-│ signatures │
-│ │
-│ 5. Generates delta ──────────────────► │
-│ POST /files/{id}/delta │
-│ [References + New data] │
-│ │
-│ 6. Reconstructs file │
-│ by applying delta │
-│ │
-│ ◄────────────── 7. Confirms update │
-│ │
-└─────────────────────────────────────────────────────────────────────┘
-```
-
----
-
-## Key Algorithms
-
-### 1. Rolling Checksum (modified Adler-32)
-
-The rolling checksum computes the hash of a sliding window in O(1):
-
-```rust
-/// Rolling checksum para búsqueda rápida de bloques coincidentes
-/// Similar al usado por rsync (Adler-32 modificado)
-pub struct RollingChecksum {
- a: u32, // Suma simple de bytes
- b: u32, // Suma ponderada
- window_size: usize,
- buffer: VecDeque,
-}
-
-impl RollingChecksum {
- pub fn new(window_size: usize) -> Self {
- Self {
- a: 0,
- b: 0,
- window_size,
- buffer: VecDeque::with_capacity(window_size),
- }
- }
-
- /// Añadir un byte y calcular nuevo checksum
- /// Complejidad: O(1)
- pub fn roll(&mut self, new_byte: u8) -> u32 {
- if self.buffer.len() >= self.window_size {
- // Remover byte antiguo
- let old_byte = self.buffer.pop_front().unwrap() as u32;
- self.a = self.a.wrapping_sub(old_byte).wrapping_add(new_byte as u32);
- self.b = self.b.wrapping_sub(old_byte * self.window_size as u32)
- .wrapping_add(self.a);
- } else {
- // Ventana no llena todavía
- self.a = self.a.wrapping_add(new_byte as u32);
- self.b = self.b.wrapping_add(self.a);
- }
-
- self.buffer.push_back(new_byte);
- self.checksum()
- }
-
- /// Calcular checksum actual
- pub fn checksum(&self) -> u32 {
- (self.b << 16) | (self.a & 0xFFFF)
- }
-
- /// Reset para nuevo archivo
- pub fn reset(&mut self) {
- self.a = 0;
- self.b = 0;
- self.buffer.clear();
- }
-}
-```
-
-### 2. Block Signature
-
-Each block carries two signatures:
-- **Weak checksum** (32-bit) -- fast O(1) lookup
-- **Strong hash** (SHA-256) -- definitive verification
-
-```rust
-/// Firma de un bloque para identificación
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct BlockSignature {
- /// Índice del bloque en el archivo
- pub index: u32,
- /// Offset en bytes desde el inicio del archivo
- pub offset: u64,
- /// Tamaño del bloque (puede ser menor para el último)
- pub size: u32,
- /// Rolling checksum (32-bit) - búsqueda rápida
- pub weak_checksum: u32,
- /// SHA-256 hash (256-bit) - verificación definitiva
- pub strong_hash: [u8; 32],
-}
-
-/// Genera firmas para todos los bloques de un archivo
-pub fn generate_signatures(data: &[u8], block_size: usize) -> Vec {
- let mut signatures = Vec::new();
- let mut offset = 0u64;
- let mut index = 0u32;
-
- for chunk in data.chunks(block_size) {
- // Weak checksum (rolling)
- let weak = adler32_checksum(chunk);
-
- // Strong hash (SHA-256)
- let mut hasher = Sha256::new();
- hasher.update(chunk);
- let strong: [u8; 32] = hasher.finalize().into();
-
- signatures.push(BlockSignature {
- index,
- offset,
- size: chunk.len() as u32,
- weak_checksum: weak,
- strong_hash: strong,
- });
-
- offset += chunk.len() as u64;
- index += 1;
- }
-
- signatures
-}
-
-fn adler32_checksum(data: &[u8]) -> u32 {
- let mut a: u32 = 1;
- let mut b: u32 = 0;
-
- for &byte in data {
- a = (a + byte as u32) % 65521;
- b = (b + a) % 65521;
- }
-
- (b << 16) | a
-}
-```
-
-### 3. Delta Generation
-
-```rust
-/// Instrucción de delta
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub enum DeltaInstruction {
- /// Copiar bloque existente del archivo original
- Copy {
- /// Índice del bloque en el archivo original
- block_index: u32,
- },
- /// Insertar datos literales nuevos
- Literal {
- /// Datos nuevos a insertar
- data: Vec,
- },
-}
-
-/// Delta completo para actualizar un archivo
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct FileDelta {
- /// ID del archivo base
- pub base_file_id: String,
- /// Hash del archivo base (para verificación)
- pub base_file_hash: String,
- /// Nuevo tamaño del archivo
- pub new_size: u64,
- /// Instrucciones de delta
- pub instructions: Vec,
- /// Hash del archivo resultante (para verificación)
- pub result_hash: String,
-}
-
-/// Genera delta comparando archivo local con firmas remotas
-pub fn generate_delta(
- local_data: &[u8],
- remote_signatures: &[BlockSignature],
- block_size: usize,
-) -> FileDelta {
- // Crear índice de weak checksums para búsqueda O(1)
- let mut weak_index: HashMap> = HashMap::new();
- for sig in remote_signatures {
- weak_index.entry(sig.weak_checksum)
- .or_default()
- .push(sig);
- }
-
- let mut instructions = Vec::new();
- let mut rolling = RollingChecksum::new(block_size);
- let mut pos = 0;
- let mut literal_buffer = Vec::new();
-
- while pos < local_data.len() {
- // Calcular rolling checksum de la ventana actual
- let end = (pos + block_size).min(local_data.len());
- let window = &local_data[pos..end];
-
- let weak = if window.len() == block_size {
- rolling.reset();
- for &b in window {
- rolling.roll(b);
- }
- rolling.checksum()
- } else {
- adler32_checksum(window)
- };
-
- // Buscar coincidencia
- let mut found_match = false;
-
- if let Some(candidates) = weak_index.get(&weak) {
- // Verificar con strong hash
- let mut hasher = Sha256::new();
- hasher.update(window);
- let strong: [u8; 32] = hasher.finalize().into();
-
- for sig in candidates {
- if sig.strong_hash == strong && sig.size as usize == window.len() {
- // ¡Coincidencia encontrada!
-
- // Flush literal buffer si hay datos pendientes
- if !literal_buffer.is_empty() {
- instructions.push(DeltaInstruction::Literal {
- data: std::mem::take(&mut literal_buffer),
- });
- }
-
- // Añadir instrucción de copia
- instructions.push(DeltaInstruction::Copy {
- block_index: sig.index,
- });
-
- pos += window.len();
- found_match = true;
- break;
- }
- }
- }
-
- if !found_match {
- // No hay coincidencia, añadir byte a literal buffer
- literal_buffer.push(local_data[pos]);
- pos += 1;
- }
- }
-
- // Flush remaining literal buffer
- if !literal_buffer.is_empty() {
- instructions.push(DeltaInstruction::Literal {
- data: literal_buffer,
- });
- }
-
- // Calcular hash del resultado
- let mut hasher = Sha256::new();
- hasher.update(local_data);
- let result_hash = hex::encode(hasher.finalize());
-
- FileDelta {
- base_file_id: String::new(), // Se llena al enviar
- base_file_hash: String::new(), // Se llena al enviar
- new_size: local_data.len() as u64,
- instructions,
- result_hash,
- }
-}
-```
-
-### 4. Delta Application
-
-```rust
-/// Aplica delta a un archivo base para obtener el nuevo archivo
-pub fn apply_delta(
- base_data: &[u8],
- signatures: &[BlockSignature],
- delta: &FileDelta,
- block_size: usize,
-) -> Result, DeltaSyncError> {
- let mut result = Vec::with_capacity(delta.new_size as usize);
-
- for instruction in &delta.instructions {
- match instruction {
- DeltaInstruction::Copy { block_index } => {
- // Copiar bloque del archivo base
- let sig = signatures.get(*block_index as usize)
- .ok_or(DeltaSyncError::InvalidBlockIndex(*block_index))?;
-
- let start = sig.offset as usize;
- let end = start + sig.size as usize;
-
- if end > base_data.len() {
- return Err(DeltaSyncError::InvalidBlockRange);
- }
-
- result.extend_from_slice(&base_data[start..end]);
- }
- DeltaInstruction::Literal { data } => {
- // Insertar datos literales
- result.extend_from_slice(data);
- }
- }
- }
-
- // Verificar hash del resultado
- let mut hasher = Sha256::new();
- hasher.update(&result);
- let actual_hash = hex::encode(hasher.finalize());
-
- if actual_hash != delta.result_hash {
- return Err(DeltaSyncError::HashMismatch {
- expected: delta.result_hash.clone(),
- actual: actual_hash,
- });
- }
-
- Ok(result)
-}
-```
-
----
-
-## Proposed Architecture
-
-### File structure
-
-```
-src/
-├── infrastructure/
-│ └── services/
-│ ├── mod.rs # Añadir: pub mod delta_sync_service;
-│ └── delta_sync_service.rs # NUEVO: Servicio principal
-│
-├── interfaces/
-│ └── api/
-│ └── handlers/
-│ ├── mod.rs # Añadir: pub mod delta_sync_handler;
-│ └── delta_sync_handler.rs # NUEVO: Endpoints API
-│
-└── common/
- └── di.rs # Añadir: delta_sync_service a AppState
-```
-
-### Main service (delta_sync_service.rs)
-
-```rust
-//! Delta Sync Service - Sincronización eficiente por diferencias
-//!
-//! Implementa algoritmo similar a rsync para transferir solo
-//! las partes modificadas de los archivos.
-
-use std::collections::HashMap;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-use tokio::fs;
-use tokio::sync::RwLock;
-use sha2::{Sha256, Digest};
-use serde::{Deserialize, Serialize};
-
-/// Tamaño de bloque por defecto (16KB - buen balance)
-pub const DEFAULT_BLOCK_SIZE: usize = 16 * 1024;
-
-/// Tamaño mínimo de archivo para usar delta sync
-pub const MIN_DELTA_SYNC_SIZE: u64 = 64 * 1024; // 64KB
-
-/// Errores del servicio Delta Sync
-#[derive(Debug, thiserror::Error)]
-pub enum DeltaSyncError {
- #[error("Archivo no encontrado: {0}")]
- FileNotFound(String),
-
- #[error("Firmas no encontradas para archivo: {0}")]
- SignaturesNotFound(String),
-
- #[error("Índice de bloque inválido: {0}")]
- InvalidBlockIndex(u32),
-
- #[error("Rango de bloque inválido")]
- InvalidBlockRange,
-
- #[error("Hash no coincide: esperado {expected}, actual {actual}")]
- HashMismatch { expected: String, actual: String },
-
- #[error("Error de I/O: {0}")]
- IoError(#[from] std::io::Error),
-
- #[error("Error de serialización: {0}")]
- SerializationError(String),
-}
-
-/// Servicio de Delta Sync
-pub struct DeltaSyncService {
- /// Directorio para almacenar índices de firmas
- signatures_dir: PathBuf,
- /// Cache en memoria de firmas recientes
- signature_cache: Arc>>>,
- /// Tamaño de bloque configurado
- block_size: usize,
- /// Máximo de entradas en cache
- max_cache_entries: usize,
-}
-
-impl DeltaSyncService {
- pub fn new(storage_root: &Path) -> Self {
- Self {
- signatures_dir: storage_root.join(".delta_signatures"),
- signature_cache: Arc::new(RwLock::new(HashMap::new())),
- block_size: DEFAULT_BLOCK_SIZE,
- max_cache_entries: 1000,
- }
- }
-
- pub fn with_block_size(mut self, block_size: usize) -> Self {
- self.block_size = block_size;
- self
- }
-
- /// Inicializar servicio (crear directorios)
- pub async fn initialize(&self) -> std::io::Result<()> {
- fs::create_dir_all(&self.signatures_dir).await?;
- tracing::info!("Delta Sync service initialized with block size: {}KB",
- self.block_size / 1024);
- Ok(())
- }
-
- /// Generar y almacenar firmas para un archivo
- pub async fn index_file(
- &self,
- file_id: &str,
- file_path: &Path
- ) -> Result, DeltaSyncError> {
- let data = fs::read(file_path).await?;
-
- // No indexar archivos pequeños
- if data.len() < MIN_DELTA_SYNC_SIZE as usize {
- return Ok(Vec::new());
- }
-
- let signatures = generate_signatures(&data, self.block_size);
-
- // Guardar en disco
- let sig_path = self.signature_path(file_id);
- let sig_json = serde_json::to_vec(&signatures)
- .map_err(|e| DeltaSyncError::SerializationError(e.to_string()))?;
- fs::write(&sig_path, sig_json).await?;
-
- // Actualizar cache
- {
- let mut cache = self.signature_cache.write().await;
- if cache.len() >= self.max_cache_entries {
- // LRU simple: eliminar primera entrada
- if let Some(key) = cache.keys().next().cloned() {
- cache.remove(&key);
- }
- }
- cache.insert(file_id.to_string(), signatures.clone());
- }
-
- tracing::debug!("Indexed file {} with {} blocks", file_id, signatures.len());
- Ok(signatures)
- }
-
- /// Obtener firmas de un archivo
- pub async fn get_signatures(
- &self,
- file_id: &str
- ) -> Result, DeltaSyncError> {
- // Buscar en cache primero
- {
- let cache = self.signature_cache.read().await;
- if let Some(sigs) = cache.get(file_id) {
- return Ok(sigs.clone());
- }
- }
-
- // Cargar de disco
- let sig_path = self.signature_path(file_id);
- if !sig_path.exists() {
- return Err(DeltaSyncError::SignaturesNotFound(file_id.to_string()));
- }
-
- let sig_json = fs::read(&sig_path).await?;
- let signatures: Vec = serde_json::from_slice(&sig_json)
- .map_err(|e| DeltaSyncError::SerializationError(e.to_string()))?;
-
- // Actualizar cache
- {
- let mut cache = self.signature_cache.write().await;
- cache.insert(file_id.to_string(), signatures.clone());
- }
-
- Ok(signatures)
- }
-
- /// Aplicar delta a un archivo
- pub async fn apply_delta(
- &self,
- file_id: &str,
- base_path: &Path,
- delta: &FileDelta,
- ) -> Result, DeltaSyncError> {
- let base_data = fs::read(base_path).await?;
- let signatures = self.get_signatures(file_id).await?;
-
- apply_delta(&base_data, &signatures, delta, self.block_size)
- }
-
- /// Eliminar firmas de un archivo (cuando se borra)
- pub async fn remove_signatures(&self, file_id: &str) -> Result<(), DeltaSyncError> {
- // Eliminar de cache
- {
- let mut cache = self.signature_cache.write().await;
- cache.remove(file_id);
- }
-
- // Eliminar de disco
- let sig_path = self.signature_path(file_id);
- if sig_path.exists() {
- fs::remove_file(&sig_path).await?;
- }
-
- Ok(())
- }
-
- /// Estadísticas del servicio
- pub async fn get_stats(&self) -> DeltaSyncStats {
- let cache = self.signature_cache.read().await;
- DeltaSyncStats {
- cached_files: cache.len() as u64,
- block_size: self.block_size,
- }
- }
-
- fn signature_path(&self, file_id: &str) -> PathBuf {
- // Usar primeros 2 chars del ID para subdirectorio
- let prefix = &file_id[..2.min(file_id.len())];
- self.signatures_dir.join(prefix).join(format!("{}.sig", file_id))
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct DeltaSyncStats {
- pub cached_files: u64,
- pub block_size: usize,
-}
-```
-
----
-
-## API Endpoints
-
-### Handler (delta_sync_handler.rs)
-
-```rust
-use axum::{
- extract::{Path, State, Json},
- http::StatusCode,
- response::IntoResponse,
-};
-use crate::common::di::AppState;
-use crate::infrastructure::services::delta_sync_service::*;
-
-pub struct DeltaSyncHandler;
-
-impl DeltaSyncHandler {
- /// GET /api/files/{id}/signatures
- ///
- /// Obtiene las firmas de bloques de un archivo para calcular delta
- pub async fn get_signatures(
- State(state): State,
- Path(file_id): Path,
- ) -> impl IntoResponse {
- let delta_service = &state.core.delta_sync_service;
-
- match delta_service.get_signatures(&file_id).await {
- Ok(signatures) => {
- Json(SignaturesResponse {
- file_id,
- block_size: delta_service.block_size,
- block_count: signatures.len() as u32,
- signatures,
- }).into_response()
- }
- Err(DeltaSyncError::SignaturesNotFound(_)) => {
- // Archivo no indexado - cliente debe hacer upload completo
- (StatusCode::NOT_FOUND, Json(serde_json::json!({
- "error": "Signatures not found",
- "hint": "File not indexed for delta sync, use full upload"
- }))).into_response()
- }
- Err(e) => {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
- "error": e.to_string()
- }))).into_response()
- }
- }
- }
-
- /// POST /api/files/{id}/delta
- ///
- /// Aplica un delta para actualizar un archivo
- pub async fn apply_delta(
- State(state): State,
- Path(file_id): Path,
- Json(delta): Json,
- ) -> impl IntoResponse {
- let delta_service = &state.core.delta_sync_service;
- let file_service = &state.applications.file_service;
-
- // Obtener path del archivo actual
- let file = match file_service.get_file(&file_id).await {
- Ok(f) => f,
- Err(_) => {
- return (StatusCode::NOT_FOUND, Json(serde_json::json!({
- "error": "File not found"
- }))).into_response();
- }
- };
-
- // Aplicar delta
- let file_path = state.core.path_service.resolve_path(file.path());
- match delta_service.apply_delta(&file_id, &file_path, &delta).await {
- Ok(new_data) => {
- // Guardar nuevo contenido
- if let Err(e) = tokio::fs::write(&file_path, &new_data).await {
- return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
- "error": format!("Failed to write file: {}", e)
- }))).into_response();
- }
-
- // Re-indexar archivo
- if let Err(e) = delta_service.index_file(&file_id, &file_path).await {
- tracing::warn!("Failed to re-index file after delta: {}", e);
- }
-
- // Calcular estadísticas
- let delta_size: usize = delta.instructions.iter()
- .filter_map(|i| match i {
- DeltaInstruction::Literal { data } => Some(data.len()),
- _ => None,
- })
- .sum();
-
- Json(DeltaApplyResponse {
- success: true,
- new_size: new_data.len() as u64,
- delta_size: delta_size as u64,
- savings_percent: if new_data.len() > 0 {
- ((1.0 - (delta_size as f64 / new_data.len() as f64)) * 100.0) as u32
- } else { 0 },
- }).into_response()
- }
- Err(e) => {
- (StatusCode::BAD_REQUEST, Json(serde_json::json!({
- "error": e.to_string()
- }))).into_response()
- }
- }
- }
-
- /// POST /api/files/{id}/index
- ///
- /// Fuerza la indexación de un archivo para delta sync
- pub async fn index_file(
- State(state): State,
- Path(file_id): Path,
- ) -> impl IntoResponse {
- let delta_service = &state.core.delta_sync_service;
- let file_service = &state.applications.file_service;
-
- // Obtener path del archivo
- let file = match file_service.get_file(&file_id).await {
- Ok(f) => f,
- Err(_) => {
- return (StatusCode::NOT_FOUND, Json(serde_json::json!({
- "error": "File not found"
- }))).into_response();
- }
- };
-
- let file_path = state.core.path_service.resolve_path(file.path());
- match delta_service.index_file(&file_id, &file_path).await {
- Ok(signatures) => {
- Json(serde_json::json!({
- "success": true,
- "blocks_indexed": signatures.len()
- })).into_response()
- }
- Err(e) => {
- (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
- "error": e.to_string()
- }))).into_response()
- }
- }
- }
-
- /// GET /api/delta/stats
- ///
- /// Estadísticas del servicio delta sync
- pub async fn get_stats(
- State(state): State,
- ) -> impl IntoResponse {
- let delta_service = &state.core.delta_sync_service;
- Json(delta_service.get_stats().await)
- }
-}
-
-#[derive(Serialize)]
-struct SignaturesResponse {
- file_id: String,
- block_size: usize,
- block_count: u32,
- signatures: Vec,
-}
-
-#[derive(Serialize)]
-struct DeltaApplyResponse {
- success: bool,
- new_size: u64,
- delta_size: u64,
- savings_percent: u32,
-}
-```
-
-### Routes to add in routes.rs
-
-```rust
-// Delta Sync routes
-let delta_sync_router = Router::new()
- .route("/files/:id/signatures", get(DeltaSyncHandler::get_signatures))
- .route("/files/:id/delta", post(DeltaSyncHandler::apply_delta))
- .route("/files/:id/index", post(DeltaSyncHandler::index_file))
- .route("/delta/stats", get(DeltaSyncHandler::get_stats))
- .with_state(app_state.clone());
-
-// Añadir a router principal
-router = router.nest("/api", delta_sync_router);
-```
-
----
-
-## Integration with Existing System
-
-### 1. Auto-index on upload
-
-In `file_handler.rs`, after a successful upload:
-
-```rust
-// Después de guardar el archivo...
-
-// Indexar para delta sync (archivos >64KB)
-if total_size >= 64 * 1024 {
- let delta_service = &state.core.delta_sync_service;
- if let Err(e) = delta_service.index_file(&file.id, &file_path).await {
- tracing::warn!("Failed to index file for delta sync: {}", e);
- // No es error fatal, el archivo se subió correctamente
- }
-}
-```
-
-### 2. Clean up signatures on delete
-
-In `file_handler.rs`, when deleting a file:
-
-```rust
-// Limpiar firmas de delta sync
-let delta_service = &state.core.delta_sync_service;
-if let Err(e) = delta_service.remove_signatures(&id).await {
- tracing::warn!("Failed to remove delta signatures: {}", e);
-}
-```
-
-### 3. Integration with Dedup Service
-
-Delta sync and dedup are complementary:
-
-```
-┌──────────────────────────────────────────────────────────────┐
-│ UPLOAD WITH DELTA + DEDUP │
-├──────────────────────────────────────────────────────────────┤
-│ │
-│ 1. Client has modified file.txt │
-│ │
-│ 2. GET /files/{id}/signatures │
-│ → Server returns block signatures │
-│ │
-│ 3. Client computes delta locally │
-│ → Only 3 of 100 blocks changed │
-│ │
-│ 4. POST /files/{id}/delta │
-│ → Sends only the 3 new blocks │
-│ │
-│ 5. Server applies delta │
-│ → Reconstructs complete file │
-│ │
-│ 6. Server runs dedup on resulting file │
-│ → If another user has same content, it deduplicates │
-│ │
-│ RESULT: │
-│ ├── Delta sync: 97% less transfer │
-│ └── Dedup: 30-50% less storage │
-│ │
-└──────────────────────────────────────────────────────────────┘
-```
-
----
-
-## Use Cases and Effectiveness
-
-| File type | Scenario | Without Delta | With Delta | Savings |
-|-----------------|-----------|-----------|-----------|--------|
-| `.txt` / `.md` | Edit paragraph | 1MB | ~4KB | **99.6%** |
-| `.json` / `.xml` | Change value | 500KB | ~1KB | **99.8%** |
-| `.rs` / `.js` | Modify function | 100KB | ~2KB | **98%** |
-| `.docx` | Edit page | 5MB | ~100KB | **98%** |
-| `.xlsx` | Change cells | 2MB | ~50KB | **97.5%** |
-| `.pdf` | Edit text | 10MB | ~2MB | **80%** |
-| `.psd` | Edit layer | 100MB | ~5MB | **95%** |
-| `.zip` | Add file | 50MB | ~5MB | **90%** |
-| `.mp4` | Re-encode | 500MB | 450MB | **10%** |
-| `.jpg` | Edit image | 5MB | 4MB | **20%** |
-
-For highly compressed or re-encoded files, delta sync is less effective.
-
----
-
-## Performance Considerations
-
-### Optimal block size
-
-| Size | Pros | Cons | Best for |
-|--------|------|------|------------|
-| 4KB | More granular, better savings | More signature overhead | Small files |
-| 16KB | Good balance | - | **General use** |
-| 64KB | Less overhead | Less granular | Large files |
-| 256KB | Minimal overhead | Little savings | Very large files |
-
-### Memory
-
-```rust
-// Estimación de memoria por archivo indexado
-//
-// BlockSignature size ≈ 48 bytes (4 + 8 + 4 + 4 + 32 - con padding)
-//
-// Archivo 100MB con bloques de 16KB:
-// - 100MB / 16KB = 6,400 bloques
-// - 6,400 × 48 bytes = ~300KB de firmas
-//
-// Cache de 1000 archivos ≈ 300MB máximo
-```
-
-### CPU
-
-```rust
-// Operaciones costosas:
-//
-// 1. generate_signatures(): O(n) donde n = tamaño archivo
-// - SHA-256: ~500MB/s en CPU moderna
-// - Adler32: ~2GB/s
-//
-// 2. generate_delta(): O(n × m) peor caso, O(n) típico
-// - n = tamaño archivo nuevo
-// - m = número de bloques originales
-// - HashMap lookup: O(1) promedio
-//
-// 3. apply_delta(): O(n) donde n = tamaño resultado
-// - Mayormente copias de memoria
-```
-
----
-
-## Testing
-
-### Unit tests
-
-```rust
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_rolling_checksum() {
- let mut rc = RollingChecksum::new(4);
-
- // Alimentar bytes
- for b in b"test" {
- rc.roll(*b);
- }
- let checksum1 = rc.checksum();
-
- // Rolling: quitar 't', añadir 'X'
- rc.roll(b'X');
- let checksum2 = rc.checksum();
-
- // Checksums deben ser diferentes
- assert_ne!(checksum1, checksum2);
- }
-
- #[test]
- fn test_generate_signatures() {
- let data = b"Hello, World! This is a test file for delta sync.";
- let sigs = generate_signatures(data, 16);
-
- assert_eq!(sigs.len(), 4); // 50 bytes / 16 = 3.125 → 4 bloques
- assert_eq!(sigs[0].offset, 0);
- assert_eq!(sigs[1].offset, 16);
- }
-
- #[test]
- fn test_delta_identical_files() {
- let data = b"Hello, World!";
- let sigs = generate_signatures(data, 8);
- let delta = generate_delta(data, &sigs, 8);
-
- // Solo instrucciones Copy, sin Literal
- for instr in &delta.instructions {
- assert!(matches!(instr, DeltaInstruction::Copy { .. }));
- }
- }
-
- #[test]
- fn test_delta_small_change() {
- let original = b"Hello, World! This is original.";
- let modified = b"Hello, World! This is MODIFIED.";
-
- let sigs = generate_signatures(original, 8);
- let delta = generate_delta(modified, &sigs, 8);
-
- // Debería haber algunas instrucciones Copy y algunas Literal
- let copies = delta.instructions.iter()
- .filter(|i| matches!(i, DeltaInstruction::Copy { .. }))
- .count();
- let literals = delta.instructions.iter()
- .filter(|i| matches!(i, DeltaInstruction::Literal { .. }))
- .count();
-
- assert!(copies > 0, "Should reuse some blocks");
- assert!(literals > 0, "Should have some new data");
- }
-
- #[test]
- fn test_apply_delta_roundtrip() {
- let original = b"The quick brown fox jumps over the lazy dog.";
- let modified = b"The quick brown cat jumps over the lazy dog.";
-
- let sigs = generate_signatures(original, 8);
- let delta = generate_delta(modified, &sigs, 8);
- let reconstructed = apply_delta(original, &sigs, &delta, 8).unwrap();
-
- assert_eq!(reconstructed, modified);
- }
-}
-```
-
-### Integration tests
-
-```rust
-#[tokio::test]
-async fn test_delta_sync_service_workflow() {
- let temp_dir = tempfile::tempdir().unwrap();
- let service = DeltaSyncService::new(temp_dir.path());
- service.initialize().await.unwrap();
-
- // Crear archivo original
- let file_path = temp_dir.path().join("test.txt");
- tokio::fs::write(&file_path, b"Original content here").await.unwrap();
-
- // Indexar
- let sigs = service.index_file("file123", &file_path).await.unwrap();
- assert!(!sigs.is_empty());
-
- // Recuperar firmas
- let retrieved = service.get_signatures("file123").await.unwrap();
- assert_eq!(sigs.len(), retrieved.len());
-
- // Simular modificación y delta
- let modified = b"Modified content here!";
- let delta = generate_delta(modified, &sigs, service.block_size);
-
- // Aplicar delta
- let result = service.apply_delta("file123", &file_path, &delta).await.unwrap();
- assert_eq!(result, modified);
-}
-```
-
----
-
-## Required Dependencies
-
-Add to `Cargo.toml`:
-
-```toml
-[dependencies]
-# Ya existentes - verificar versiones
-sha2 = "0.10"
-hex = "0.4"
-
-# Nuevas dependencias para delta sync
-thiserror = "1.0" # Para errores tipados (probablemente ya existe)
-```
-
----
-
-## Implementation Checklist
-
-- [ ] Create `delta_sync_service.rs` with basic structures
-- [ ] Implement **RollingChecksum**
-- [ ] Implement **generate_signatures()**
-- [ ] Implement **generate_delta()**
-- [ ] Implement **apply_delta()**
-- [ ] Create handler and API endpoints
-- [ ] Integrate into DI (**AppState**)
-- [ ] Add routes in `routes.rs`
-- [ ] Integrate with upload (automatic indexing)
-- [ ] Integrate with delete (signature cleanup)
-- [ ] Unit tests
-- [ ] Integration tests
-- [ ] Document API endpoints
-- [ ] Metrics and logging
-
----
-
-## References
-
-- [rsync algorithm](https://rsync.samba.org/tech_report/)
-- [Rolling hash - Wikipedia](https://en.wikipedia.org/wiki/Rolling_hash)
-- [Adler-32 checksum](https://en.wikipedia.org/wiki/Adler-32)
-- [librsync](https://github.com/librsync/librsync)
diff --git a/doc/internal-architecture.md b/doc/internal-architecture.md
deleted file mode 100644
index a89bdcf3..00000000
--- a/doc/internal-architecture.md
+++ /dev/null
@@ -1,474 +0,0 @@
-# 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)
-```
diff --git a/doc/lto-optimizations.md b/doc/lto-optimizations.md
deleted file mode 100644
index 2267eb9d..00000000
--- a/doc/lto-optimizations.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# 05 - LTO Optimizations
-
-OxiCloud uses Link Time Optimization (LTO) to improve runtime performance. LTO allows the compiler to optimize across module boundaries during linking -- better inlining, dead code elimination, and more efficient binaries.
-
----
-
-## Implemented Optimizations
-
-### Release Profile
-```toml
-[profile.release]
-lto = "fat" # Full cross-module optimization
-codegen-units = 1 # Maximum optimization but slower compile time
-opt-level = 3 # Maximum optimization level
-panic = "abort" # Smaller binary size by removing panic unwinding
-strip = true # Removes debug symbols for smaller binary
-```
-
-### Development Profile
-```toml
-[profile.dev]
-opt-level = 1 # Light optimization for faster build time
-debug = true # Keep debug information for development
-```
-
-### Benchmark Profile
-```toml
-[profile.bench]
-lto = "fat" # Full optimization for benchmarks
-codegen-units = 1 # Maximum optimization
-opt-level = 3 # Maximum optimization level
-```
-
----
-
-## Performance Effects
-
-1. **Smaller binary size** -- unused code and metadata removed
-2. **Faster execution** -- better inlining and code optimizations
-3. **Reduced memory usage** -- more efficient code layout
-
----
-
-## LTO Options
-
-- **fat**: Full LTO across all crate boundaries. Maximum optimization, longest compile time.
-- **thin**: Faster LTO that trades some optimization for compile speed. Good for development.
-- **off**: No cross-module optimization.
-
----
-
-## Build Time Impact
-
-LTO increases compilation time. The tradeoff:
-
-- Development builds: minimal LTO (`opt-level = 1`) for faster iteration
-- Release builds: full LTO for maximum runtime performance
-- Benchmark builds: full LTO to measure actual optimized performance
-
----
-
-## Measuring Impact
-
-```bash
-# Run benchmarks with all optimizations
-cargo bench
-
-# Compare with non-optimized build (remove for comparison only)
-RUSTFLAGS="-C lto=off" cargo bench
-```
-
----
-
-## When to Adjust
-
-Consider changing these settings if:
-
-1. You need faster compile times during development
-2. You're experiencing unexpected runtime behavior
-3. You want to experiment with optimization vs. binary size tradeoffs
-
-The defaults work well for most cases.
diff --git a/doc/oidc-architecture.md b/doc/oidc-architecture.md
deleted file mode 100644
index 9539ee44..00000000
--- a/doc/oidc-architecture.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# 29 - OIDC Architecture
-
-OpenID Connect (OIDC) authentication follows the Authorization Code Flow. The system supports multiple identity providers (Authentik, Authelia, KeyCloak) through a single configurable integration point.
-
-## Architecture Diagram
-
-```
-┌─────────────────────────────────────────────────────────────────────────┐
-│ │
-│ IDENTITY PROVIDER │
-│ │
-│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
-│ │ │ │ │ │ │ │
-│ │ Authentik │ │ Authelia │ │ KeyCloak │ │
-│ │ │ │ │ │ │ │
-│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
-│ │ │ │ │
-└────────────┼──────────────────────┼──────────────────────┼─────────────┘
- │ │ │
- │ │ │
- │ │ │
- │ OIDC │
- │ │ │
- │ │ │
-┌────────────┼──────────────────────┼──────────────────────┼─────────────┐
-│ │ │ │ │
-│ ▼ ▼ ▼ │
-│ ┌───────────────────────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ OXICLOUD │ │
-│ │ │ │
-│ │ ┌───────────────┐ ┌───────────────┐ │ │
-│ │ │ │ │ │ │ │
-│ │ │ OidcService │◄────►│ AuthService │ │ │
-│ │ │ │ │ │ │ │
-│ │ └───────┬───────┘ └───────┬───────┘ │ │
-│ │ │ │ │ │
-│ │ ▼ ▼ │ │
-│ │ ┌───────────────────────────────────────────┐ │ │
-│ │ │ │ │ │
-│ │ │ AuthApplicationService │ │ │
-│ │ │ │ │ │
-│ │ └───────────────────┬───────────────────────┘ │ │
-│ │ │ │ │
-│ │ ▼ │ │
-│ │ ┌───────────────────────────────────────────┐ │ │
-│ │ │ │ │ │
-│ │ │ Auth Handler │ │ │
-│ │ │ │ │ │
-│ │ └───────────────────────────────────────────┘ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────────────────────────┘
- ▲
- │
- │ HTTP/HTTPS
- │
- │
-┌────────────────────────────────────────────────────────────────────────┐
-│ │
-│ WEB BROWSER │
-│ │
-│ ┌───────────────────────────────────────────────────────────────┐ │
-│ │ │ │
-│ │ User Interface │ │
-│ │ │ │
-│ │ ┌──────────────┐ ┌──────────────┐ │ │
-│ │ │ │ │ │ │ │
-│ │ │ Login.html │ │ auth.js │ │ │
-│ │ │ │ │ │ │ │
-│ │ └──────────────┘ └──────────────┘ │ │
-│ │ │ │
-│ └───────────────────────────────────────────────────────────────┘ │
-│ │
-└────────────────────────────────────────────────────────────────────────┘
-```
-
-## OIDC Authentication Flow
-
-The flow follows the standard Authorization Code Flow:
-
-1. **Authentication Start** -- the user clicks "Login with [Provider]" on the login page. The frontend generates a random state for CSRF protection and requests an authorization URL from the backend.
-
-2. **Redirect to Identity Provider** -- the backend generates and returns the authorization URL. The browser redirects the user to the provider's login page.
-
-3. **Authentication at the Provider** -- the user authenticates (password, 2FA, etc.). The provider redirects back with an authorization code.
-
-4. **Authorization Code Exchange** -- the frontend sends the authorization code to the backend. The backend exchanges it for access and ID tokens with the provider, then verifies the ID token and extracts user info.
-
-5. **User Creation/Retrieval** -- the backend looks up an existing user by the provider's external ID. If none exists and auto-provisioning is enabled, a new user is created. If disabled, an error is returned.
-
-6. **Session Token Generation** -- the backend generates its own access and refresh tokens for the user. These tokens authenticate subsequent API requests.
-
-7. **Response to Client** -- tokens and user info are returned to the frontend. The frontend stores them and redirects to the main page.
-
-## Main Components
-
-### OidcService
-
-Handles communication with OIDC providers:
-- Discovers provider OIDC endpoints
-- Generates authorization URLs
-- Exchanges authorization codes for tokens
-- Verifies tokens and extracts user info
-
-### AuthApplicationService
-
-Coordinates the authentication process:
-- Acts as interface between the API layer and domain services
-- Manages user creation/retrieval
-- Coordinates access token generation
-
-### Auth Handler
-
-Exposes HTTP endpoints for the OIDC auth flow:
-- `GET /api/auth/oidc/providers` -- lists available OIDC providers
-- `GET /api/auth/oidc/authorize` -- generates an authorization URL for the OIDC provider
-- `GET /api/auth/oidc/callback` -- receives the redirect from the provider with the authorization code
-- `POST /api/auth/oidc/exchange` -- exchanges the authorization code for session tokens
-
-### Frontend (login.html + auth.js)
-
-Handles the client-side of the auth flow:
-- Shows SSO button for the configured OIDC provider in `login.html`
-- Initiates the authentication flow via `auth.js`
-- Handles the return redirect from the provider
-- Processes and stores session tokens
-
-## Provider Configuration
-
-One OIDC provider is configured per instance via environment variables prefixed with **OXICLOUD_OIDC_***:
-
-1. **Single provider** per instance.
-2. **Environment variables**: **OXICLOUD_OIDC_ENABLED**, **OXICLOUD_OIDC_ISSUER_URL**, **OXICLOUD_OIDC_CLIENT_ID**, **OXICLOUD_OIDC_CLIENT_SECRET**, etc.
-3. **Auto-provisioning**: users can be created automatically on first OIDC login (**OXICLOUD_OIDC_AUTO_PROVISION**).
-4. **Role mapping**: admin groups are configured via **OXICLOUD_OIDC_ADMIN_GROUPS**.
-
-See `oidc-config-examples.md` for provider-specific configuration examples.
-
-## Security
-
-The OIDC implementation includes several security measures:
-
-1. **CSRF protection** -- random state parameter prevents CSRF attacks.
-2. **Token validation** -- JWT signatures and expiration are verified.
-3. **Authorization Code Flow** -- more secure than the implicit flow.
-4. **HTTPS** -- required for all OIDC communications.
-5. **Client secrets** -- stored securely, never exposed to the frontend.
diff --git a/doc/oidc-config-examples.md b/doc/oidc-config-examples.md
deleted file mode 100644
index 788bdfd8..00000000
--- a/doc/oidc-config-examples.md
+++ /dev/null
@@ -1,215 +0,0 @@
-# 31 - OIDC Config Examples
-
-Configuration examples for integrating with different OIDC (OpenID Connect) providers. One OIDC provider per instance.
-
-## Table of Contents
-
-1. [General OIDC Configuration](#general-oidc-configuration)
-2. [Authentik](#authentik)
-3. [Authelia](#authelia)
-4. [KeyCloak](#keycloak)
-5. [Troubleshooting](#troubleshooting)
-
-## General OIDC Configuration
-
-To enable OIDC, set these environment variables:
-
-```bash
-# Enable OIDC
-OXICLOUD_OIDC_ENABLED=true
-
-# OIDC provider configuration
-OXICLOUD_OIDC_PROVIDER_NAME="Display Name"
-OXICLOUD_OIDC_ISSUER_URL="https://provider.example.com/realms/your-realm"
-OXICLOUD_OIDC_CLIENT_ID="your-client-id"
-OXICLOUD_OIDC_CLIENT_SECRET="your-client-secret"
-OXICLOUD_OIDC_REDIRECT_URI="https://your-oxicloud.example.com/api/auth/oidc/callback"
-OXICLOUD_OIDC_SCOPES="openid profile email"
-OXICLOUD_OIDC_FRONTEND_URL="https://your-oxicloud.example.com"
-OXICLOUD_OIDC_AUTO_PROVISION="true"
-OXICLOUD_OIDC_ADMIN_GROUPS="admin-group"
-OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN="false"
-```
-
-## Authentik
-
-[Authentik](https://goauthentik.io/) is an open-source identity platform providing authentication, authorization, and user management.
-
-### 1. Create an Application in Authentik
-
-1. Log into the Authentik admin panel
-2. Go to "Applications" -> "Create"
-3. Enter a name for the application (e.g. "OxiCloud")
-4. Select "OAuth2/OpenID Provider" as the provider type
-5. In the OAuth2 configuration:
- - **Redirect URI/Callback URL**: `https://your-oxicloud.example.com/api/auth/oidc/callback`
- - **Client Type**: Confidential
- - **Client ID**: auto-generated (note it down)
- - **Client Secret**: auto-generated (note it down)
- - **Scopes**: openid, email, profile
-6. In the UI configuration:
- - **Launch URL**: `https://your-oxicloud.example.com/`
- - **Icon**: optional
-
-### 2. Configure for Authentik
-
-```yaml
-# docker-compose.yml
-version: '3'
-services:
- oxicloud:
- image: diocrafts/oxicloud:latest
- environment:
- OXICLOUD_OIDC_ENABLED: "true"
- OXICLOUD_OIDC_PROVIDER_NAME: "Authentik"
- OXICLOUD_OIDC_ISSUER_URL: "https://authentik.example.com/application/o/oxicloud"
- OXICLOUD_OIDC_CLIENT_ID: "your-authentik-client-id"
- OXICLOUD_OIDC_CLIENT_SECRET: "your-authentik-client-secret"
- OXICLOUD_OIDC_REDIRECT_URI: "https://oxicloud.example.com/api/auth/oidc/callback"
- OXICLOUD_OIDC_SCOPES: "openid profile email"
- OXICLOUD_OIDC_FRONTEND_URL: "https://oxicloud.example.com"
- OXICLOUD_OIDC_AUTO_PROVISION: "true"
- OXICLOUD_OIDC_ADMIN_GROUPS: ""
- OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN: "false"
- ports:
- - "8086:8086"
- volumes:
- - ./storage:/app/storage
-```
-
-## Authelia
-
-[Authelia](https://www.authelia.com/) is an open-source multi-factor authentication solution.
-
-### 1. Configure Authelia
-
-Edit your Authelia configuration (`configuration.yml`):
-
-```yaml
-identity_providers:
- oidc:
- hmac_secret: your-secure-secret # Change to a secure random value
- issuer_private_key: /config/private.pem # Path to your private key
- cors:
- endpoints: ['authorization', 'token', 'revocation', 'introspection']
- allowed_origins:
- - https://oxicloud.example.com
- clients:
- - id: oxicloud
- description: OxiCloud
- secret: your-secure-client-secret # Change this
- public: false
- authorization_policy: two_factor
- redirect_uris:
- - https://oxicloud.example.com/api/auth/oidc/callback
- scopes: ['openid', 'profile', 'email', 'groups']
- userinfo_signing_algorithm: none
-```
-
-### 2. Configure for Authelia
-
-```yaml
-# docker-compose.yml
-version: '3'
-services:
- oxicloud:
- image: diocrafts/oxicloud:latest
- environment:
- OXICLOUD_OIDC_ENABLED: "true"
- OXICLOUD_OIDC_PROVIDER_NAME: "Authelia"
- OXICLOUD_OIDC_ISSUER_URL: "https://authelia.example.com"
- OXICLOUD_OIDC_CLIENT_ID: "oxicloud"
- OXICLOUD_OIDC_CLIENT_SECRET: "your-secure-client-secret"
- OXICLOUD_OIDC_REDIRECT_URI: "https://oxicloud.example.com/api/auth/oidc/callback"
- OXICLOUD_OIDC_SCOPES: "openid profile email groups"
- OXICLOUD_OIDC_FRONTEND_URL: "https://oxicloud.example.com"
- OXICLOUD_OIDC_AUTO_PROVISION: "true"
- OXICLOUD_OIDC_ADMIN_GROUPS: ""
- OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN: "false"
- ports:
- - "8086:8086"
- volumes:
- - ./storage:/app/storage
-```
-
-## KeyCloak
-
-[KeyCloak](https://www.keycloak.org/) is an open-source identity and access management solution.
-
-### 1. Create a Client in KeyCloak
-
-1. Log into the KeyCloak admin console
-2. Select your Realm
-3. Go to "Clients" -> "Create"
-4. Fill in the form:
- - **Client ID**: `oxicloud`
- - **Client Protocol**: `openid-connect`
- - **Root URL**: `https://oxicloud.example.com`
-5. In the client configuration:
- - **Access Type**: `confidential`
- - **Valid Redirect URIs**: `https://oxicloud.example.com/api/auth/oidc/callback`
- - **Web Origins**: `https://oxicloud.example.com` (or `+` to allow all origins)
-6. Save the configuration
-7. Go to the "Credentials" tab and copy the generated "Secret"
-
-### 2. Configure for KeyCloak
-
-```yaml
-# docker-compose.yml
-version: '3'
-services:
- oxicloud:
- image: diocrafts/oxicloud:latest
- environment:
- OXICLOUD_OIDC_ENABLED: "true"
- OXICLOUD_OIDC_PROVIDER_NAME: "KeyCloak"
- OXICLOUD_OIDC_ISSUER_URL: "https://keycloak.example.com/realms/your-realm"
- OXICLOUD_OIDC_CLIENT_ID: "oxicloud"
- OXICLOUD_OIDC_CLIENT_SECRET: "your-keycloak-client-secret"
- OXICLOUD_OIDC_REDIRECT_URI: "https://oxicloud.example.com/api/auth/oidc/callback"
- OXICLOUD_OIDC_SCOPES: "openid profile email"
- OXICLOUD_OIDC_FRONTEND_URL: "https://oxicloud.example.com"
- OXICLOUD_OIDC_AUTO_PROVISION: "true"
- OXICLOUD_OIDC_ADMIN_GROUPS: "oxicloud-admins"
- OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN: "false"
- ports:
- - "8086:8086"
- volumes:
- - ./storage:/app/storage
-```
-
-## Troubleshooting
-
-### Error: "Failed to discover OIDC provider"
-
-The backend cannot reach the provider's discovery endpoint.
-
-**Fixes:**
-1. Verify the discovery URL is correct
-2. Check that the backend can reach the URL (firewalls, DNS, etc.)
-3. If using a self-signed certificate, configure the appropriate trust
-
-### Error: "Invalid redirect URI"
-
-The OIDC provider is rejecting the redirect URI.
-
-**Fixes:**
-1. Make sure the redirect URI configured in the backend matches exactly what is registered in the provider
-2. Check for protocol differences (http vs https), port, or path mismatches
-
-### Error: "User does not exist and auto-creation is disabled"
-
-**Fixes:**
-1. Enable auto-provisioning: `OXICLOUD_OIDC_AUTO_PROVISION="true"`
-2. Or manually create the user before attempting OIDC login
-
-### Error: "Could not extract user ID from claim"
-
-The backend cannot find the user ID attribute in the token claims.
-
-**Fixes:**
-1. Verify the provider returns the `sub` claim in tokens
-2. Make sure scopes in **OXICLOUD_OIDC_SCOPES** include `openid`
-3. Configure the provider to include the required claims in tokens
-
-See `oidc-architecture.md` and `oidc-integration.md` for deeper technical details.
diff --git a/doc/oidc-integration.md b/doc/oidc-integration.md
deleted file mode 100644
index cc21c649..00000000
--- a/doc/oidc-integration.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# 30 - OIDC Integration
-
-OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0. It lets clients verify user identity based on authentication performed by an authorization server and obtain basic profile information. Adding OIDC enables SSO with providers like Authentik, Authelia, and KeyCloak.
-
-What it gives us:
-1. Users authenticate with their existing IdP credentials
-2. No need for separate username/password management
-3. Modern auth best practices baked in
-4. Seamless experience for users already on SSO
-
-## OIDC Configuration
-
-OIDC is configured separately from **AuthConfig** via **OidcConfig** in `src/common/config.rs`. This is a single-provider model -- one OIDC provider per instance:
-
-```rust
-/// OpenID Connect (OIDC) configuration
-pub struct OidcConfig {
- pub enabled: bool, // Whether OIDC is enabled
- pub issuer_url: String, // OIDC Issuer URL
- pub client_id: String, // OIDC Client ID
- pub client_secret: String, // OIDC Client Secret
- pub redirect_uri: String, // Redirect URI (default: http://localhost:8086/api/auth/oidc/callback)
- pub scopes: String, // Scopes to request (default: "openid profile email")
- pub frontend_url: String, // Frontend URL for post-login redirect
- pub auto_provision: bool, // Auto-create users on first login (JIT provisioning)
- pub admin_groups: String, // Comma-separated OIDC groups that map to admin role
- pub disable_password_login: bool, // Disable password-based login entirely
- pub provider_name: String, // Display name (default: "SSO")
-}
-```
-
-Environment variables use the **OXICLOUD_OIDC_*** prefix:
-
-```bash
-OXICLOUD_OIDC_ENABLED=true
-OXICLOUD_OIDC_ISSUER_URL="https://authentik.example.com/application/o/oxicloud/"
-OXICLOUD_OIDC_CLIENT_ID="your-client-id"
-OXICLOUD_OIDC_CLIENT_SECRET="your-client-secret"
-OXICLOUD_OIDC_REDIRECT_URI="https://oxicloud.example.com/api/auth/oidc/callback"
-OXICLOUD_OIDC_SCOPES="openid profile email"
-OXICLOUD_OIDC_FRONTEND_URL="https://oxicloud.example.com"
-OXICLOUD_OIDC_AUTO_PROVISION=true
-OXICLOUD_OIDC_ADMIN_GROUPS="oxicloud-admins"
-OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=false
-OXICLOUD_OIDC_PROVIDER_NAME="Authentik"
-```
-
-## OIDC Service Implementation
-
-The OIDC service lives in the infrastructure layer at `src/infrastructure/services/oidc_service.rs` and implements the **OidcServicePort** trait defined in `src/application/ports/auth_ports.rs`:
-
-```rust
-// src/application/ports/auth_ports.rs — Port trait
-#[async_trait]
-pub trait OidcServicePort: Send + Sync + 'static {
- fn enabled(&self) -> bool;
- fn provider_name(&self) -> &str;
- fn generate_auth_url(&self, state: &str) -> Result;
- async fn exchange_code(&self, code: &str) -> Result;
- async fn get_user_info(&self, token_set: &OidcTokenSet) -> Result;
-}
-
-// src/infrastructure/services/oidc_service.rs — Implementation
-pub struct OidcService {
- config: OidcConfig,
- http_client: reqwest::Client,
- // Discovery metadata cached after initialization
-}
-
-impl OidcService {
- pub async fn new(config: OidcConfig) -> Result