Commit Graph

350 Commits

Author SHA1 Message Date
Dionisio ed433df2af perf(zip): eliminate N+1 queries with ltree bulk subtree fetch
Replace BFS traversal that issued 2 SQL queries per folder (list_files +
list_folders) with 2 total queries using PostgreSQL ltree <@ operator:

1. list_subtree_folders: single GiST-indexed scan for all folders
2. list_files_in_subtree: single GiST-indexed join for all files

Files are grouped by folder_id in a HashMap, then iterated in directory
order (folders pre-sorted by path from SQL).

Changes across 4 architecture layers:
- Domain: FolderRepository::list_subtree_folders (default impl)
- Application ports: FolderUseCase, FileRetrievalUseCase, FileReadPort
- Application services: FolderService, FileRetrievalService passthroughs
- Infrastructure: PG implementations + ZipService rewrite

Query count: O(N) → O(1). Latency for 100-folder tree: ~200 round-trips → 3.
2026-02-24 12:18:38 +01:00
Dionisio 4fc0ab8831 perf: replace buffered decompress_stream with async-compression streaming
- Add async-compression dependency with tokio+gzip features
- Rewrite decompress_stream to use true streaming pipeline:
  Stream<Bytes> → StreamReader → BufReader(64KB) → GzipDecoder → ReaderStream(64KB)
