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()
Previously, if enable_auth=true but the database or auth services failed
to initialize, the server silently started in PUBLIC mode — all API routes
accessible without authentication. This is a critical fail-open bug.
Fixes (fail-closed behavior):
1. main.rs: DB pool creation failure with enable_auth=true now panics
instead of falling through with db_pool=None
2. di.rs: create_auth_services() failure now propagates the error via ?
instead of logging and continuing with auth_services=None
3. main.rs: Added defensive assert! that auth_service is Some when
enable_auth=true, preventing any future refactor from reintroducing
the silent degradation
The server will now refuse to start if authentication is configured
but cannot be properly initialized.
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)
- Add PG trigger trg_files_decrement_blob_ref (AFTER DELETE ON storage.files)
that auto-decrements storage.blobs.ref_count for every deleted file row.
Covers all paths: explicit DELETE, ON DELETE CASCADE, trash emptying.
- Remove manual remove_reference() call from delete_file() in
file_blob_write_repository — trigger is now the single source of truth.
- Fix double-decrement bug in FileManagementService::delete_with_cleanup:
was decrementing ref_count on trash (soft-delete) when the file row still
existed, causing premature blob GC and potential data corruption on restore.
- Remove dead fields (file_read, dedup_service) from FileManagementService
and simplify constructors — ref_count fully handled by PG trigger.
- Create example.env with all 31 environment variables documented
- Update docker-compose.yml to use env_file directive
- Update doc/deployment.md with Quick Start and missing variables
- Add OXICLOUD_BASE_URL and WOPI configuration docs
- Update README.md Docker section with .env setup
- Remove duplicate port mapping (incorporates 4577e56)
- Fix server port default from 8085 to 8086
- Add search_files_paginated and count_files to MockFileRepository in share_service.rs
- Add search_files_paginated and count_files to MockFileRepository in trash_service_test.rs
These methods were added to the FileReadPort trait to support database-level
pagination for search optimization.
The original implementation tried to find user's home folder via
file_repository.list_files(None), but this only returns files (not folders)
and folders are in a separate table. This resulted in storage always
showing 0 bytes.
Fixed by using a direct SQL query to sum file sizes by user_id from
storage.files table, which is more efficient (O(1) vs recursive) and
correctly calculates storage for all files owned by the user.
When a user has no home folder (e.g., legacy users or failed folder
creation during registration), the frontend would get an empty list
from GET /api/folders, leaving userHomeFolderId undefined. This caused
uploads to fail or go to the wrong location.
Now list_folders_for_owner() automatically creates a home folder when:
- Listing root folders (parent_id is None)
- The result is empty
This self-healing approach fixes the issue at the source, ensuring
the frontend always gets a valid userHomeFolderId.
- Add search_files_paginated method to FileReadPort for database-level pagination
- Implement efficient SQL-based search with LIMIT/OFFSET in file_blob_read_repository
- Add relevance scoring (exact match > starts-with > contains)
- Support multiple sort options (name, date, size) with ascending/descending order
- Fix cache expiration handling with proper borrow checker semantics
- Use i64 for SQL LIMIT/OFFSET parameters instead of usize
- Clean up duplicate SQL query builder code in search_files_paginated
This significantly improves search performance for non-recursive queries by:
- Pushing pagination to the database layer
- Avoiding loading all files into memory for filtering
- Supporting database-level sorting
- 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)
The admin user list now displays an OIDC/Local badge per user and
hides the password reset button for OIDC-provisioned accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add auth_provider field to UserDto, derived from oidc_provider
("local" for password users, provider name for OIDC users)
- Block change_password() for OIDC users with clear error message
- Block admin_reset_password() for OIDC users
Fixes#122, Fixes#123
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Run `cargo fmt` across all Rust source files to enforce consistent
formatting (import ordering, line wrapping, match arm braces).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added formatQuotaSize() function to display ∞ for unlimited (0) quota
- Added format_quota_size() Rust function matching JavaScript behavior
- Updated quota defaulting logic from '||' to '== null' check
- This allows 0 (unlimited) to pass through while defaulting to 10 GB
only when the value is null/undefined
- Call sites now use dedicated formatQuotaSize() or format_quota_size()
instead of options parameter for cleaner API
- Add breadcrumbPath array to app state for tracking folder hierarchy
- Rewrite updateBreadcrumb() to render full path: Home > folder > subfolder
- Each breadcrumb segment is clickable to navigate back to that level
- Current folder shown in bold (non-clickable), parent folders as links
- Reset breadcrumb path on tab switch, home navigation, and initial load
- Update navigateFolder/selectFolder to push to breadcrumb path
- Enhanced breadcrumb CSS with hover effects and dark mode support