Commit Graph

1124 Commits

Author SHA1 Message Date
Claude 175a908e99 perf(search): clamp search page size to bound per-request memory
The main search endpoint took `limit: params.limit.unwrap_or(100)` with no
ceiling, and that value flows straight into the SQL LIMIT of
search_files_paginated / search_files_in_subtree. A client passing
?limit=<huge> would make Postgres return that many rows into memory and into
the result cache. The suggestions endpoint already clamps (.min(20)); search
did not.

Cap at MAX_SEARCH_LIMIT (500). total_count still reflects the full match set
(COUNT(*) OVER()), so deeper results stay reachable via offset.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 14:59:01 +00:00
Claude 8dc528f403 fix(dedup): count chunk ref_count per distinct chunk, not per occurrence
store_chunks bumped storage.blobs.ref_count once per chunk *occurrence* (it
looped over the full chunk list, duplicates included), but
remove_manifest_reference decrements once per *distinct* chunk
(WHERE hash = ANY(chunk_hashes) matches each row a single time). For any file
that repeats a chunk -- zero-filled regions in disk/VM images, repeated
document structures, concatenated archives -- storing added +N while deleting
removed -1, so the blob's ref_count never returned to 0 and the chunk was
never garbage-collected: a permanent storage leak.

Count per distinct chunk on the store side too, matching deletion. This also
makes it faster:
- existing chunks: one batched `UPDATE ... WHERE hash = ANY($1)` instead of
  one UPDATE per occurrence;
- a brand-new chunk repeated within a file is read, uploaded and INSERTed
  once instead of once per occurrence.

The manifest still stores the full per-occurrence chunk sequence (needed to
reassemble the file). Forward fix: blobs already over-counted by the old path
stay over-counted (a reconcile/verify pass could recompute them), but the
bias is upward (leak), so no data is ever deleted early.

CDC tests pass (12); fmt + clippy clean.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 14:19:14 +00:00
Claude 19b41af2e4 perf(http): single smart compression layer — stop compressing media on /api
The /api router added its own predicate-less CompressionLayer (routes.rs),
nested inside the global predicate-aware one in main.rs. As the inner layer it
compressed responses first, so the global predicate that skips already-
compressed media was bypassed for every /api response: video/audio/image/zip
downloads got Brotli-compressed (CPU + first-byte latency for ~0 bytes saved)
and lost their Content-Length (forced to chunked -> no client progress bar).

- Remove the redundant /api CompressionLayer; /api now flows through the
  single global layer in main.rs.
