Commit Graph

188 Commits

Author SHA1 Message Date
Edouard Vanbelle cba9be8c21 feat(rebac): first pass 2026-05-20 22:56:00 +02:00
Edouard Vanbelle dfb082fdf4 refactor(server): file_management_service: move all method without owner check into private, add folder_ports 2026-05-20 15:39:53 +02:00
Edouard Vanbelle f8b30e78a6 refactor(create_folder): add an ownership check while creating a folder + refactor code 2026-05-20 12:59:04 +02:00
Edouard Vanbelle 79a12018b1 fix(folder_service): propagate 409 error if folder aleady exists 2026-05-19 10:18:40 +02:00
Dionisio Pozo 1e34c5f3f8 Merge pull request #330 from EdouardVanbelle/fix/thumbnail-on-update
bugfix/thumbnails on update
2026-05-14 00:38:03 +02:00
Edouard Vanbelle d85b8055b8 refactor(services): add file_lifecycle and blob_lifecycle
- complete src/application/ports/blob_lifecycle.rs with traits:
    * BlobCreationHook
    * BlobDeletionHook

 - add src/application/ports/file_lifecycle.rs with traits:
    * FileCreatedHook
    * FileDeletedHook
    * FileUpdatedHook
2026-05-14 00:18:12 +02:00
Dionisio Pozo 412e31388d Merge pull request #347 from abnvle/feat/share-folder-browsing 2026-05-14 00:07:45 +02:00
Edouard Vanbelle 28e25f9d16 fix+test: check hash ref count on copy-on-write (a duplicated beeing updated) 2026-05-13 15:55:41 +02:00
Edouard Vanbelle 78cb37b311 feat: check thumbnail cleanup on files deletion + correct ref counter 2026-05-13 15:55:38 +02:00
Edouard Vanbelle 1afacb65cd style(srv): correct clippy recos 2026-05-12 00:34:11 +02:00
Edouard Vanbelle b4d4056a15 fix/security: move_folder(): check that target belongs to caller 2026-05-12 00:29:04 +02:00
Edouard Vanbelle 3a18507158 fix/security: copy_file_owned(): ensure that target is also owned by the user 2026-05-12 00:28:42 +02:00
Edouard Vanbelle 2f49daa4ee feat(folders): implement copy_folders taking care of ownership 2026-05-12 00:22:51 +02:00
abnvle e8399443e9 Merge remote-tracking branch 'origin/main' into feat/share-folder-browsing
# Conflicts:
#	static/js/views/public/publicShare.js
2026-05-10 20:46:29 +02:00
Edouard Vanbelle 89ec0d2c24 feat(contacts): add API to address-books, contacts, groups
* full gateway to /carddav
 * add system readonly address-book issued from OxiCloud users
2026-05-10 00:39:25 +02:00
Edouard Vanbelle 7697f3d35b feat(ui): handle errors on folder creation or folder/file renaming
- protect file_management_service::rename_file with validate_storage_name
 - remove specific rename modal and use the generic modal class (less duplicate)
 - handle errors on modal action: do not close the modal on error and display this error
 - hide "Go to parent folder" contextMenu if section is files and folder is the same as current one
2026-05-09 00:06:36 +02:00
Dionisio Pozo 52f4a865a9 Merge pull request #349 from EdouardVanbelle/feat/item-path-tool-tip+go-to-parent
show item path on Recent + Favorite, add go to parent folder, fix Folder browsing in Favorites
2026-05-08 01:22:08 +02:00
Dionisio Pozo fec9bb1f96 Merge pull request #351 from EdouardVanbelle/security/refresh-token-rotation
security: prevent re-use of refresh token (reduce surface for any stolen token)
2026-05-08 01:21:27 +02:00
Edouard Vanbelle 6696ba3ae4 fix(storage names): enure folder and file names are not using reserved chars
- create common function `validate_storage_name()` to check files & folder name
 - replace previous duplicate check with this one
 - better check on create_folder rename_folder (was only checking non empty)
 - use correct error class to ensure 400 on API (was 500)

