Commit Graph

86 Commits

Author SHA1 Message Date
Dionisio fdbb2bf60a fix(security): patch critical IDOR & auth vulnerabilities
- Fix logout no-op: extract refresh token from cookie/body (auth_handler)
- Secure all 12 WebDAV handlers with AuthUser + resolve_path_for_user
- Secure all 7 batch handlers with caller_id ownership checks
- Add _owned variants: copy_file_owned, delete_file_owned, get_file_stream_owned, get_folder_owned
- Secure list_files_query: add AuthUser, SQL-level user_id filter, tenant-isolated ETag
- Remove deprecated unscoped resolve_path() and exists() from PathResolverService
- Remove dead list_files handler (unmounted, no auth)
- Add list_files_for_owner (SQL) and list_files_owned across trait chain
2026-03-05 10:30:39 +01:00
Diocrafts ee86c3a128 fix: resolve all clippy warnings and convert integration_tests to custom cfg
- Add type aliases (FileRow, FolderRow, FolderRowPaginated, FolderRowOptUser) to reduce type complexity
- Simplify redundant closures in app_password_handler and webdav_handler
- Remove needless borrow in auth_handler
- Collapse nested if/let chains in login_lockout, webdav_lock, auth, rate_limit
- Box LockEntry in acquire() Err variant to fix large enum variant warning
- Rename DeviceCodeStatus::from_str to parse to avoid should_implement_trait lint
- Add #[allow(clippy::too_many_arguments)] and #[allow(clippy::result_unit_err)] where appropriate
- Convert integration_tests from cargo feature to custom cfg attribute
- Add check-cfg lint config in Cargo.toml for integration_tests cfg
2026-03-04 23:55:08 +01:00
Dionisio 98fb3e6408 fix(security): VULN-01 admin escalation + VULN-02 path traversal hardening
VULN-01 - Admin privilege escalation:
- Harden register() to reject is_admin=true
- Add /api/setup endpoint with setup_token for initial admin creation
- Add SetupAdminDto and setup_token to AppState
- Remove dead code from auth handler

VULN-02 - Path traversal (CVSS ~8.6):
- Solution A+E: Harden StoragePath constructors (from_string, new, join)
  to strip '..' and '.' segments and reject slash injection
- Solution B: resolve_path() now returns Result<PathBuf>, calls
  validate_path() internally, and verifies resolved path stays under root
- Update StoragePort trait signature to return Result<PathBuf, DomainError>
- Remove dead code: FilePathResolutionPort, StorageVerificationPort,
  DirectoryManagementPort (declared but never implemented)
- Add 17 security tests covering traversal attack vectors
2026-03-04 14:14:40 +01:00
Claude 3b0c09cd56 Replace standalone md5 crate with RustCrypto md-5
The standalone `md5` (0.8.0) crate is replaced with `md-5` (0.10) from
the RustCrypto ecosystem, which shares `digest v0.10` with sha2, argon2,
blake2 and other crates already in the dependency tree — eliminating one
redundant implementation.

https://claude.ai/code/session_01V23pGpfNw5ujZvtwRFG6qy
2026-03-03 16:16:44 +00:00
Claude 78c983136f perf: use Arc<str> for repetitive DTO fields to eliminate clone allocations
Replace String with Arc<str> for fields that contain repeated static
values (mime_type, icon_class, icon_special_class, category) in
FileDto, FolderDto, and OptimizedFileContent.

These fields are computed from ~40 static lookup tables and cloned
on every request. With Arc<str>, clone becomes O(1) atomic increment
instead of O(n) heap allocation — saving thousands of allocations/s
under load.

Fields kept as String: id, name, path, folder_id, owner_id
(unique per item, rarely cloned).

Zero API impact — serde serializes Arc<str> identically to String.

https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
2026-03-03 15:55:15 +00:00
Claude 4bee05c690 merge: resolve conflict in auth_factory.rs with origin/main
Keep custom Result type alias (replacing anyhow) from main while
preserving our removal of unused TokenServicePort import.

