Nextcloud app uploads sent application/octet-stream as Content-Type,
causing images to not be recognized. Now both WebDAV PUT and chunked
upload paths call refine_content_type() which detects via magic bytes,
then extension, then falls back to the client header. Also fixes
update_file() which previously hardcoded application/octet-stream.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move pg_trgm extension creation before CalDAV indexes that depend on it
- Remove duplicate app-password route registration that caused panic
- Fix missing comma in language selector array (Dutch entry)
- Await async canEdit() in file click handler (Promise was always truthy)
- Detect images by extension fallback when mime_type is octet-stream
(files uploaded via Nextcloud WebDAV API lack correct mime types)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement a complete Nextcloud client compatibility layer so that
Nextcloud desktop/mobile sync clients can connect to OxiCloud.
Key additions:
- Login Flow v2 (device auth) with OIDC bridge support
- WebDAV handler compatible with Nextcloud clients (PROPFIND, GET,
PUT, DELETE, MKCOL, MOVE, COPY, HEAD, PROPPATCH)
- OCS API endpoints (user info, capabilities, notifications stubs,
sharees, unified search)
- Basic Auth middleware with app password verification, account
lockout integration, and blake3-keyed auth cache
- App password management: create, list, revoke via both native
API (JWT-authenticated profile page) and Nextcloud OCS endpoints
- Nextcloud file ID mapping (oc:fileid) with persistent DB storage
- Chunked upload support (Nextcloud v2 chunking protocol)
- Trashbin WebDAV interface
- Avatar (SVG placeholder) and preview (redirect) handlers
- User profile page with app password management UI
- URL user validation on all DAV routes (403 on mismatch)
- Database schema for app_passwords and nextcloud_object_ids tables
All services are behind a `nextcloud.enabled` config flag and
cleanly separated under src/interfaces/nextcloud/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The embed-assets commit made locales_path optional, and main.rs was
updated to pass None. Removing that commit restored the PathBuf
signature but left main.rs passing None. Restore the original
locales_path initialization.
admin_guard() only checked Authorization: Bearer header, ignoring the
oxicloud_access HttpOnly cookie used by browser sessions. All admin
endpoints (dashboard, OIDC settings, etc.) returned 401 for logged-in
users. Fall back to cookie_auth::extract_cookie_value() when no Bearer
token is present.
wopiEditor.canEdit() is async but was called without await, so the
returned Promise was always truthy — routing every file click to the
WOPI editor (which 404'd). Made openFile() async and added await in
both ui.js and inlineViewer.js.
Added <img> thumbnail elements to _createFileCard() and
_createFileItem() for image files, loading from
/api/files/{id}/thumbnail/icon with lazy loading and error fallback.
The protected auth routes (/me, /change-password, /logout) were merged
with public routes in auth_handler.rs but never had auth middleware
applied in main.rs — so the CurrentUserId extractor always failed with
401. Split auth_routes() into auth_public_routes() and
auth_protected_routes(), applying auth + CSRF middleware to the latter.
Also added credentials: 'same-origin' to all 13 fetch calls in admin.js
so the browser sends HttpOnly auth cookies with requests.
The admin setup form was calling /api/auth/register which creates a
regular user (role is hardcoded to User) and never sets the
system_initialized flag. Switch to /api/setup which creates an actual
admin and marks the system as initialized. Add setup token input field.
Profile page showed "Not Authenticated" because fetch calls to
/api/auth/me and /api/auth/change-password were missing
credentials: 'same-origin', preventing HttpOnly cookies from being sent.
The logout function fired a non-awaited POST /logout then immediately
redirected to /login. The login page's session probe would find the
cookies still valid and refresh the token, redirecting back to the app.
Fix by awaiting the fetch and clearing local state before redirect.
Since the blob storage migration (3c7c16f), thumbnail generation failed
with "No such file or directory" because the handler constructed logical
file paths that don't exist on disk. Resolve the actual blob path via
get_blob_hash() + dedup_service.blob_path() in both upload and get
thumbnail handlers. Add regression tests.
- Enable HTTP/2 auto-detection in axum (add 'http2' feature)
- Remove direct hyper dependency with 'full' features (already transitive via axum/reqwest)
- Replace std::sync::RwLock with tokio::sync::RwLock in oidc_service.rs and file_system_i18n_service.rs to prevent async deadlocks
1. Share password bypass (HIGH): enforce password check in get_shared_link_by_token,
verify_shared_link_password now returns ShareDto only on correct password.
2. WebDAV MOVE ownership (MEDIUM): add assert_owner on destination parent folder
for file moves in both PathResolver and legacy branches.
3. Path traversal defense-in-depth (LOW): add reject_path_traversal() to WebDAV,
CalDAV, and CardDAV handlers rejecting '..' segments at HTTP boundary.
4. Setup race condition (LOW): atomic INSERT ... ON CONFLICT DO NOTHING in
try_claim_initialization prevents duplicate admin creation.
- 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
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
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
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
- 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
Replace push_str(&format!(...)) pattern in generate_full_calendar_ical,
generate_event_ical, and generate_vevent with direct write!() into a
pre-sized String buffer.
Before: ~5N+1 heap allocations for N events (temporary Strings created
by format!(), copied into the main buffer, then dropped).
After: 1 allocation (the initial String::with_capacity). All write!()
calls format directly into the destination buffer with zero intermediate
Strings.
All content from 003_add_device_codes.sql and 004_add_trigram_indexes.sql
was already absorbed into db/schema.sql (the single source of truth).
The migrations had additional problems:
- Broken numbering (started at 003, missing 001/002)
- 004 used CREATE INDEX CONCURRENTLY which fails inside sqlx transactions
- No production flow ever invoked the migrate binary
Removed: db/migrations/, src/bin/migrate.rs, migrations Cargo feature,
[[bin]] migrate target, and doc/database-migrations.md.
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.
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).
Cache successful Basic Auth verifications for 30s using blake3(username:password)
as cache key. This eliminates repeated Argon2id computation (~3-5ms CPU + 64MiB
RAM) and 3 PostgreSQL round-trips per DAV request from the same client.
- Add moka::future::Cache<[u8;32], CachedBasicAuthResult> field
- Cache hits return in ~200ns vs ~5ms (25,000x improvement)
- Failed verifications are never cached (brute-force protection intact)
- revoke() invalidates all cached entries for the affected user
- ~1.6MB max memory footprint (10,000 entries)
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
Eliminates full table scans on all text search queries by enabling
pg_trgm extension and creating GIN indexes with gin_trgm_ops on
every column used in LIKE/ILIKE '%text%' patterns.
Changes:
- Add pg_trgm extension to schema.sql
- Add 10 GIN trigram indexes: contacts (full_name, first_name,
last_name, nickname, organization, email::text, phone::text),
calendar_events (summary), files (name), folders (name)
- Unify all LOWER(col) LIKE patterns to col ILIKE — eliminates
.to_lowercase() allocation in Rust and ensures index match
- Add minimum 3-char guard on search queries so PostgreSQL uses
the trigram index instead of falling back to sequential scan
- Add migration 004 with CONCURRENTLY for zero-downtime upgrades
Expected improvement: 100-500x faster text searches on large
datasets (e.g. 100K contacts: ~1.5s → ~3ms).
https://claude.ai/code/session_01QpWV7HXAagdZfyefUw6wKC
- 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)
- 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)