Commit Graph

1062 Commits

Author SHA1 Message Date
Claude bf46c1ca10 perf: make StoragePath::join and File builders consume self to avoid clones
StoragePath::join deep-cloned the whole Vec<String> (every segment String)
just to append one element. Take self by value and push in place. All callers
pass owned values except PathService::create_file_path, which holds a borrow
and now clones explicitly — the same copy the old &self join already made.

File::with_name / with_folder / with_size took &self and rebuilt the struct,
cloning every carried-over field (id, mime_type, folder_id, blob_hash, ...).
Consume self and mutate only the fields that change. Behaviour is identical;
the fallible builders now drop the input on Err, which is fine for these
rename/move/resize transforms (all current callers replace the file).

Impact is small in practice — with_folder/with_size have no callers and
with_name is test-only, while the one hot join caller (create_file_path)
must copy segments regardless — but the consuming form is the idiomatic one.

Verified: cargo fmt + clippy --all-features --all-targets -D warnings clean;
domain tests (path_service::, entities::file::) pass — 30 + 6.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 13:45:16 +00:00
Claude ec8ddebc30 perf: drop intermediate allocs in WebDAV href encoding; fold group-list COUNT into one query
webdav encode_uri_path runs on every PROPFIND href and did
.map(...).collect::<Vec<_>>().join("/"), allocating a String per segment plus
a joined Vec. Write each utf8_percent_encode Display adapter straight into a
single preallocated String. Behavior is identical (split on '/', encode each
segment, join with '/'), including leading/trailing-slash edge cases.

subject_group list / list_with_counts each issued a second SELECT COUNT(*)
round-trip for the total. Fold it into the page query via COUNT(*) OVER() —
the pattern folder_db_repository already uses — halving the round-trips.
total_count is read from the first row and is 0 on an empty page, matching
folder_db_repository's documented convention.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 13:29:05 +00:00
Claude f82c5ccf47 perf(authz): bound unbounded grant-list queries with a fail-loud row cap
list_incoming_grants and list_grants_on_resource ran fetch_all with no
LIMIT, so a pathological number of grants on one resource (or targeting one
subject) would be pulled fully into memory. These back the grant-management
endpoints ("Manage sharing", "Shared with me"), not the hot require() path.

A blind LIMIT is unsafe here: apply_role reads the full grant set to compute
an add/remove diff, so a silently truncated list would be acted on as if
complete (stale grants never revoked). Instead fetch MAX_GRANT_ROWS + 1 and
reject with an audit line (authz.grant_list_rejected / reason=over_row_cap)
when the cap is exceeded, bounding worst-case RAM without ever returning a
partial set. The check is shared via PgAclEngine::guard_grant_row_cap. Cap is
10_000 — orders of magnitude above any realistic single-resource/subject
grant count.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 13:24:22 +00:00
Claude fc55e92299 perf(blob-cache,db): release index mutex before disk I/O; skip per-acquire DB ping
CachedBlobBackend held its single tokio::Mutex<LruCache> across filesystem
syscalls, serializing every concurrent cache operation behind one lock:
- get_blob_stream / get_blob_range_stream: held across File::open()/seek()
- delete_blob: held across remove_file()
- initialize: held across the full cache-dir walk
- eviction (insert + fetch paths): held across remove_file() loops

Now the lock only guards the in-memory LRU. Presence checks bump recency
and release the guard before touching the filesystem (a vanished file falls
through to the existing fetch-and-cache path, covering the race), and
eviction selects victims under the lock then unlinks them after releasing
it. The duplicated eviction loop is extracted into
CachedRef::collect_evictions.