https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
2026-03-03 15:42:28 +00:00
Claude 1b49135ca9 perf: replace dyn trait objects with concrete types to eliminate vtable overhead
Remove async-trait dependency and use native Rust async fn in traits.
Replace Arc<dyn Trait> with Arc<ConcreteType> throughout the codebase
to enable monomorphization and eliminate dynamic dispatch overhead.

Key changes:
- Remove write-behind cache (no implementation existed)
- Fix should_transcode static method call
- Use ContactStorageAdapter directly instead of dyn AddressBookUseCase
- Clean up unused trait imports across services and DI

https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
2026-03-03 15:36:42 +00:00
Dionisio 6f30ac761f refactor: replace WebDAV LOCK stub with moka-backed lock store (auto-expire TTL) 2026-03-03 11:49:52 +01:00
Dionisio 0cd544ab96 perf: enable BLAKE3 multithreaded hashing (update_rayon) for files >10MB 2026-03-03 11:18:40 +01:00
Dionisio efcf88c4d7 style: cargo fmt --all 2026-03-03 01:49:18 +01:00
Dionisio 1df52fd702 security: add IP rate limiting + account lockout on auth endpoints
- Rate limit login (5/min), register (3/hr), refresh (10/min) per IP
- Account lockout after 5 consecutive failed logins (15 min cooldown)
- Fix stored XSS in admin panel (escapeHtml on all user-controlled data)
- All limits configurable via OXICLOUD_RATE_LIMIT_* / OXICLOUD_LOCKOUT_* env vars
- Zero new dependencies (uses existing moka crate for in-memory caches)
- Includes unit tests for lockout service
2026-03-03 01:44:39 +01:00
Dionisio b9bf7ba288 perf(webdav): single UNION ALL query for path resolution
Replace the double-query pattern (get_folder_by_path + get_file_by_path)
across PROPFIND, HEAD, DELETE, MOVE, and COPY handlers with a single
UNION ALL query via PathResolverService.

PG Append node short-circuits on LIMIT 1: if the folder branch matches,
the file branch is never executed. Cuts WebDAV path resolution from
2 round-trips to 1 per request.

Also adds an exists() method using EXISTS subqueries for the Overwrite
header checks in MOVE/COPY (avoids constructing full DTOs).

