The sidebar CSS had incorrect :nth-child() selectors for nav item colors.
When the Photos menu item was added, the CSS wasn't updated to account
for the new 6th position of the Trash item.
Changes:
- Add color styling for Photos nav item (5th child, pink)
- Update Trash nav item to use :nth-child(6) instead of :nth-child(5)
- Add active state colors for both Photos and updated Trash positions
Fixes#194
Fix two issues causing login loop after successful admin setup:
1. CSP blocking inline styles: The frontend JavaScript dynamically sets
inline styles (e.g., element.style.display = 'none') for UI state
management. The CSP header only allowed 'self' for style-src, blocking
these dynamic styles. Added 'unsafe-inline' to style-src directive.
2. Session refresh 401 errors: The cookie Secure flag defaulted to true
when OXICLOUD_BASE_URL was not set, causing cookies to not be sent
over HTTP in Docker deployments. Changed the default to false when
the base URL is not explicitly set to HTTPS, with clear logging to
guide users to set OXICLOUD_COOKIE_SECURE=true for production.
Fixes#203
The constant 10 * 1024 * 1024 * 1024 (10 GB) overflows on 32-bit systems
where usize is 32-bit (max ~4GB). This caused compilation failures on
ARMv7 architecture.
Fix by using architecture-appropriate limits:
- 64-bit: 10 GB (unchanged)
- 32-bit: 1 GB (safe maximum for 32-bit usize)
Fixes#206
The database schema defines owner_id as UUID, but the Calendar entity
stored it as String. This caused a type mismatch error when creating
calendars via CalDAV clients like DAVx:
column "owner_id" is of type uuid but expression is of type text
Changes:
- Change Calendar.owner_id from String to Uuid
- Update new() and with_id() to accept Uuid
- Update owner_id() getter to return &Uuid
- Update belongs_to() to accept &Uuid
- Update calendar_storage_adapter to pass Uuid directly
- Update tests to use Uuid::new_v4()
Fixes#200
Patch denial-of-service vulnerability where invalid QUIC transport
parameters could cause a panic in quinn-proto.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Nextcloud Android client compares ETags before and after upload to
verify its write landed. OxiCloud was returning the stable file UUID as
the ETag, which never changed on content updates, causing false
SYNC_CONFLICT errors on every upload.
Five fixes applied:
1. Thread blob_hash (SHA-256) through File entity, FileDto, all read/write
queries, and all WebDAV/PROPFIND responses as the ETag — changes on
every content update, no DB migration needed.
2. Honor X-OC-Mtime header: parse the client-supplied mtime and use it
for updated_at via COALESCE(to_timestamp($n), NOW()).
3. Disable phantom checksum capability (preferredUploadType/supportedTypes)
that the server never actually implemented, stopping retry loops.
4. Add nc:creation_time and nc:upload_time to PROPFIND responses.
5. Return oc-etag header in chunked upload MOVE (assemble) responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
target-cpu=native causes SIGILL in CI because the build script is
compiled with CPU-specific instructions that the Docker builder
doesn't support. Docker images should use generic CPU targets for
portability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The trash nav handler manually set view flags instead of calling
setCurrentSection('trash'), which meant the photos container was
never hidden when switching to trash. Use the central view-switching
function so all other views (photos, shared) are properly cleaned up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the storage.files table already exists from a manual migration,
CREATE TABLE IF NOT EXISTS is skipped entirely, so the new
media_sort_date column is never added. Add an ALTER TABLE ADD COLUMN
IF NOT EXISTS immediately after the CREATE TABLE to handle both fresh
and pre-existing databases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI and Docker builds broke after removing db/ in favor of migrations/.
- ci.yml: point psql init at migrations/20260307000000_initial_schema.sql
- Dockerfile: COPY migrations instead of db; remove unused db copy in final stage
- docker-build.yml: remove db/schema.sql presence check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pre-commit section: cargo fmt then cargo clippy before committing
- Update db/schema.sql references to migrations/ (sqlx migrations)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual schema.sql application with sqlx's built-in migration
system. Migrations are embedded at compile time and tracked in the
_sqlx_migrations table. Pending migrations run automatically on startup.
- Move db/schema.sql → migrations/20260307000000_initial_schema.sql
- Remove apply_schema() and split_sql_statements() from db.rs
- Add run_migrations() using sqlx::migrate!() macro
- Remove docker-compose schema.sql mount (app handles it now)
- Enable sqlx "migrate" feature in Cargo.toml
Future schema changes: add a new timestamped .sql in migrations/.
Closes#190
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix all clippy lints (collapsible if, clone on Copy, needless borrow,
redundant bindings, unused params) and apply rustfmt across the codebase.
Update test mocks to match Uuid-based trait signatures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CREATE TRIGGER statement is not idempotent — it fails on restart
when the trigger already exists. Add the same DROP/CREATE guard used
by trg_files_decrement_blob_ref.
Replace the innerHTML full-rebuild in _render() with two paths:
- _renderFull(): used for first load, group-mode change, and deletions
- _appendBatch(n): append-only for infinite-scroll pages — O(batch)
instead of O(total). Existing <img> nodes are never destroyed,
eliminating the visual flash and unnecessary DOM churn.
Also:
- Extract _renderTile() helper (DRY tile HTML generation)
- Extract _observeSentinel() helper
- Scope _setupVideoThumbnails(startIndex) to only process new tiles
- Add data-group attribute on headers for efficient CSS.escape lookup
- Fix stale WebP references in comments (now JPEG)
- Add virtual scrolling idea to TODO-LIST.md for future evaluation
Replace all 3 ImageFormat::WebP encode sites with JpegEncoder q=80.
Update fast-path to detect JPEG SOI instead of RIFF/WEBP magic.
Change file extension .webp -> .jpg, Content-Type headers, and
browser toBlob. Remove unused ImageFormat import and stale comments.
The webp feature stays for DECODING uploaded WebP images.
Schema (media_sort_date denormalization):
- Add media_sort_date column to storage.files with DEFAULT created_at
- Add trigger sync_media_sort_date: when file_metadata is upserted,
copies COALESCE(captured_at, created_at) into files.media_sort_date
- Add partial index idx_files_media_timeline on (user_id, media_sort_date DESC)
WHERE NOT is_trashed AND media type -- enables Index Scan + Limit (no Sort)
- copy_folder_tree now copies media_sort_date for copied files
- Remove dead idx_file_metadata_captured (no longer needed)
Query optimization (list_media_files):
- Rewrite to use fi.media_sort_date instead of COALESCE(fm.captured_at,...)
- Eliminates LEFT JOIN file_metadata -- one fewer table touch
- Plan: Limit to Index Scan O(LIMIT) instead of Sort O(N)
Video thumbnail pre-scaling (client + server):
- JS: pre-scale canvas to max 400px before toBlob -- 22x less RAM, 15x less BW
- Rust: fast-path in store_external_thumbnail -- if payload is already
WebP with dims within max_dim, store as-is (zero decode, zero encode)
Solution A — Cache-first path:
- get_thumbnail now tries moka (RAM) → disk BEFORE any DB query
- Ownership was verified at creation time; UUIDv4 prevents enumeration
- 146-photo timeline: 0 SQL queries instead of 146 per page load
Solution C — ETag / If-None-Match short-circuit:
- Deterministic ETag 'thumb-{id}-{size}' on every response
- If browser sends matching If-None-Match → 304 with zero I/O
- Cache-Control: immutable prevents even conditional requests
Additional improvements:
- Remove moka TTL (was 600s); thumbnails are immutable, weight-only eviction
- generate_all_sizes_background populates moka after disk write;
first GET after upload served from RAM with zero disk I/O
- DB path only taken on cache miss for images needing generation
The double-submit CSRF middleware blocks all mutating requests
without the X-CSRF-Token header. photos.js was uploading video
thumbnails via PUT without it, causing a flood of 403 errors.
- Add getCsrfHeaders() spread to the fetch headers
- Add credentials: 'same-origin' to ensure cookie is sent
Backend:
- 204 response: add Cache-Control: no-store so browser never caches
'no thumbnail yet' — next GET after PUT upload gets the WebP
Frontend:
- _videoThumbCache (Map): persists fileId → URL across re-renders,
so switching tabs reuses cached URLs instantly (no re-decode)
- Render: videos with cached URL skip the 204/error/decode cycle
- After PUT succeeds: swap blob URL → server ?v=1 URL so blob is GC'd
- Concurrency throttle: max 3 simultaneous video decodes to avoid
overwhelming network + CPU when gallery has many videos
- Decode queue: pending videos processed as slots free up
Backend:
- get_thumbnail: for non-images, serve cached thumb or 204 (not 400)
- store_external_thumbnail: validate + re-encode to WebP + persist
- get_cached_thumbnail: memory → disk lookup without generation
- PUT /api/files/{id}/thumbnail/{size} endpoint for client uploads
- ThumbnailPort trait: add get_cached_thumbnail + store_external_thumbnail
Frontend (photos.js):
- On <img> error for video tiles, use <video> + <canvas> to extract
a frame at 25% duration using browser's native codec
- Show frame immediately via blob URL
- Fire-and-forget PUT to server for permanent caching
- Subsequent visits serve cached WebP instantly (no re-extraction)
Zero server-side dependencies — CPU distributed across clients.
- frame-src: add 'blob:' (wildcard * only matches network schemes)
- media-src: new directive with 'self' blob: (was falling back to default-src)
- Fixes: PDFs blocked in iframe, videos blocked by media-src fallback