Commit Graph

18 Commits

Author SHA1 Message Date
Diocrafts 05108d3e12 perf: denormalize media_sort_date + pre-scale video thumbs
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)
2026-03-07 20:12:31 +01:00
Diocrafts fc5f101e61 perf(thumbnails): cache-first + ETag 304 eliminates DB queries on GET
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
2026-03-07 19:48:35 +01:00
Diocrafts db93b48149 feat: client-side video thumbnail generation (no ffmpeg)
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.
2026-03-07 18:55:44 +01:00
Jared Wolff 6a84a5c44e fix(photos): fix SQL type mismatch, pagination panic, missing icons, and add day/month/year grouping
- Fix photos endpoint 500: remove ::uuid cast on user_id WHERE clause (VARCHAR column)
- Fix pagination underflow panic when total_pages is 0
- Add missing 'images' and 'play' icons to SVG icon registry
- Add day/month/year grouping toggle with localStorage persistence
- Improve grid spacing and group header styling per mode
- Add i18n translations for grouping labels (all 9 locales)
2026-03-05 17:32:28 -05:00
Jared Wolff 69fe3a8b07 feat(photos): add EXIF metadata extraction and storage
Extract EXIF orientation, GPS coordinates, camera info, and timestamps
from uploaded images using kamadak-exif. Store metadata in a new
file_metadata PG table. Apply EXIF orientation to thumbnail generation
so images display correctly. Add /api/files/{id}/metadata endpoint.
2026-03-05 17:32:28 -05:00
Dionisio f2d35ca792 feat: auto-persist JWT secret, remove setup token requirement
- JWT secret auto-generates and persists to <STORAGE_PATH>/.jwt_secret
- Remove setup token: first admin setup is open until system initialized
- Fix schema.sql: move CREATE EXTENSION pg_trgm/ltree to top
- Update login UI and auth.js to remove setup token fields
2026-03-05 22:12:53 +01:00
Claude 1b49135ca9 perf: replace dyn trait objects with concrete types to eliminate vtable overhead
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
2026-03-03 15:36:42 +00:00
Dionisio 6df68d3716 style: cargo fmt --all 2026-02-26 00:47:32 +01:00
Dionisio fb4eaf2cf9 perf: eliminate double disk read in thumbnail generation (read-once buffer) 2026-02-26 00:32:02 +01:00
Dionisio 97cf6402e2 fix: resolve all clippy warnings and enforce cargo fmt
- display_helpers: convert module doc-comments to regular comments,
  merge identical text/markdown + text/ branches
- search_service: replace needless range loops with slice-based pagination
- folder_repository, folder_db_repository: collapse nested if statements
- favorites_pg_repository: remove unnecessary borrow on generic arg
- file_blob_read_repository: collapse 6 nested if-let blocks
- file_blob_write_repository: collapse nested if for dedup ref decrement
- chunked_upload_service: use div_ceil(), collapse 2 nested if blocks
- folder_handler: collapse nested if-let for owner check
- webdav_handler: replace 7x io::Error::new(ErrorKind::Other, ..) with
  io::Error::other(..)
- cargo fmt applied to all files

Passes: cargo clippy --all-targets --all-features -- -D warnings
2026-02-25 10:28:34 +01:00
Dionisio 6aa38d0d24 perf: thumbnail semaphore, WOPI streaming, store_bytes guard, spawn_blocking SHA-256
- Issue #1: Add Semaphore(4) + 50MP resolution guard to thumbnail_service
  Bounds peak RAM from 4.8GB (50 uploads) to 384MB (4 concurrent decodes)
- Issue #3: Migrate WOPI PutFile from Bytes to streaming temp file + SHA-256
  RAM per WOPI PUT: ~100MB → ~256KB regardless of file size
- Issue #3: Add 10MB guard in dedup store_bytes (defense-in-depth)
- Issue #5: Move chunked upload assembly to spawn_blocking (sync I/O)
  Frees Tokio workers during SHA-256 hashing (~130ms for 500MB)
- Clean up unused tokio imports (OpenOptions, BufWriter)
2026-02-24 16:11:52 +01:00
Dionisio d907a3eb63 perf(thumbnails): load image once for all 3 sizes in background gen
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
2026-02-24 12:41:07 +01:00
Dionisio 677b3eafa9 perf: use adaptive filter for thumbnails (Triangle/CatmullRom instead of Lanczos3)
- Icon (150px): Triangle filter (~5x faster)
- Preview (400px): CatmullRom filter (~2.5x faster)
- Large (800px): CatmullRom filter (~2.5x faster)
- Quality difference is imperceptible at these resolutions
2026-02-24 00:44:27 +01:00
Dionisio ab0c4476b3 perf: fix thundering herd in ThumbnailService with moka entry()
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)
2026-02-23 23:22:54 +01:00
Diocrafts 92e0364a60 fix: OOM protection, lock-free thumbnail cache, OIDC JWKS TTL
- Streaming WebDAV PUT: body spooled to tempfile with incremental SHA-256,
  peak RAM ~64KB regardless of file size (Solution 2)
- RequestBodyLimitLayer (1MB) on CalDAV/CardDAV routers (Solution 3)
- All body::to_bytes(body, usize::MAX) replaced with explicit limits:
  PROPFIND/PROPPATCH/LOCK → 1MB, MKCOL → 4KB
- Added AppError::payload_too_large (HTTP 413)
- Added max_upload_size to StorageConfig (default 10GB, env override)
- New streaming update chain: FileWritePort::update_file_content_from_temp
  → FileUploadUseCase::update_file_streaming
- ThumbnailService: migrated from RwLock<LruCache> to moka::future::Cache
  with weight-based eviction — eliminates lock contention on read hot-path
- OIDC: discovery + JWKS caches now expire after 1 hour (Cached<T> wrapper)
  so IdP key rotation no longer requires server restart
2026-02-22 23:28:03 +01:00
Dionisio 4c98c5a657 style: apply cargo fmt to entire codebase
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.
2026-02-14 01:29:34 +01:00
Diocrafts a82faa5eaf refactoring hexagonal and clean architecture 2026-02-08 13:40:23 +01:00
Dionisio 8f2b0a354c big refactoring 2026-02-03 17:59:04 +01:00