fix: #345
note: no unit test here, but plan e2e test for that
next: improve UI to display error
2026-05-08 00:26:44 +02:00
Edouard Vanbelle b90fa6f619 security: prevent re-use of refresh token (reduce surface for any stolen token)
Security: session hardening

  Refresh token rotation with theft detection (family_id)
  - Added family_id column to auth.sessions (migration 20260507000000_session_family.sql) grouping all tokens issued from the same login into a family
  - On refresh, the new session inherits the parent's family_id
  - If a revoked token is replayed (indicates the token was stolen after rotation), the entire family is immediately invalidated and a warning is logged — forcing re-authentication on all devices

  SameSite=Strict on refresh cookie
  - Access cookie stays SameSite=Lax (needed for top-level navigation)
  - Refresh cookie upgraded to SameSite=Strict — it is only ever used for explicit POST to /api/auth/refresh, never via cross-site navigation

  Refresh token TTL: 30 days → 7 days
  - With rotation, active sessions auto-renew and effectively never expire
  - Inactive sessions expire after 7 days instead of 30, reducing the theft window
2026-05-07 13:57:10 +02:00
Edouard Vanbelle 8e1e9fe201 show item path on Recent + Favorites, add go to parent folder, fix Folder browsing in Favorites 2026-05-06 23:36:05 +02:00
abnvle d15d7b8f8e feat(share): public folder browsing API + range support + zip
Five new public endpoints under /api/s/{token}/...:
  GET /contents
  GET /contents/{folder_id}
  GET /file/{file_id}
  GET /zip
  GET /zip/{folder_id}

All honour the unlock cookie from /verify, so password-protected
folder shares work end-to-end.

Folder/file IDs are validated against the share subtree via a single
ltree containment query (O(log N) on the existing GiST index).
Out-of-scope IDs return 404.

download_shared_file refactored to a Range/304/206/416-aware
serve_share_file helper, shared with the new /file/{file_id}
endpoint. content_disposition extracted from FileHandler so RFC 5987
formatting is identical across auth and share download paths.
2026-05-05 22:41:55 +02:00
abnvle 3bc5c0873b fix(share): password-protected downloads via signed unlock cookie
After successful POST /api/s/{token}/verify, the server issues a
short-lived signed JWT cookie (oxi_share_unlock_<token>; HttpOnly;
SameSite=Lax; 1h TTL). Subsequent /api/s/{token} and
/api/s/{token}/download requests honour the cookie to bypass the
password gate, so password-protected file downloads work end-to-end.

- New module src/infrastructure/services/share_unlock_cookie.rs:
  issue_jwt, verify_jwt, extract_from_cookie_header, build_set_cookie
  + 10 unit tests.
- New ShareService methods issue_unlock_jwt and
  get_shared_link_with_unlock; trait method get_shared_link_by_token
  delegates to a private fetch_share_resolved with an
  allow_password_protected flag.
- access_shared_item, verify_shared_item_password, and
  download_shared_file honour the unlock cookie.

Reuses OXICLOUD_JWT_SECRET, no new env var. Auth-token JWTs and
unlock-cookie JWTs cannot be confused: auth requires
username/email/role/jti claims, unlock has only sub/exp/iat.
Cross-share replay rejected via claims.sub == requested_token check.
2026-05-05 21:56:49 +02:00
Dionisio Pozo 1cb541dca2 Merge pull request #331 from EdouardVanbelle/feat/full-openapi-coverage 2026-05-04 22:08:33 +02:00
Edouard Vanbelle 6e1c435098 style(server): correct clippy warnings 2026-05-04 12:58:30 +02:00
Edouard Vanbelle d0c6bd6a73 feat(openapi): upgrade code to gnerate openapi on all path 2026-05-04 12:02:33 +02:00
DioCrafts 30fd3cc488 Fix thumbnail generation for CDC blob storage 2026-04-26 11:55:05 +02:00
BillionClaw 0c4290261e fix(auth): sanitize claims.sub when padding/truncating OIDC usernames
When OIDC providers (e.g. Keycloak) use email addresses as usernames or
when claims.sub contains @ or other invalid characters, the username
padding and collision-suffix logic could introduce invalid characters.

The fix filters claims.sub through the same allowed-character filter
before using it in username construction.

Fixes DioCrafts/OxiCloud#259
2026-04-18 07:26:36 +00:00
BillionClaw b2f0cec6bc fix(auth): handle email-style usernames in OIDC JIT provisioning 2026-04-17 18:10:17 +00:00
Diocrafts 761d159a92 feat(dedup): CDC sub-file deduplication with FastCDC + parallel chunk storage + dedup skip
- Replace whole-file SHA-256 dedup with FastCDC 2020 content-defined chunking
  (min 64KB, avg 256KB, max 1MB) + BLAKE3 hashing