- Make that predicate smarter: compress by default so nothing shrinkable is
  missed, and skip ONLY already-compressed types. It no longer blanket-excludes
  image/*, so image/svg+xml (text, ~70% shrink) now compresses; raster formats
  are listed individually. Added the previously-missed already-compressed
  types: Office (docx/xlsx/pptx), ODF, epub, jar, apk, 7z/rar/bzip2/zstd/xz,
  woff/woff2 fonts, icons.

Net: media downloads keep Content-Length and skip pointless compression, while
text/JSON/JS/CSS/SVG/XML/ttf/otf/wasm still compress. fmt + clippy clean.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
2026-06-09 14:08:27 +00:00
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 f9c3adadc5 feat(chunked-upload): keep previous pending upload
a prefix path as been add to avoid collision if OXICLOUD_UPLOAD_TMPDIR=OXICLOUD_CHUNK_DIR
    upgrade to this version will convert previous upload chunked into the prefixed version at server start
2026-06-09 11:33:08 +02:00
Edouard Vanbelle c7b359eae3 chore(vitepress): link new /config/storage-fine-tuning doc 2026-06-09 11:12:18 +02:00
Edouard Vanbelle 50ea406719 feat(OXICLOUD_DIRECT_PUT_MAX_BYTES): add a security limit on direct PUT
ensure files does not exeed OXICLOUD_MAX_UPLOAD_SIZE, prefer to deny from header rather consuming bandwidth
    add OXICLOUD_DIRECT_PUT_MAX_BYTES for direct PUT (non chunked), admins can fine tune their prefered values
2026-06-09 11:06:44 +02:00
Edouard Vanbelle 4e36de49eb feat(uploads): add OXICLOUD_CHUNK_DIR + documentation for admins
explain OXICLOUD_CHUNK_DIR and OXICLOUD_UPLOAD_TMPDIR
    and also the OXICLOUD_CHUNK_MAX_BYTES & OXICLOUD_UPLOAD_TMPDIR
    to help administratorrs to defined correctly their storage architecture
2026-06-09 11:06:44 +02:00
Edouard Vanbelle 2843b3351b feat(blake digest): ensure digest is done on same passe as the write of chunks
purpose: avoid current scheme:
        1. write to disk
        2. reopen file to read data and digest it

    now: digest is done while writing data to disk

    all other implementation than Nextcloud are corrrect
2026-06-09 11:06:41 +02:00
Edouard Vanbelle 964d66e7f8 featchukn-upload): client can provide full file hash completion 2026-06-09 09:55:27 +02:00
Edouard Vanbelle abac074fe5 feat(fssync): ensure fs sync once upload is recomposed 2026-06-09 09:55:27 +02:00
Edouard Vanbelle 41aad26702 feat(upload): cover chunk upload + add support of different digest hash
Prefer stream storage rather using buffered (in memory)

  note: on many unix like tmpfs are in-memory, sungle PUT are sized limited

  Storage map (NC stands for Nextcloud gateway)

  ┌───────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────┐
  │                   Streaming surface                   │                            Destination                             │                Configurable via                 │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ REST chunked PUT /api/uploads/{id} chunk              │ {storage_path}/.uploads/{upload_id}/chunk_{NNNNNN}                 │ OXICLOUD_STORAGE_PATH (the .uploads subdir is   │
  │                                                       │                                                                    │ hard-wired)                                     │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ REST chunked assemble (during /complete)              │ {storage_path}/.uploads/{upload_id}/assembled                      │ same                                            │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ NC chunked PUT /dav/uploads/.../{chunk}               │ {storage_path}/.uploads/nextcloud/{user}/{upload_id}/{chunk_name}  │ same                                            │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ NC chunked assemble (during MOVE)                     │ {storage_path}/.uploads/nextcloud/{user}/{upload_id}/.assembled    │ same                                            │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ NC single-file PUT /dav/files/.../{path} (via         │ OXICLOUD_UPLOAD_TMPDIR if set, else OS default temp (/tmp on       │ OXICLOUD_UPLOAD_TMPDIR                          │
  │ spool_body_to_temp)                                   │ Linux)                                                             │                                                 │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ REST WebDAV PUT /webdav/{path} (via                   │ same as above                                                      │ OXICLOUD_UPLOAD_TMPDIR                          │
  │ spool_body_to_temp)                                   │                                                                    │                                                 │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ REST multipart upload /api/files/upload               │ {storage_path}/.dedup_temp/upload-{uuid}                           │ OXICLOUD_STORAGE_PATH (hard-wired subdir)       │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ WOPI PutFile                                          │ OS default temp via NamedTempFile::new() (no override)             │ (none — bug worth tracking)                     │
  ├───────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ Final blob storage (after fsync + rename)             │ {storage_path}/.blobs/{ab}/{abc…}.blob                             │ OXICLOUD_STORAGE_PATH                           │
  └───────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────┘

  one caveat: a malicious user can create many chunked upload and saturate local storage
2026-06-09 09:52:09 +02:00
Edouard Vanbelle 5e638691ad security(upload): cap upload size to prevent memody/disk consumption
add OXICLOUD_CHUNK_MAX_BYTES which correspond to the max upload chunk allowed
    (differs from OXICLOUD_MAX_UPLOAD_SIZE which is the max total size of a file)
    hurl test validate the change

    Streams the request body straight to the chunk file with peak heap of
    ~one HTTP frame, regardless of chunk size or the configured cap. The
    `storage.chunk_max_bytes` config (env `OXICLOUD_CHUNK_MAX_BYTES`,
    default 100 MB) bounds a single PUT — separate from `max_upload_size`
    which governs whole-file uploads. Without this separation, a client
    could submit a chunk up to the whole-file cap (10 GB default) and
    monopolise server memory.
2026-06-09 09:26:33 +02: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