The redirect middleware was a leftover from the custom HTTP server → Axum
migration. It executed on every single request (including static files),
allocating a String from the URI path and performing 3 starts_with() checks,
but never actually redirected anything — just debug logging.
- Remove middleware application from main.rs
- Remove pub mod redirect from middleware/mod.rs
- Delete redirect.rs (121 lines of dead code)
Saves ~80-170ns of overhead per request (String alloc + comparisons +
Tower layer dispatch).
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
- 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)
- 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
- 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
- Change SearchUseCase::search() return type to Arc<SearchResultsDto>
- Replace Cache<u64, SearchResultsDto> with Cache<u64, Arc<SearchResultsDto>>
- Eliminate .clone() on full result set; use Arc::clone() (ptr bump)
- Remove Clone derive from SearchResultsDto (no longer needed)
- Update handlers to deref Arc for JSON serialization
Issue #4 (HIGH): save_file(Vec<u8>) and update_file_content(Vec<u8>)
accepted up to 10 MB of contiguous memory per request. While the main
upload paths already used streaming, the WebDAV compat methods
(create_file, update_file) and the empty-file handler still used the
buffered path, creating a .to_vec() copy.
Changes:
- FileWritePort trait: remove save_file(Vec<u8>) and
update_file_content(Vec<u8>) — only streaming variants remain
- FileUploadUseCase trait: remove upload_file(Vec<u8>)
- file_upload_service.rs: create_file() and update_file() now spool
&[u8] to NamedTempFile + Sha256::digest, then delegate to streaming
path (save_file_from_temp / update_file_streaming)
- file_handler.rs: empty file uploads use upload_file_streaming with
- FileBlobWriteRepository: remove save_file and update_file_content impls
- StubFileWritePort, StubFileUploadUseCase, MockFileRepository: remove
corresponding dead method impls
Impact: impossible to accidentally use a buffered upload path. All
content goes through streaming with ~256 KB peak RAM. -166 LOC.
- Replace in-memory Cursor<Vec<u8>> with async_zip + NamedTempFile (O(256KB) RAM)
- Stream ZIP to client via ReaderStream instead of materializing entire archive
- Replace N+1 BFS folder traversal with 2 bulk ltree queries
- Remove dead zip_service field and with_zip_service() from BatchOperationService
- Remove synchronous zip crate dependency (only async_zip remains)
- Eliminates ~2.5GB RAM spike per 2GB batch download
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.
- 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
- 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()
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)
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>
- Add CompressionLayer to web router for gzip compression of JS/CSS/JSON/SVG
- Add Cache-Control header (7 days + stale-while-revalidate) via SetResponseHeaderLayer
- Add 'set-header' feature to tower-http dependency
- Result: 67-85% reduction in transfer size for all static assets
Browsers send the full relative path (e.g. 'Screenshots/file.png') as
the multipart filename when uploading folders via webkitRelativePath.
The File entity rejects names containing '/' or '\', causing all files
in a folder upload to fail with 'Invalid file name'.
Three fixes:
- Backend: strip path components from multipart filename in file_handler,
keeping only the basename. Also prevents path-traversal attacks.
- Frontend (fileOperations.js): explicitly pass file.name as the third
argument to FormData.append() in uploadFolderFiles() to override the
browser's relative path.
- Frontend (ui.js): detect folder drops in drag-and-drop handlers by
checking webkitRelativePath, and route them to uploadFolderFiles()
instead of uploadFiles() so subfolders are created first.
Closes#121
V1: Add owner-scoped folder pagination (list_folders_by_owner_paginated)
- New method in FolderRepository trait, PG implementation, service & handler
- Prevents IDOR by filtering folder listings to authenticated user
V2: Enforce ownership checks on folder mutations
- rename_folder, move_folder, delete_folder now require caller_id
- Service verifies folder.owner_id == caller_id (returns 404 on mismatch)
- Propagated to folder_handler, batch_handler, batch_operations, webdav_handler
- delete_folder_with_trash upgraded from OptionalAuthUser to AuthUser
- download_folder_zip now checks ownership before streaming
V3: Fix XSS in frontend via DOM APIs
- sharedView.js: innerHTML → createElement + textContent
- contextMenus.js: innerHTML → DOM construction for share dialog
Cleanup: removed unused OptionalAuthUser import, updated all stubs/mocks
Root cause: when window.app.currentPath was empty/falsy (due to timing,
page state reset, or initialization), the frontend sent parent_id: null.
The backend then created folders at the storage root instead of inside
the user's home folder.
Backend fix (folder_handler.rs):
- Added AuthUser extractor to create_folder handler
- When parent_id is None, auto-resolves the user's home folder
('My Folder - {username}') as the parent folder
- Folders are now always created inside the user's directory tree
Frontend fix (fileOperations.js):
- Changed parent_id fallback from null to window.app.userHomeFolderId
- Prevents sending null parent_id even if currentPath is reset
Search in subfolders: no fix needed — search_recursive() already
traverses the filesystem correctly; it was only failing because folders
were physically flat instead of nested.
Bumps service worker cache to v12.
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.
Add server-side routes for /profile, /admin, and /shared that
serve their respective HTML pages directly (same pattern already
used for /login). Updated all frontend references to use clean
URLs instead of .html extensions.
Fixes#99
Non-admin users were seeing all users' root folders, including the
admin's. Three root causes fixed:
1. Backend: list_root_folders now extracts AuthUser and filters
results so each user only sees their own home folder at the
root level (folders matching 'My Folder - {username}' or
'Mi Carpeta - {username}').
2. Frontend: findUserHomeFolder() searched only for the Spanish
pattern 'Mi Carpeta - {username}' but the backend creates
folders with the English pattern 'My Folder - {username}'.
Now checks both naming conventions.
3. Frontend: when the home folder was not found, the code fell
back to folderList[0] — which was usually the admin's folder.
Removed that dangerous fallback; now shows empty root instead.
Fixes#94
Axum's default body limit for Multipart extraction is 2 MB.
OxiCloud never overrode this default, so any file upload larger
than ~2 MB was silently truncated.
Added DefaultBodyLimit::max(10 GB) both globally on the app
router and specifically on the file upload routes, matching the
chunked upload capability already in place for large files.