- Add chunk_manifests table (file_hash → chunk_hashes[] + chunk_sizes[])
- Add put_blob_from_bytes to BlobStorageBackend trait (all 7 backends)
- 3-phase store_chunks pipeline:
  Phase 0: batch-check existing chunks (single PG query)
  Phase 1: selective disk read (skip existing chunks entirely)
  Phase 2: parallel upload with buffer_unordered(8)
- CDC-aware read_blob_stream and read_blob_range_stream with legacy fallback
- Transactional manifest + chunk ref-count cascade on remove_reference
- 12 CDC tests (determinism, reassembly, contiguity, sub-file dedup, etc.)
- Update deduplication.md to reflect new architecture
2026-04-14 23:17:39 +02:00
Diocrafts cd3733b459 feat: pluggable storage backends (S3, Azure, local) with admin UI
Implement 4-phase external storage backends architecture:

Phase 1 - Foundation:
- BlobStorageBackend trait (application/ports/blob_storage_ports.rs)
- LocalBlobBackend: extracted all tokio::fs ops from DedupService
- S3BlobBackend: AWS SDK with custom endpoint support (MinIO, R2, B2)
- DedupService refactored to use Arc<dyn BlobStorageBackend>

Phase 2 - Admin Panel:
- StorageSettingsService with DB persistence + env override
- Storage tab in admin panel (backend selector, S3 form, provider presets)
- GET/PUT/POST endpoints for storage settings + connection test
- i18n keys (en/es) and BEM CSS

Phase 3 - Migration:
- MigrationBlobBackend decorator (dual-read: target-first + source fallback)
- Background migration job with parallel transfers + progress tracking
- Migration UI (progress bar, ETA, pause/resume/verify/complete)
- 6 admin API endpoints for migration lifecycle

Phase 4 - Enterprise Extras:
- CachedBlobBackend: LRU disk cache for remote backends
- EncryptedBlobBackend: AES-256-GCM at-rest encryption
- AzureBlobBackend: Azure Blob Storage support
- RetryBlobBackend: exponential backoff for transient errors
- Decorator composition in DI: retry → encryption → cache