db: set test_before_acquire(false). With warm min_connections and a bounded
max_lifetime, the liveness ping sqlx issues on every acquire() costs more
than the rare dead connection it catches; stale sockets surface as a query
error and the pool recycles them either way.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 13:18:09 +00:00
Edouard Vanbelle 233531bce5 chore(docker): fix docker healthcheck (#435)
- fix #418
2026-06-08 15:25:39 +02:00
Edouard Vanbelle 2ad55879f3 feat(nextcloud): use user's image for avatar (#434)
- add /ocs/v1.php/cloud/user route (same as /ocs/v2.php/cloud/user )
    - add also /remote.php/dav/avatars/{user}/{size}.png route (used by Nextcloud sync)

    note: webp are transcoded in png, no cache but using pragma HTTP header
2026-06-08 15:25:36 +02:00
Dionisio Pozo 6811e3e4f7 Merge pull request #437 from EdouardVanbelle/fix/nextcloud-propfind-on-chunk-upload 2026-06-07 23:04:26 +02:00
Edouard Vanbelle a3a8fdbf12 test: activate nextcloud on tests 2026-06-07 21:40:22 +02:00
Dionisio Pozo 15e69bf934 Merge pull request #436 from EdouardVanbelle/fix/ui-ellipsis-list-view 2026-06-07 20:49:54 +02:00
Edouard Vanbelle 3fc0a272ca fix(nextcloud): resume (PROPFIND) on upload chunk
PROPFIND on upload chunk is necessary for application to resume the upload
    it is used at least by Nextcloud Android app

    issue has been raised via #415

    unable to test, so I am using a playbook in test/webdav to simulate a
    PROPIND on remote.php/dav/uploads/{user}/{session}
2026-06-07 20:47:16 +02:00
Edouard Vanbelle 916b666b13 fix(ui): ensure owner ellipsis on list view 2026-06-07 20:10:05 +02:00
EdouardVanbelle b25d1593d3 test(e2e): update playwright snapshots
- due to change one91d4dbab9f644feaa6216bb6bd697156272f07e
2026-06-07 19:46:42 +02:00
Dionisio Pozo cd925704b9 Merge pull request #433 from EdouardVanbelle/fix/db-migration
fix(db-migration): fix 2 changes with same ID
2026-06-07 17:13:50 +02:00
Dionisio Pozo f170528b35 Merge pull request #432 from leofishman/fix/hide-admin-setup-link-when-initialized
fix(auth): hide admin setup link after language selection when system…
2026-06-07 17:13:24 +02:00
Edouard Vanbelle 4531ee9f15 fix(db-migration): fix 2 changes with same ID
This solve issue with 2 migrations made the same day, due to merge on pull request, DB migration is blocking

    the same version prefix:
     - 20260625000000_files_user_size_index.sql    (Dio)
     - 20260625000000_folder_tree_modified_at.sql  (Ed)
    They were renamed to ...0001 and ...0002 (disjoint versions) + protection like "IF NOT EXISTS"

    I have opt for an automated clean up of old entry:
        `DELETE FROM _sqlx_migrations WHERE version = 20260625000000;`
    runned on startup

    affected users: Dio, myself and any dev that wanted to work on this project since eb0ba58158
2026-06-07 16:32:42 +02:00
leofishman e91d4dbab9 fix(auth): hide admin setup link after language selection when system is initialized
When a new browser visits the login page, the language selector runs first.
After the user selects a language and clicks continue, the code checked
system status and correctly showed the login panel when `initialized=true`
— but did not hide the "Set up administrator" link.

That link was only hidden by `showInitialPanel()`, which returns early
(without reaching the hide logic) whenever `isFirstRun()` is true. So on
any browser that had not previously stored the locale key, the link stayed
visible and clickable, leading users back to the admin setup panel even
after an admin already existed.

Fix: hide the link in the language-continue handler's `else` branch,
mirroring the same guard already present in `showInitialPanel()`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:37:31 -03:00
Dionisio Pozo eb0ba58158 Merge pull request #426 from EdouardVanbelle/refactor/etag-centralize
refactor & normalize etag for Nextcloud + fix NFC string (important fix)
2026-06-07 01:26:30 +02:00
Dionisio Pozo 95b9e08724 Merge pull request #429 from AtalayaLabs/perf/auth-me-quota
perf(quota): stop recomputing storage usage on every GET /api/auth/me
2026-06-07 01:26:03 +02:00
DioCrafts 72715c66a0 Merge remote-tracking branch 'origin/main' into perf/auth-me-quota
# Conflicts:
#	example.env
#	src/common/config.rs
2026-06-07 01:25:14 +02:00
Dionisio Pozo 41708367d5 Merge pull request #428 from AtalayaLabs/fix/webdav-large-upload-oom
fix(upload): stream WebDAV/NextCloud PUT to disk to prevent OOM on large files
2026-06-07 01:21:51 +02:00
DioCrafts d7c6894c80 perf(quota): stop recomputing storage usage on every GET /api/auth/me
GET /api/auth/me ran a synchronous O(N) SUM(size) over all the user's
files plus an unconditional UPDATE of auth.users on every call — one of
the most frequently hit endpoints — adding per-request latency, DB write
load, dead tuples and WAL even when nothing changed.

- /api/auth/me now serves the cached storage_used_bytes column instead of
  recomputing it inline.
- New StorageUsageService::start_reconciliation_job runs a periodic sweep
  on the maintenance pool that keeps the cached value current for every
  mutation (uploads, deletes, trash), so freshness no longer depends on
  hitting /me. Interval via OXICLOUD_STORAGE_USAGE_RECONCILE_SECS (default
  600s, floored at 30s; first sweep deferred one interval to avoid boot load).
- update_storage_usage only writes when the value actually changes
  (IS DISTINCT FROM), so the sweep produces no dead tuple / WAL on no-ops.
- New covering partial index idx_files_user_size_active makes the usage
  SUM an index-only scan instead of a heap scan over all the user's files.

Also collapse the same pre-existing clippy collapsible_else_if in
carddav_handler that blocks the -D warnings gate on this base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 01:13:58 +02:00
DioCrafts 061306cc84 fix(upload): stream WebDAV/NextCloud PUT to disk to prevent OOM on large files
Large uploads (e.g. ~800 MB ISOs) could OOMKill the process, even on
dedup hits, due to three separate full-file-in-memory paths:

- NextCloud PUT (/remote.php/dav) buffered the entire body in RAM via
  body::to_bytes before any dedup logic, then re-wrote and re-hashed it.
  Now streams the body to a temp file with incremental BLAKE3 and goes
  through update_file_streaming (shared spool helper with the native
  WebDAV PUT handler); peak heap is ~one HTTP frame regardless of size.

- DedupService::store_chunks materialized every new chunk's data in a Vec
  before uploading. Now reads each new chunk by positioned I/O
  (read_exact_at, off the runtime via spawn_blocking) just before its
  upload; peak heap bounded to ~CHUNK_UPLOAD_CONCURRENCY x CDC_MAX_CHUNK.

- The upload spool used the OS temp dir, often tmpfs/RAM in containers
  where its page-cache counts against the cgroup memory limit. Add
  OXICLOUD_UPLOAD_TMPDIR to point the spool at real disk.

Also collapse a pre-existing clippy collapsible_else_if in carddav_handler.

Refs #404

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 01:03:52 +02:00
Edouard Vanbelle bde7c83932 feat(content_hash): propagate etag and hash_content to */resources 2026-06-06 20:11:13 +02:00
Edouard Vanbelle 46f8789f4f fix(folder): keep current folder view on refresh 2026-06-06 20:11:13 +02:00
Edouard Vanbelle eb895b0412 fix(webdav): fix trailing / on collection 2026-06-06 20:11:13 +02:00
Edouard Vanbelle 7db547a669 fix(name duplicate): fixed via NFC normalisation
TL;DR:

    fix duplicate filename via:

    ```
    docker exec <container> migrate-nfc-filenames --dry-run    # preview
    docker exec <container> migrate-nfc-filenames              # execute
    ```

 == issue ==

  Last week I uploaded Capture d'écran 2026-06-03 à 20.04.24.png from the web. It synced down to Nextcloud on my Mac. Two minutes later, the Web UI was showing the file twice.

  Both rows had:
  - the same name
  - the same size
  - the same content hash

  So why two rows? Because to PostgreSQL, the names weren't the same.

  Web upload (browser → Postgres):
    "é" stored as 1 codepoint  (U+00E9)        bytes: c3 a9     ← NFC

  NiextCloud client (macOS → Postgres):
    "é" stored as 2 codepoints (e + U+0301)    bytes: 65 cc 81  ← NFD

  macOS's APFS keeps filenames in NFD (decomposed); browsers send NFC (composed). Visually é and é are identical. To WHERE name = $1 they're two different keys. Our UNIQUE index on (folder_id, name, user_id) never fired — and the row count quietly drifted every time a Mac user touched an accented
  filename.

 == The fix is two halves ==

  1. No new duplicates — every name-receiving boundary (file upload, NC PUT, rename, MOVE, path lookup) now NFC-normalizes before touching the database. The storage invariant becomes "every stored name is NFC".
  2. Clean up existing data — one-shot migrate-nfc-filenames binary walks storage.files, NFC-normalizes any non-NFC row, and resolves the collisions we've accumulated. Same-content duplicates go to trash (recoverable); different-content collisions get renamed with a .duplicate suffix.

 == use of the clean up ==

    example of use (do not forget to define env **DATABASE_URL**)

    either
        `cargo run --bin migrate-nfc-filenames -- --dry-run`
    or
        `cargo build --bin migrate-nfc-filenames`
        `./target/debug/migrate-nfc-filenames --dry-run`

    example:
```
    % ./target/debug/migrate-nfc-filenames --dry-run
    === NFC filename migration (DRY RUN — no writes) ===

    Loaded 543 non-trashed file rows

    NORMALIZE  163451b5-5e6c-404b-9b1e-f4b01a2b7269  user=42433185-4717-416d-9a15-4580fff171ec  'Capture d’écran 2026-03-20 à 14.44.50.png' → 'Capture d’écran 2026-03-20 à 14.44.50.png'
    NORMALIZE  827dddec-4dd5-48c2-a120-dec5289f7d29  user=969deca6-7935-4f12-a430-4d636b62fa3e  'Capture d’écran 2026-04-03 à 15.43.38.png' → 'Capture d’écran 2026-04-03 à 15.43.38.png'
    NORMALIZE  09559934-a620-472d-9ba8-fc3cfeb6dc6f  user=a0643a21-0092-4a84-9dde-7ac4e76bc1a5  'Capture d’écran 2026-06-03 à 20.05.38.png' → 'Capture d’écran 2026-06-03 à 20.05.38.png'
    NORMALIZE  5ce6dbf9-0562-4758-8783-671aa9069590  user=a0643a21-0092-4a84-9dde-7ac4e76bc1a5  'Capture d’écran 2026-06-05 à 11.07.25.png' → 'Capture d’écran 2026-06-05 à 11.07.25.png'
    DEDUP      newer=26bcf82b-99cc-45c8-9d69-dd7e5c4484ff (trash, same blob)  older=df3adc67-a778-424d-a817-b930c75f3b06  user=a0643a21-0092-4a84-9dde-7ac4e76bc1a5  hash=0d2cc7b0ffce2850

    === Summary ===
      scanned                            : 543
      already in NFC                     : 538
      normalized in place (no collision) : 4
      dedup-trashed (same content)       : 1
      renamed to .duplicate              : 0

    DRY RUN — no rows were written. Re-run without --dry-run to apply.
```

    once valid remove --dry-run
2026-06-06 20:11:08 +02:00
Edouard Vanbelle 5a18ec4bac test(hash_content): check that hash matches 2026-06-06 18:49:18 +02:00
Edouard Vanbelle 1b9d9af5c0 feat(etag): cascade tag change on each folerd's parent
on a file or folder change, the etag of each parent will be updated, this is O(n)
    this is required to ensure NextCloud client will be aware of changes

    In the future a Cursor will be better, this will be for OxiCloud-desktop
2026-06-06 18:49:14 +02:00
Edouard Vanbelle c91515cb65 feat(etag): include mtime in file ETag formula 2026-06-06 18:49:14 +02:00
Edouard Vanbelle 0135930da9 refactor(file|folder): separate etag and blob_hash 2026-06-06 18:49:14 +02:00
Edouard Vanbelle 6aee68b34c fix(nextcloud): ensure collection have trailing / in webdav 2026-06-06 18:49:14 +02:00
Edouard Vanbelle 04a4868cd2 refactor(api): ensure trace are for all HTTP req.
- ensure Nextcloud handler has also traces
    - ensure that user_id is provisionned by the nextcloud auth method
2026-06-06 18:49:14 +02:00
Edouard Vanbelle e4bb5b064a feat(tooltip): add tooltip on users and groups 2026-06-06 18:49:14 +02:00
Dionisio Pozo c5e0800336 Merge pull request #423 from EdouardVanbelle/feat/grant-notificaion-with-coalesce 2026-06-05 15:47:39 +02:00
Edouard Vanbelle 8b08b081c5 chore(ci): check icons and i18n 2026-06-05 14:18:41 +02:00
Edouard Vanbelle 49f9a14ef0 chore(ui): complete missing icons via tools/check-icons.py 2026-06-05 14:18:41 +02:00
Edouard Vanbelle 6f9e12624b chore(tools) add script to audit translations 2026-06-05 14:18:37 +02:00
Edouard Vanbelle 540c947e61 fix(ui): better handling of new folder creation
issue: on folder creation view was wiped and displaying only the new folder
    fix: add a "new" swimlane if in group mode and scroll up to the new created folder
2026-06-05 11:25:09 +02:00
Edouard Vanbelle ca0f5b3fc5 fix(ui): ensure usermenu always in viewport
- ensure also use of only one method to position different contextmenus
2026-06-05 11:25:09 +02:00
Edouard Vanbelle ba9922bcb8 i18n: add missing translatiions 2026-06-05 11:25:09 +02:00
Edouard Vanbelle 3eb74f83b0 feat(userVignette): show by preference givenname/familyname 2026-06-05 11:25:09 +02:00
Edouard Vanbelle 8cc21f17c5 feat(notify): add notif to internal users when granted
- add coalesced protection to avoid mail bombing if an invited goes many grant in a short period
    - add resentd method in share menu item (work for both internal and external users)
    - user can disable email notification via his properties
    - add env variable from admin to disable notifications
2026-06-05 11:25:06 +02:00
Dionisio Pozo b8e8fcd7dc Merge pull request #421 from EdouardVanbelle/feat/mail-and-magic-templating 2026-06-03 19:03:49 +02:00
Edouard Vanbelle 16c5e06e2b security(/magic): no cache + no referer + no index 2026-06-03 16:19:03 +02:00
Edouard Vanbelle d678d3e31a feat(ui): use same Oxicloud theme for magic-link page 2026-06-03 15:58:09 +02:00
Edouard Vanbelle c9bda7cbda chore(docker): add new /templates directory 2026-06-03 15:56:35 +02:00
Edouard Vanbelle 20e187f26d i18n: translate server.* keys 2026-06-03 15:11:57 +02:00
Edouard Vanbelle 7db27af7a6 feat(user.prefered_locale): save user's locale + invited have same locale as inviters
- OIDC JIT define the locale only at user creation, user can so change his preference later
    - invited users will inherit inviter's locale
    - email will use prefered_locale
    - login to a new browser will use prefered_locale
2026-06-03 14:37:22 +02:00
Edouard Vanbelle 854f1d3a07 feat(templating): add and use templates for /magic and emails 2026-06-03 14:10:43 +02:00
Edouard Vanbelle 044bd76738 feat(i18n): add i18n on server side
- remove the hardcoded list of locales in favor of a discovry on start time
    - server will stop on badly formatted locale .json
    - add server.* entries for serer side translation

    server side translation will be used for templating and email
    note: no json in some embded html (like in /magic), amount of work was similar
2026-06-03 13:27:12 +02:00