Legacy double-query fallback retained when PathResolver is unavailable.
2026-03-02 23:40:48 +01:00
Claude b9de342223 fix: resolve CalDAV PROPFIND returning empty property values (#153)
Root cause: XML namespace prefixes (e.g. "D", "C") were not being
resolved to their actual namespace URIs (e.g. "DAV:",
"urn:ietf:params:xml:ns:caldav") during PROPFIND parsing. This caused
all property match arms to fall through to the catch-all, producing
empty XML elements.

Changes:
- Add namespace-aware XML parsing (collect_ns_decls + resolve_name) to
  WebDavAdapter, used by all DAV protocol parsers (WebDAV, CalDAV,
  CardDAV)
- Add /.well-known/caldav -> /caldav/ redirect (RFC 6764)
- Add root /caldav/ PROPFIND response with current-user-principal and
  calendar-home-set discovery properties
- Add /caldav/principals/{username}/ PROPFIND handler (was 500 error)
- Add /caldav/{username}/ user calendar home handler (calendar-home-set
  target)
- Respect Depth header at root: depth 0 returns only root entry, depth
  1+ includes calendar children
- Fix pre-existing TRANSCODE_POOL_THREADS test compilation error
- Add 8 new tests covering namespace resolution, discovery properties,
  and principal responses

https://claude.ai/code/session_01T49VBJSimgo28APxbucHzq
2026-03-02 20:40:41 +00:00
Dionisio d023386ebd perf: add cargo-chef multi-stage Docker build and JWT validation cache
- Replace dummy main.rs caching with cargo-chef planner/cook/build stages
  for granular dependency caching (only invalidates when deps actually change)
- Add BuildKit cache mounts for cargo registry, git checkouts, and target dir
  enabling incremental compilation across Docker builds
- Add BLAKE3-keyed moka cache for JWT token validation results (30s TTL)
  avoiding redundant HMAC-SHA256 verification on repeated requests (~20x faster)
- Include cache hit/miss counters for observability
- Add tests for cache hit behavior and invalid token non-caching
2026-03-02 04:46:13 +01:00
Dionisio dd27872a8c perf: replace SHA-256 with BLAKE3 in WebDAV/WOPI, fix JOIN index usage, add LIMIT to favorites, remove dead lru crate, trim tokio features
- WebDAV PUT and WOPI PutFile: replace sha2::Sha256 with blake3::Hasher (~5x faster hashing, compatible with dedup service)
- Fix TEXT↔UUID JOIN anti-pattern in favorites and recent_items repos (enables PK index usage)
- Add LIMIT 500 to get_favorites query to prevent unbounded memory allocation
- Remove unused lru crate from Cargo.toml (superseded by moka)
- Replace tokio features=["full"] with explicit feature list (removes signal, process, test-util)
2026-03-02 02:13:08 +01:00
Dionisio b06206207f perf: optimize hot paths — batch concurrency, pagination, sorting, transcoding, folder ops
- batch_operations.rs: replace join_all with buffer_unordered, Arc<str> for shared IDs, remove redundant clones and dead Semaphore
- folder_db_repository.rs: use COUNT(*) OVER() for single-query pagination; UPDATE RETURNING for rename/move (eliminates extra SELECTs)
- folder_service.rs: remove StorageTransaction wrapper from rename/move — direct repo call (4→2 and 5→3 queries)
- search_service.rs: replace sort_by(to_lowercase) with sort_by_cached_key (N vs 2·N·log₂N allocations)
- image_transcode_service.rs: dynamic rayon pool sizing via available_parallelism() instead of hardcoded 2 threads
- Remove dead transactions module (zero consumers after folder_service refactor)
2026-03-02 01:30:34 +01:00
Dionisio 641b6853ad perf: add socket2 TCP_NODELAY + socket tuning for low-latency responses
- Replace basic TcpListener::bind with socket2 tuned socket
- TCP_NODELAY: disable Nagle's algorithm (-5 to 40ms latency on small responses)
- SO_REUSEADDR: port available immediately after server restart
- SO_REUSEPORT: ready for multi-worker scaling (Linux)
- TCP_KEEPALIVE: detect dead connections within 60s/10s interval
- listen(2048): high backlog for WebDAV connection bursts
- Eliminate redundant create_dir_all calls from upload hot path
2026-03-02 00:12:33 +01:00
Dionisio e2fb29ea60 perf: replace SHA-256 with BLAKE3 + add mimalloc global allocator
- Replace SHA-256 with BLAKE3 (~5x faster) for content-addressable hashing
  in dedup_service, file_handler, file_upload_service, chunked_upload_service
- Add mimalloc as global allocator for 10-30% throughput improvement
- sha2 crate retained only for PKCE (OAuth2 standard requirement)
- BLAKE3 produces 64-char hex hashes (same format), no DB schema changes needed
2026-03-01 21:47:39 +01:00
Dionisio 2421724b80 style: cargo fmt --all 2026-02-26 01:02:25 +01:00
Dionisio b05805ee1c fix: resolve clippy warnings (redundant closure, collapsible if) 2026-02-26 00:57:49 +01:00
Dionisio 6df68d3716 style: cargo fmt --all 2026-02-26 00:47:32 +01:00
Dionisio 7e278a0b32 perf: bulk-delete expired trash in 2 SQL queries instead of N+1 loop 2026-02-26 00:42:41 +01:00
Dionisio fb4eaf2cf9 perf: eliminate double disk read in thumbnail generation (read-once buffer) 2026-02-26 00:32:02 +01:00
Dionisio 9f8a6f5177 perf: stream_files_in_subtree — replace Vec<File> with async Stream
Replace list_files_in_subtree (fetch_all → Vec) with stream_files_in_subtree
that returns a Pin<Box<dyn Stream<Item = Result<File/FileDto>>>> backed by a
PostgreSQL cursor via sqlx::fetch().

Changes:
- FileReadPort::stream_files_in_subtree() returns streaming cursor (no default)
- FileRetrievalUseCase::stream_files_in_subtree() maps File→FileDto on the fly
- FileBlobReadRepository: async_stream::try_stream! + sqlx::fetch() cursor
- batch_operations: consume stream into HashMap incrementally
- zip_service: consume stream into HashMap incrementally
- All stubs/mocks updated (return empty stream)

Eliminates:
- Double allocation: Vec<(9-tuple)> + Vec<File> materialized simultaneously
- Unbounded RAM proportional to subtree size (was ~500 bytes × N files)
- Latency: callers blocked until last row fetched from PG

RAM is now O(folders) for the HashMap, not O(files).
2026-02-26 00:07:10 +01:00
Dionisio 1ad7a32a61 perf: batch garbage_collect in 500-row mini-transactions
- Replace single unbounded DELETE FROM storage.blobs WHERE ref_count=0
  with a loop of DELETE...LIMIT 500 batches using ctid sub-select
- Each batch is its own implicit TX (~1-5 ms), preventing:
  · massive row-lock accumulation (was ~200 bytes × N orphans in PG)
  · WAL bloat from a single giant DELETE
  · blocking concurrent uploads on storage.blobs
- Blob files deleted AFTER each batch commits (crash-safe)
- tokio::task::yield_now() between batches to avoid starving uploads
2026-02-25 23:54:09 +01:00
Dionisio f9dde6ffff perf: replace RwLock<HashMap> with DashMap in ChunkedUploadService + decouple disk I/O from lock
Issue #3 (CRITICAL): The global RwLock<HashMap> serialised ALL chunk uploads
across all users. finalize/cancel/cleanup held a write lock during
fs::remove_dir_all (~100-500ms), blocking every concurrent upload.

Changes:
- Replace tokio::sync::RwLock<HashMap<String, UploadSession>> with
  dashmap::DashMap (sharded concurrent map, ~64 shards)
- Operations on independent sessions never contend
- finalize_upload_inner: remove from map (µs), THEN delete temp dir
- cancel_upload_inner: same pattern — disk I/O outside lock
- cleanup_loop: collect expired IDs via lock-free iteration, remove
  from map, THEN delete dirs sequentially with no lock held
- upload_chunk_inner: DashMap::get_mut replaces global write lock
- get_status_inner / complete_upload_inner: DashMap::get replaces read lock
- Remove tokio::sync::RwLock import (dead)

Also includes Issue #2 (dedup_service.rs write-first + upsert) from
previous session.

Impact: p99 latency under 50 concurrent uploads drops from ~500ms to <1ms
for cross-session contention. Cleanup loop no longer blocks uploads.
2026-02-25 23:31:51 +01:00
Dionisio 97cf6402e2 fix: resolve all clippy warnings and enforce cargo fmt
- display_helpers: convert module doc-comments to regular comments,
  merge identical text/markdown + text/ branches
- search_service: replace needless range loops with slice-based pagination
- folder_repository, folder_db_repository: collapse nested if statements
- favorites_pg_repository: remove unnecessary borrow on generic arg
- file_blob_read_repository: collapse 6 nested if-let blocks
- file_blob_write_repository: collapse nested if for dedup ref decrement
- chunked_upload_service: use div_ceil(), collapse 2 nested if blocks
- folder_handler: collapse nested if-let for owner check
- webdav_handler: replace 7x io::Error::new(ErrorKind::Other, ..) with
  io::Error::other(..)
- cargo fmt applied to all files

Passes: cargo clippy --all-targets --all-features -- -D warnings
2026-02-25 10:28:34 +01:00
Dionisio 9f692f03c3 Implement dual DB pools (primary + maintenance) and wire services 2026-02-24 19:28:00 +01:00
Dionisio 28966ce28e optimize folder search: SQL-level filtering, user isolation, no in-memory filter; batch cascade trigger 2026-02-24 17:15:36 +01:00
Dionisio 6aa38d0d24 perf: thumbnail semaphore, WOPI streaming, store_bytes guard, spawn_blocking SHA-256
- Issue #1: Add Semaphore(4) + 50MP resolution guard to thumbnail_service
  Bounds peak RAM from 4.8GB (50 uploads) to 384MB (4 concurrent decodes)
- Issue #3: Migrate WOPI PutFile from Bytes to streaming temp file + SHA-256
  RAM per WOPI PUT: ~100MB → ~256KB regardless of file size
- Issue #3: Add 10MB guard in dedup store_bytes (defense-in-depth)
- Issue #5: Move chunked upload assembly to spawn_blocking (sync I/O)
  Frees Tokio workers during SHA-256 hashing (~130ms for 500MB)
- Clean up unused tokio imports (OpenOptions, BufWriter)
2026-02-24 16:11:52 +01:00
Dionisio 71c2cb5edb perf: Arc<AppState>, streaming PROPFIND, spawn_blocking SHA-256
- Issue #4: Wrap AppState in Arc — eliminates 42 Arc::clone + 16 String::clone per request
- Issue #2: Reject Depth:infinity with 403 + streaming XML with paginated DB queries
- Issue #5: Move chunked upload assembly (SHA-256 hash-on-write) to spawn_blocking
- Remove ~270 lines dead code from di.rs (unused builders, Default impl, stubs)
- Clean up unused tokio imports in chunked_upload_service.rs
2026-02-24 15:11:56 +01:00
Dionisio cace61127f perf(cache): use Arc<str> for etag/content_type in ContentCachePort
Replace String with Arc<str> for etag and content_type fields in the
content cache. String::clone() allocates and copies the full string on
every cache hit (O(n)), while Arc<str>::clone() is O(1) — just an atomic
ref-count increment.

This eliminates 2 heap allocations per cache hit on the hottest download
path. At 1000 req/s that is 2000 fewer alloc/dealloc cycles per second.

Changed files:
- cache_ports.rs: trait signatures String → Arc<str>
- file_content_cache.rs: CacheEntry fields, get/put methods, tests
- stubs.rs: StubContentCachePort signatures
- file_retrieval_service.rs: caller creates Arc<str> before put()
2026-02-24 13:22:04 +01:00
Dionisio 962468a1ce perf(dedup): remove blocking std::fs calls from async context
Two fixes in dedup_service.rs:

1. Rename error path (L234): std::fs::remove_file → tokio::fs::remove_file
   Restructured from map_err closure to match block since .await
   cannot be used inside a sync closure.

2. Integrity verify (L691): fused blocking .exists() + async metadata()
   into a single fs::metadata().await call. Eliminates one stat()
   syscall per blob AND removes the only remaining blocking I/O from
   the verify_integrity hot loop (buffer_unordered × VERIFY_CONCURRENCY).
2026-02-24 13:06:40 +01:00
Dionisio 78ae145af5 perf(chunked-upload): hoist 512KB read buffer out of per-chunk loop
causing N × 512KB alloc+memset+dealloc cycles per upload. Moving it
before the loop reuses a single allocation across all chunks.

For a 100-chunk upload this eliminates 99 allocations totalling ~50 MB
of unnecessary memset work.
2026-02-24 13:03:18 +01:00
Dionisio 0986ebf81c perf(transcode): eliminate double buffer copy by accepting Bytes instead of &[u8]
Change ImageTranscodePort::get_transcoded signature from &[u8] to Bytes.
- Rayon closure now receives Bytes::clone() (O(1) ref-count) instead of .to_vec() (~5 MB copy)
- Fallback path returns owned Bytes directly instead of Bytes::from(to_vec()) (~5 MB copy)
- Caller passes content.clone() (O(1)) instead of implicit deref

Saves ~10 MB of allocation per transcode call on a 5 MB image.
2026-02-24 12:48:08 +01:00
Dionisio d907a3eb63 perf(thumbnails): load image once for all 3 sizes in background gen
Rewrite generate_all_sizes_background to use a single spawn_blocking
that decodes the source image once and produces all 3 thumbnail sizes
(Icon 150px, Preview 400px, Large 800px) from the same DynamicImage.

Before: 3× image::open() + 3× JPEG/PNG decode + 3× spawn_blocking
After:  1× image::open() + 1× decode + 3× resize in 1 spawn_blocking

Impact for a 20 MB JPEG (5472×3648):
- Disk I/O: 60 MB → 20 MB (3× reduction)
- CPU decode: ~900 ms → ~300 ms (67% saved)
- Peak RAM (10 concurrent uploads): ~5.4 GB → ~1.8 GB
- spawn_blocking slots: 3 → 1 per upload
2026-02-24 12:41:07 +01:00
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