MockTrashRepository now holds shared Arc<Mutex> refs to the
trashed_files and trashed_folders collections so that clear_trash()
also purges them — mirroring the ON DELETE CASCADE + PG trigger
behaviour in production.
All 112 tests pass.
The existing clear_trash() already performs bulk SQL DELETEs:
1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
The per-item loop (get_trash_items + N individual deletes) was redundant
since clear_trash() re-deleted everything anyway. Removed the loop entirely.
Folder CASCADE (FK ON DELETE CASCADE) handles child folders and their files.
PG trigger trg_files_decrement_blob_ref automatically decrements blob
ref_counts for every deleted file row — no Rust-side remove_reference()
call needed. Orphan blobs are cleaned by garbage collection.
Result: O(2) SQL queries instead of O(N+2) for any trash size.
- Add breadcrumb navigation to move dialog for folder navigation
- Add 'Copy' button alongside 'Move' button in the dialog
- Implement copyFile and copyFolder functions in fileOps
- Add copy handler for batch operations
- Add CSS styles for btn-outline button (light and dark mode)
- Add translations for new dialog strings
fix: properly show home folder contents in move dialog
- Use effectiveParentId for all checks and rendering
- Show 'Select this folder' option for home folder
- Only show 'go to parent' when breadcrumb has items (navigated into subfolders)
fix: improve move dialog UX
- Hide breadcrumb at home folder level (not needed)
- Only show 'Select this folder' option after navigating into subfolders
- Show 'no subfolders' message when there are no folders to navigate
- Properly display subfolders for navigation
- Remove 'Root' option from move dialog (users move within their home folder)
- Show children of current folder instead of flat folder list
- Add breadcrumb navigation for folder browsing
- Add 'go to parent' navigation option
- Add 'select this folder' option to choose current location
- Add CSS styles for new navigation elements
- Add dark mode support for move dialog
- Add i18n translations for new strings (en, es)
RUSTSEC-2023-0071 (Marvin Attack) affects RSA private key operations.
This application uses HS256 for internal JWT signing and only performs
RSA public key verification (not private key operations) for OIDC/OAuth2.
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()
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).
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.
sync::Cache uses an internal Mutex that blocks the Tokio worker thread
during maintenance (eviction, frequency bookkeeping). Under high
concurrency this serialises all tasks scheduled on the same worker.
future::Cache defers maintenance to an internal async task and never
holds a synchronous lock visible to the caller.
Changes:
- Field type: sync::Cache → future::Cache
- get_from_cache / store_in_cache: now async, calls .await
- clear_search_cache: added run_pending_tasks().await after invalidate
Consistent with the pattern already used in thumbnail_service,
image_transcode_service, and file_content_cache.
Use NamedTempFile::into_parts() to reuse the existing fd instead of
opening a second one, and store TempPath in response extensions so the
file is only deleted after the body stream finishes.
Before: temp_file was dropped when the handler returned (before Axum
streamed the body). Worked only by accident on Unix (unlinked files
remain readable while an fd is open) but used 2 fds and was fragile.
After: single fd, explicit lifetime guarantee, cross-platform correct.
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
Replace separate COUNT and SELECT queries in paginated search with a
single query using PostgreSQL COUNT(*) OVER() window function.
Affected methods:
- search_files_paginated: collapsed 4 branches × 2 queries into a
single dynamic query builder with 1 query per call
- search_files_in_subtree: merged COUNT + SELECT into one query
Additionally, search_files_paginated is now a dynamic query builder
(like search_files_in_subtree was) instead of 4 hardcoded branches,
reducing code from ~240 lines to ~80 lines.
Impact:
- DB round-trips per search: 2 → 1 (50% reduction)
- Latency: ~50% lower per paginated search
- Connection pool pressure: halved for search workloads
- Atomicity: count and data from same snapshot (no race)
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.
- Wrap hash_password calls with tokio::sync::Semaphore (max 2 concurrent)
- Prevents Argon2 from monopolizing CPU cores and starving async tasks
- Caps peak RAM from unbounded to ~38 MB for concurrent hash operations
- Uses spawn_blocking via injected PasswordHasherPort to avoid blocking Tokio
- 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.
- Add storage.shares table with indexes on token, (item_id, item_type), created_by
- Create SharePgRepository with indexed SQL queries and window-function pagination
- Rewire DI to inject SharePgRepository with PgPool instead of config
- Delete legacy share_fs_repository.rs (295 lines of JSON file I/O)
- Remove dead module declaration from repositories/mod.rs
Eliminates O(n) full-file JSON reads/writes, TOCTOU races, and crash
corruption risk. All share operations now use indexed PG queries.
- Delete cache.rs middleware that buffered entire response bodies (up to 10MB)
in RAM on every cache miss, defeating streaming and causing memory spikes
- Also buffered non-GET responses unnecessarily via response_map_body()
- Add lightweight ETag support (based on max modified_at + count) to:
- FolderHandler::list_folder_listing (combined folder+files endpoint)
- FileHandler::list_files_query (file listing endpoint)
- Both support If-None-Match / 304 Not Modified without any body buffering
- File downloads already had ETag/304 support at handler level
- Service-level caches (FileContentCache, SearchService, ThumbnailService)
remain unchanged — they handle caching without HTTP body materialization
- Add hamburger menu toggle button in top bar (visible on mobile only)
- Sidebar slides in/out from the left with smooth transition
- Add dark overlay when sidebar is open on mobile
- Close sidebar on overlay click, nav item click, or escape key
- Support RTL languages (sidebar slides from right)
- Add dark theme styles for toggle button
- Responsive breakpoint at 768px
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
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)
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)
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)
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.
- 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
- Root PROPFIND: list_folders + list_files now run concurrently
- Sub-folder PROPFIND: list_files + list_folders now run concurrently
- ~50% latency reduction on every PROPFIND operation
- Matches existing pattern used in folder_handler::list_folder_listing
- Add breadcrumb navigation with folder hierarchy display
- Create setCurrentSection() helper to centralize view state management
- Derive nav item section dynamically from DOM data-i18n attribute
- Remove remnant /shared page and consolidate to SPA sharedView
- Add search input to sharedView for client-side filtering
- Fix 'Go to Files' button to use switchToFilesView()