All 223 tests passing, clippy clean, fmt verified.
2026-04-14 21:33:38 +02:00
Diocrafts edf1e66989 feat: add public share page with download support (#253)
Share links now point to /s/{token} (was /api/s/{token}) and render a
proper HTML page instead of raw JSON.

Changes:
- static/share.html: standalone public share page
- static/css/views/share-public.css: share page styles
- static/js/views/public/publicShare.js: client-side logic that fetches
  share metadata via /api/s/{token}, handles password-protected shares,
  and renders file download / folder info
- build.rs: include share.html in the HTML embed pipeline
- web/mod.rs: serve /s/{token} route (unauthenticated)
- share_dto.rs: generate URLs as /s/{token} instead of /api/s/{token}
- share_handler.rs: new download_shared_file() handler that validates the
  share token and streams file content without requiring authentication
- routes.rs: mount GET /api/s/{token}/download (public, uses AppState)
2026-04-12 01:51:30 +02:00
Diocrafts 2dde4da5cf feat: thumbnail dedup — store thumbnails by blob_hash instead of file_id (#233)
Thumbnails are now keyed by blob_hash on disk so identical files share
a single set of thumbnails (icon/preview/large). For 4000 duplicate
files with the same content, this reduces thumbnail storage from 12,000
files to just 3.

Changes:
- get_thumbnail_path() keys by blob_hash instead of file_id
- get_thumbnail(), get_cached_thumbnail(), generate_all_sizes_background()
  accept blob_hash parameter for disk dedup
- generate_all_sizes_background() fast path: if blob-hash thumbnails
  already exist on disk, skip image processing entirely and just
  populate moka cache for the new file_id
- delete_thumbnails() only invalidates moka cache (shared disk
  thumbnails must not be deleted when one file is removed)
- delete_blob_thumbnails() added for GC; garbage_collect() now cleans
  up orphaned thumbnail files alongside blob files
- External thumbnails (video frames) stored as ext-{file_id}.jpg
  since they are client-generated and not dedup-able
- ThumbnailPort trait updated with blob_hash parameters
- All handler call sites updated (file_handler, preview_handler)
- Tests updated for new signatures
2026-04-12 00:50:10 +02:00
Diocrafts 63bcd0ffe7 perf: non-blocking hash + parallel thumbnail generation
- Replace blocking blake3::hash() with DedupService::hash_file() in
  file_upload_service (create_file, update_file). Uses spawn_blocking +
  mmap_rayon instead of blocking the Tokio async worker.
- Parallelize thumbnail resize+encode with rayon par_iter() for the 3
  sizes (Icon, Preview, Large) inside spawn_blocking.
2026-04-11 19:13:48 +02:00
Diocrafts d32a32c30a perf: add TTL/TTI to content cache and invalidate on file mutations 2026-04-11 17:39:17 +02:00
Diocrafts 81f33458e0 perf: streaming hash-on-write dedup upload, remove dead code 2026-04-11 16:17:47 +02:00
Andrey Tkachenko da066f47fa Music Player & Playlist Manager 2026-04-08 15:14:03 +03:00
Edouard Vanbelle badf35f08f chore: remove all executable attributes on non bash files 2026-04-01 23:14:42 +02:00
iltumio bf7e030cd6 feat: add OpenAPI spec generation with utoipa and justfile
- Add utoipa v5 dependency with ToSchema derives on all REST API DTOs
- Annotate free-function handlers with #[utoipa::path] (trash, share, favorites, recent)
- Create ApiDoc struct with OpenApi derive registering 37 schemas across 7 tags
- Add generate-openapi binary outputting resources/gen/openapi.json
- Serve OpenAPI spec at GET /api/openapi.json (public, no auth)
- Add justfile with common dev commands (build, test, lint, check, openapi, db)
2026-03-29 18:49:10 +02:00
Edouard Vanbelle d78b279ec9 style: apply rust format + fix clippy warning 2026-03-26 10:05:44 +01:00
BillionClaw 5cf0266d24 fix(auth): cap admin initial quota to available disk space
The admin_create_user method was using hardcoded quota values (100GB for
admin, 1GB for user) instead of the capped_quota method that checks
available disk space. This could result in setting a quota higher than
the actual available disk space.

Fixes #92
2026-03-18 11:06:39 +08:00
Dionisio Pozo 775abab8e1 Merge pull request #224 from BillionClaw/clawoss/fix/file-move-folder-ownership
fix(file): verify target folder ownership on move
2026-03-17 09:57:28 +01:00
BillionClaw 30d6e53fca fix(file): verify target folder ownership on move
When moving a file to a folder, verify that the caller owns the target
folder. Without this check, a file could be moved to another user's
folder, causing the file to "disappear" from the original user's view
since file listings filter by user_id.

- Add folder_repo to FileManagementService
- Add verify_target_folder_owner() method
- Call it in move_file_owned() before moving
2026-03-17 16:53:55 +08:00
BillionClaw 852db02cd0 fix(share): correct shared link URL to include /api prefix
The shared link URL was generated as {base_url}/s/{token} but the
API endpoint is actually at /api/s/{token}. This caused 404 errors
when users accessed shared links.

Fixes #101
2026-03-17 16:48:11 +08:00
BillionClaw 32c5630a5f fix(trash): add missing display fields to TrashedItemDto
Add category, icon_class, and icon_special_class fields to TrashedItemDto
to fix trash view rendering issues. The frontend expects these fields to
properly display file type information, icons, and deletion dates.

- category: Human-readable file category (e.g., "Image", "Document")
- icon_class: FontAwesome icon class for the file type
- icon_special_class: Special CSS class for icon styling

Fixes #107
2026-03-17 16:08:12 +08:00
BillionClaw 665220dd0b fix(files): batch folder deletion fails
Add debug logging to diagnose batch trash failures for folders.
When batch trash operations fail, the specific error was not being
logged, making it difficult to diagnose issues like #124.

- Add tracing::debug! logs for failed file and folder trash operations
  in batch_operations.rs
- Add unit tests for batch operation result handling to verify
  correct success/failure counting

Fixes #124
2026-03-17 11:21:16 +08:00
Jared Wolff 1fcd02a519 Fix Nextcloud sync conflict by using content-hash ETags
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>
2026-03-15 14:14:24 -04:00
zjean 18518bedaf fix: resolve clippy warnings and rustfmt issues for CI compliance
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>
2026-03-09 14:34:07 +01:00
Diocrafts 661c9cb688 fix: delete thumbnails when files are permanently deleted
- Inject ThumbnailService into TrashService and FileManagementService
- Call delete_thumbnails() after permanent file deletion in:
  - TrashService::delete_permanently (single item)
  - TrashService::empty_trash (bulk: collects file IDs first)
  - FileManagementService::delete_file
  - FileManagementService::delete_with_cleanup (fallback path)
- All thumbnail cleanup is best-effort (warn on failure, never blocks)
- Prevents orphaned thumbnail files from accumulating on disk
2026-03-07 19:15:36 +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