- Memory usage drops from ~4GB (1GB compressed file) to constant ~128KB
- Eliminates OOM risk on large compressed file decompression
2026-02-24 11:47:21 +01:00
Dionisio b628a3a166 perf(issue#4): stream dedup verify_integrity & garbage_collect
- Replace fetch_all() with fetch() streaming cursor in verify_integrity
  so memory stays O(batch=16) instead of O(total_blobs)
- Replace fetch_all() with fetch() streaming cursor in garbage_collect
  so memory stays O(1) instead of O(orphans)
- Add TryStreamExt import for try_next() on cursors

Eliminates OOM risk with millions of blobs — RAM usage is now constant
regardless of table size.
2026-02-24 10:45:38 +01:00
Dionisio 677b3eafa9 perf: use adaptive filter for thumbnails (Triangle/CatmullRom instead of Lanczos3)
- Icon (150px): Triangle filter (~5x faster)
- Preview (400px): CatmullRom filter (~2.5x faster)
- Large (800px): CatmullRom filter (~2.5x faster)
- Quality difference is imperceptible at these resolutions
2026-02-24 00:44:27 +01:00
Dionisio 12c6914d5d perf(#28): parallelize verify_integrity() with buffer_unordered(16)
Replace sequential blob-by-blob SHA-256 verification with
futures::stream::buffer_unordered(16) to hash up to 16 blobs
concurrently.

Each hash_file() already runs on spawn_blocking, so 16 concurrent
verifications saturate both disk I/O queue and CPU cores.

Before: 10K blobs × 13ms = ~130s (NVMe) — 1 core, 1 I/O in flight
After:  10K blobs / 16 concurrency = ~8s — 16 cores, 16 I/O in flight
2026-02-23 23:43:59 +01:00
Dionisio ab0c4476b3 perf: fix thundering herd in ThumbnailService with moka entry()
get_thumbnail() used a check-then-act pattern (get → miss → generate →
insert) that allowed N concurrent requests for the same thumbnail to
each trigger independent CPU-heavy generation (image decode + Lanczos3
resize + WebP encode, 50-500ms each).

Replace with moka's entry().or_insert_with() which guarantees only ONE
init closure runs per key; all concurrent callers coalesce and await
the same computation:
- 90% less CPU under concurrent thumbnail requests
- 90% less peak RAM (1 buffer instead of N)
- 90% fewer redundant disk writes
- Removed dead thumbnail_exists() method (no callers)
2026-02-23 23:22:54 +01:00
Dionisio a162aafd43 perf: replace sync zip crate with async_zip in ZipService
ZipWriter<std::fs::File> performed every write_all() as a blocking
write(2) syscall on the Tokio worker thread, sequestering it for
10-100ms per file (0.8-12s for a 100-file ZIP).

Replace with async_zip::ZipFileWriter backed by a buffered
tokio::fs::File:
- All I/O (headers, deflate chunks, central directory) is fully async
- 256 KB BufWriter minimises syscall count
- Zero Tokio worker blocking during ZIP creation
- Streaming per-chunk writes keep RAM O(1) regardless of archive size
- Removed dead From<zip::result::ZipError> for DomainError impl
- zip crate retained for batch_operations.rs (separate concern)
2026-02-23 23:11:55 +01:00
Dionisio 958836e96b perf: replace blocking std::fs calls with tokio::fs::metadata in PathService
file_exists(), directory_exists(), and ensure_directory() were using
synchronous Path::exists() + is_file()/is_dir() which each perform two
blocking stat(2) syscalls on the Tokio worker thread (0.2-4ms total).

Replace with a single tokio::fs::metadata().await call per method:
- Async: worker thread is never blocked
- 1 syscall instead of 2: metadata() returns file type in one stat(2)
- Proper error propagation for I/O errors (not just silent false)
2026-02-23 22:56:20 +01:00
Dionisio b5652b029d perf: offload MD5 checksum to spawn_blocking in chunked uploads
md5::compute(&data) is CPU-bound (~1.2ms per 5MB chunk) and was
blocking the Tokio worker thread. Move it to the blocking thread-pool
via spawn_blocking so the async worker is freed in ~5µs. Bytes::clone
is O(1) (Arc increment) so no extra copy overhead.
2026-02-23 22:49:34 +01:00
Dionisio 1da284a841 perf: move 512KB I/O buffer from stack to heap in chunked upload assembly
- Async Future size drops from ~525KB to ~5KB
- Eliminates stack overflow risk and reduces work-stealing copy cost
2026-02-23 22:41:50 +01:00
Dionisio 41b5500b77 perf: move persist_progress disk I/O outside write lock in chunked uploads
- Write lock now held only for RAM updates (~microseconds instead of ~ms)
- Bitmask built under lock, written to disk after lock release
- Concurrent uploads across all sessions no longer blocked by disk I/O
- Under 10 concurrent sessions: lock wait drops from ~50ms to ~10µs
2026-02-23 22:32:14 +01:00
Diocrafts 85908311dc perf: findings 6.1, 6.2, 2.6 — async Argon2, moka cache, full streaming migration
- 6.1: PasswordHasherPort now async_trait with spawn_blocking for Argon2
- 6.2: OIDC pending maps migrated from std::sync::Mutex to moka::sync::Cache with TTL
- 2.6: All file download paths migrated to 64KB streaming (get_file_stream / read_blob_stream)
  - WOPI, dedup, batch ZIP, file_retrieval_service consumers migrated
  - WebDAV COPY uses zero-copy dedup (copy_file)
  - Removed dead code: get_file_content, get_file_mmap, read_blob, read_blob_bytes
    from traits, impls, stubs, and mocks (18 files touched)
2026-02-23 00:51:46 +01:00
Diocrafts 92e0364a60 fix: OOM protection, lock-free thumbnail cache, OIDC JWKS TTL
- Streaming WebDAV PUT: body spooled to tempfile with incremental SHA-256,
  peak RAM ~64KB regardless of file size (Solution 2)
- RequestBodyLimitLayer (1MB) on CalDAV/CardDAV routers (Solution 3)
- All body::to_bytes(body, usize::MAX) replaced with explicit limits:
  PROPFIND/PROPPATCH/LOCK → 1MB, MKCOL → 4KB
- Added AppError::payload_too_large (HTTP 413)
- Added max_upload_size to StorageConfig (default 10GB, env override)
- New streaming update chain: FileWritePort::update_file_content_from_temp
  → FileUploadUseCase::update_file_streaming
- ThumbnailService: migrated from RwLock<LruCache> to moka::future::Cache
  with weight-based eviction — eliminates lock contention on read hot-path
- OIDC: discovery + JWKS caches now expire after 1 hour (Cached<T> wrapper)
  so IdP key rotation no longer requires server restart
2026-02-22 23:28:03 +01:00
Diocrafts 5b4cd30e2b fix(zip): stream ZIP to temp file instead of loading entire archive into RAM
Solution C - Hybrid temp-file streaming:
- ZipPort trait now returns NamedTempFile instead of Vec<u8>
- ZipService writes to a temp file via ZipWriter<std::fs::File> (O(1) RAM)
- Files are read in 64KB stream chunks via get_file_stream() instead of get_file_content()
- HTTP response streams the temp file via ReaderStream (never loads full ZIP in memory)
- Temp file auto-deleted on drop after response completes
- Removed dead imports (HeaderName, HeaderValue, Cursor, Read)
2026-02-22 22:29:07 +01:00
Diocrafts fd9e509648 perf: implement findings #2, #3, #17 from architecture audit
- Finding #2: Replace Mutex<HashMap> with moka::sync::Cache in file_blob_read_repository (10K cap, 30s TTI)
- Finding #3: Add chunked upload persistence with session.json + progress.bin bitmask for crash recovery
- Finding #17: Remove manual gzip compression, delegate entirely to tower-http CompressionLayer
- Remove dead code: StubCompressionPort, GzipCompressionService re-export, duplicate response structs
- All 114 tests passing
2026-02-22 14:12:53 +01:00
George Wu 52a47d4b33 Add email_verified check for OIDC login
- Parse email_verified from ID token and UserInfo endpoint
- Reject OIDC login if email is present and not verified
- Only applies when email is in OIDC claims (not required otherwise)
2026-02-21 11:51:48 -08:00
Jan Wiebe 4e2c9d2592 feat(wopi): add WOPI protocol support for collaborative editing
Implement the Web Application Open Platform Interface (WOPI) protocol
to enable collaborative document editing with Collabora Online and
OnlyOffice through OxiCloud.

Backend:
- WOPI token service with HMAC-SHA256 signed access tokens
- WOPI lock service with in-memory lock management and expiry
- WOPI discovery service for auto-detecting editor capabilities
- WOPI HTTP handler: CheckFileInfo, GetFile, PutFile, Lock/Unlock
- File entity extended with owner_id for WOPI file-info responses
- Configuration via WOPI_* environment variables
- Services wired through DI in AppState

Frontend:
- WOPI editor component with modal and new-tab viewing modes
- Context menu integration for opening files in online editors
- Inline viewer integration for document preview

Infrastructure:
- Docker Compose file for local Collabora/OnlyOffice dev setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:39:27 +01:00
Dionisio b7bd656a43 perf: fix issues #1,#2,#3,#13 from performance audit
- #1  image_transcode: dedicated rayon pool + moka cache (no tokio blocking)
- #2  ltree materialized paths: eliminate N+1 folder/file queries
- #3  compute_content_hash: read blob_hash column instead of loading file into RAM
- #13 event delegation + DocumentFragment: ~15 delegated listeners replace ~19k per-item listeners

Backend: 82/82 tests pass, cargo check clean.
Frontend: ui.js and app.js syntax-validated via Node.js.
2026-02-16 13:10:44 +01:00
Dionisio 1527223d62 refactor: use typed Vec<Jwk> in JWKS parsing, remove double deserialize 2026-02-15 18:19:11 +01:00
Dionisio Pozo b59ff75489 Merge pull request #115 from gbw/feature/es256-jwks-support
Merged: ES256 JWKS support for OIDC
2026-02-15 18:09:36 +01:00
Dionisio 0be7ef8c0b style: fix clippy collapsible_if + cargo fmt 2026-02-15 18:04:32 +01:00
Dionisio 1ed20f425f perf: Phase 4+5 optimizations — uploads 10x, downloads 2x, concurrent 2x. moka cache, 512KB buffers, remove sync_all, hash-on-write, preloaded queries, bench.sh v3, gitignore storage/. 500MB upload 12.6s->1.3s (392MB/s). RSS 69-113MB, 0 swap. 2026-02-15 17:56:47 +01:00
George Wu 77c0dd5907 Add ES256 JWKS support for OIDC
- Simplified oidc_service.rs to use jsonwebtoken's DecodingKey::from_jwk()
- Automatically handles key type detection (RSA/EC) and algorithm detection
- Supports RS256, RS384, RS512, ES256, and ES384 algorithms
- Much cleaner code by leveraging library's built-in JWK parsing
2026-02-14 13:24:25 -08:00
Dionisio fac0b5e77b fix: critical bugs from deep audit
- Fix copy_files() data loss: implement real copy_file across full stack
  (FileWritePort, FileManagementUseCase, stubs, service, repository with
  atomic CTE + dedup ref_count increment, batch_operations caller)
- Fix plaintext password in replace_default_admin: hash password via
  PasswordHasherPort before User::new()
- Fix CalendarService hardcoded user_id: unify CalendarUseCase trait with
  explicit user_id parameter on all methods, remove zombie _for_user
  duplicates and hardcoded 'current_user_id', update 20 CalDAV handler
  call sites
- Previous session: migrate DedupService to PostgreSQL (storage.blobs),
  atomic CTEs with compensation for file/folder repository operations
2026-02-14 20:22:19 +01:00
Dionisio 3179e1dd91 quick fix 2026-02-14 19:30:49 +01:00
Dionisio bc01840fa4 chore: remove dead code from blob storage migration
Remove legacy abstractions that are no longer used after the
100% blob storage model migration (#113):

- IdMappingPort trait from application/ports/outbound.rs
- storage_mediator.rs module (StorageMediator trait + impls)
- StorageMediator impl from PathService
- write_behind_cache.rs (FS-based, incompatible with blob model)

The WriteBehindCachePort trait and Optional fields in services
are preserved for potential future blob-compatible caching.

-968 lines of dead code removed. Build clean, RC=0.
2026-02-14 18:10:37 +01:00
Dionisio 3c7c16f07e feat(#113): 100% blob storage model — PostgreSQL metadata + DedupService blobs
BREAKING CHANGE: Storage model completely rewritten. All file/folder
metadata now lives in PostgreSQL (storage schema). File content stored
as content-addressable blobs via DedupService. Filesystem directories
are no longer used for user storage.

New components:
- storage.folders / storage.files / storage.trash_items (PG schema)
- FolderDbRepository: virtual folders backed by PG
- FileBlobReadRepository: file reads via PG metadata + dedup blobs
- FileBlobWriteRepository: file writes via PG metadata + dedup blobs
- TrashDbRepository: soft-delete trash using is_trashed flags

Removed legacy FS components (~5500 lines deleted):
- FolderFsRepository, FileFsReadRepository, FileFsWriteRepository
- CompositeFileRepository, ParallelFileProcessor
- IdMappingService, IdMappingOptimizer, FileMetadataCache
- BufferPool, FileSystemUtils, RepositoryErrors
- TrashFsRepository, FolderFsRepositoryTrash

DI rewired: build_app_state() now requires PgPool (no FS fallback).
FileUploadService.new_with_read() and FileRetrievalService.new_with_cache()
constructors added for blob model (no write-behind needed).

Closes #113
2026-02-14 17:54:25 +01:00
Dionisio 4c98c5a657 style: apply cargo fmt to entire codebase
Standardize code formatting across all 173 Rust source files
using rustfmt. No functional changes - purely cosmetic.

This establishes a consistent code style baseline for the
project going forward.
2026-02-14 01:29:34 +01:00
Dionisio 67137a3ef2 refactor: apply clippy auto-fixes (162 warnings resolved)
- Fix needless borrows and references
- Collapse nested if statements
- Replace manual strip_prefix with str::strip_prefix()
- Remove redundant closures in map/unwrap_or_else
- Use Iterator::next_back() instead of rev().next()
- Simplify map_or patterns
- Use std::io::Error::other() instead of new(ErrorKind::Other, ..)
- Use div_ceil() instead of manual ceiling division
- Consolidate format! string arguments
- Various other idiomatic Rust improvements

39 files changed, 220 insertions(+), 320 deletions(-)
2026-02-14 01:26:02 +01:00
Dionisio c384a08763 fix: fetch OIDC discovery before building authorization URL (#91)
get_authorize_url() was synchronous and fell back to constructing
{issuer}/authorize when the discovery cache was empty. This produced
incorrect URLs for providers like Keycloak whose authorization
endpoint is {issuer}/protocol/openid-connect/auth.

Made get_authorize_url() async so it can call get_discovery() to
fetch the real authorization_endpoint from .well-known/openid-configuration
before the first redirect. The discovery document is cached after the
initial fetch.
2026-02-13 21:42:41 +01:00
Dionisio ad07a5abda fix: auto-apply DB schema on fresh install and fix admin registration (#81)
- Auto-apply schema.sql when database tables don't exist (embedded in binary)
- Handle missing tables gracefully during admin registration (treat as fresh install)
- Fix docker-compose depends_on to wait for postgres healthcheck
- Rename personal folder from 'Mi Carpeta' to 'My Folder' with backward compat
- Translate remaining Spanish messages to English
2026-02-12 14:45:52 +01:00
Dionisio 76d9038e5b fix: resolve compiler warnings and downgrade JWT secret log to warn
- Downgrade OXICLOUD_JWT_SECRET missing log from error to warn level,
  since generating a random secret per session is valid behavior
- Remove unused import super::* in calendar_storage_adapter tests
- Remove unused import tokio_stream::StreamExt in compression_service tests
- Prefix unused validate_user_ownership with _ in trash_service
- Add nohup.out to .gitignore
2026-02-12 12:29:12 +01:00
Dionisio 6ca1ac4294 fix: resolve Docker volume permission denied on startup
Root cause: Docker named volumes are created as root, but the container
ran as the unprivileged 'oxicloud' user (UID 1001). Services like
thumbnail_service, image_transcode, and dedup_service call
create_dir_all under /app/storage during initialization, which fails
with 'Permission denied (os error 13)'.

Changes:
- Add entrypoint.sh that runs as root to chown /app/storage, then
  drops privileges via su-exec before executing the application
- Update Dockerfile to install su-exec, copy entrypoint, and use
  ENTRYPOINT instead of USER+CMD
- Downgrade id_mapping_service initial write failure from ERROR to WARN
  (empty in-memory map is perfectly valid, will persist on next save)
- Improve panic message in main.rs to hint at Docker permission issue

Fixes #<issue>
2026-02-12 09:57:33 +01:00
Dionisio d31a413e57 chore: translate all Spanish comments and log messages to English 2026-02-12 09:41:25 +01:00
Dionisio 3050955f9c feat: add admin/profile UI, i18n (fr/de/pt), fix CI pipelines
- Add admin panel link and profile modal in user menu dropdown
- Add French, German and Portuguese locale support (full translations)
- Register new locales across JS frontend and Rust backend
- Create CI pipeline (fmt, clippy, test, audit, build)
- Fix docker-build.yml (cache, real tests, reduced timeout)
- Fix docker-publish.yml (multi-arch via QEMU, latest tag, pre-publish tests)
- Add dependabot.yml for automated dependency updates
2026-02-11 12:37:03 +01:00
Dionisio 1a1dee9179 fix(oidc): add CSRF state validation, PKCE S256, nonce, secure token delivery, registration guard
Security fixes for OIDC authentication flow:

1. CSRF state validation (High): State nonce is now stored server-side
   and validated on callback (single-use, 600s TTL)

2. PKCE S256 (Medium): code_challenge/code_verifier pair generated per
   RFC 9126, sent in authorize URL and token exchange

3. Nonce in ID token (Medium): Random nonce included in authorize URL,
   verified against ID token claims to prevent token replay

4. Secure token delivery (Medium): Tokens no longer in URL fragments.
   One-time exchange code redirected to frontend, tokens retrieved via
   POST /api/auth/oidc/exchange endpoint (60s TTL, single-use)

5. Registration guard (Low): POST /api/auth/register returns 403 when
   disable_password_login is active in OIDC-only mode
2026-02-11 00:37:47 +01:00
Dionisio 8ef62109a3 feat(auth): add OpenID Connect (OIDC) authentication support
Implements OIDC Authorization Code Flow for external identity providers
(Authentik, Keycloak, etc.) with JIT user provisioning.

New features:
- OidcService with OpenID Discovery, JWKS caching, RS256 ID token validation
- Authorization Code Flow: /api/auth/oidc/authorize -> IdP -> /api/auth/oidc/callback
- JIT user provisioning from OIDC claims (sub, email, name, groups)
- OIDC group-to-role mapping (admin_groups config)
- Provider info endpoint: GET /api/auth/oidc/providers
- Option to disable password login entirely (OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN)
- Auto-provision toggle (OXICLOUD_OIDC_AUTO_PROVISION)
- Email collision detection (security: prevents account takeover)

Configuration (env vars):
- OXICLOUD_OIDC_ENABLED, OXICLOUD_OIDC_ISSUER_URL
- OXICLOUD_OIDC_CLIENT_ID, OXICLOUD_OIDC_CLIENT_SECRET
- OXICLOUD_OIDC_REDIRECT_URI, OXICLOUD_OIDC_SCOPES
- OXICLOUD_OIDC_FRONTEND_URL, OXICLOUD_OIDC_PROVIDER_NAME
- OXICLOUD_OIDC_AUTO_PROVISION, OXICLOUD_OIDC_ADMIN_GROUPS
- OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN

DB migration:
- ALTER TABLE auth.users ADD oidc_provider, oidc_subject columns
- UNIQUE index on (oidc_provider, oidc_subject)

Files changed: 14 files, ~1400 lines added
Dependencies: reqwest 0.12 (rustls-tls-webpki-roots), base64 0.22
2026-02-10 20:32:32 +01:00
Diocrafts a82faa5eaf refactoring hexagonal and clean architecture 2026-02-08 13:40:23 +01:00
Dionisio 8f2b0a354c big refactoring 2026-02-03 17:59:04 +01:00
Dionisio 52840e57df refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities
- Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer
- Update contact_pg_repository to use persistence DTOs
- Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0)
- Fix unused variable warnings in main.rs
- Move PathService import from domain to infrastructure
- Add missing fields to CoreServices and RepositoryServices
- Create proper service initialization in main.rs

Clean Architecture improvements:
- Domain layer no longer depends on serde framework
- Persistence concerns isolated to infrastructure layer
- TokenClaims in auth_service.rs is only exception (required for JWT)
2026-02-02 23:56:40 +01:00
DioCrafts 5306bdc9e3 fix uploding bugs 2025-04-12 12:21:57 +02:00
DioCrafts 8f1d213526 improve postgresql performance 2025-04-09 00:21:20 +02:00
DioCrafts 705cb5b069 adding pdf and image viewer 2025-04-02 01:22:05 +02:00
dionidev e79ca8304b adding comments, moving technical documentation, improve Dockerfile, delete unuseful files 2025-03-30 14:17:09 +00:00
DioCrafts e22c0ac855 fix trash and additional bugs 2025-03-26 18:33:22 +01:00
DioCrafts 38b0e9594b fix auth errors and add primigenial paper trash 2025-03-24 16:47:42 +01:00
DioCrafts 9a9fd72f61 fixing bugs 2025-03-23 22:44:18 +01:00
DioCrafts cafad0fbfd adding user authentication 2025-03-20 09:22:31 +01:00
DioCrafts d9bbd575d2 adding several features 2025-03-19 00:44:27 +01:00
root fe19bc8505 Initial commit 2025-03-17 21:28:08 +01:00