Commit Graph

124 Commits

Author SHA1 Message Date
Edouard Vanbelle 21ac3a178f test(webdav): cover the dead properties 2026-06-30 20:32:16 +02:00
Edouard Vanbelle 85bc6c51de fix(test): fix quota test (race condition in test) 2026-06-30 20:27:15 +02:00
Edouard Vanbelle 1e2882973b fix(test): correct due to commit 43cf4a2bg
- MKCOL is now better protected
    - Webdav now handle 201 (created) 204 (overritten)
2026-06-30 20:18:19 +02:00
M.Schmidt f9999cdd0f fix(webdav): RFC 4918 litmus compliance 2026-06-22 21:37:14 +02:00
Edouard Vanbelle 934d6b4af0 feat(drive): NC chunked upload respect drive's quota
personal drive are based on user's quota
2026-06-29 23:21:17 +02:00
Edouard Vanbelle 583af517c2 fix(drive): fix drive_id on copy to other drive
- fix alsot drive_id exposition
2026-06-29 23:21:17 +02:00
Edouard Vanbelle ee92d365b9 feat(drive): ensure drive_id updated on file|folder moved to another drive 2026-06-29 21:11:40 +02:00
Edouard Vanbelle acf2311cea test(oidc): ensure that static-dist is built to validate tests 2026-06-29 21:10:26 +02:00
Edouard Vanbelle ada4b021bf test(oidc): test OIDC login and refresh flow
- ensure Oxicloud s correctly functional with OIDC login
    - refresh token works
    - givenname, familyname, picture are imported in users' informations
    - test relogin (server is not using same path)
    - test email verified
    - test admin group provided from IdP
2026-06-28 23:22:37 +02:00
Edouard Vanbelle 66f2aaa250 feat(drive): add drive policie
- add policy forbid_external_sharing
    - add policy forbid_sharing
    - add polocy forbid_cross_drive_move
    - add policy forbid_owner_role_change
2026-06-26 14:04:36 +02:00
Edouard Vanbelle ddb131da8b feat(drive): add policy forbid_public_links 2026-06-26 13:59:02 +02:00
Edouard Vanbelle cfd783cbd3 feat(drive): personal drives have the user's quota in commun 2026-06-26 13:59:02 +02:00
Edouard Vanbelle f4032e9e23 feat(recent): update recent list server side
initially the recent was done client side
    recent files are now directly updated on serverside when accessing a file

    note: nextcloud and webdav voluntary not included
2026-06-26 01:44:19 +02:00
Edouard Vanbelle 2643484860 test(storage_cleanup_check.sh): add support of drive cleanup 2026-06-25 00:05:36 +02:00
Edouard Vanbelle 90474aa885 chore(test): blob lifecyclc with thumbnail cleanup
renable thumbnail test, ensure that blob lifecycle correctly
    trigger thumbnail cleanup on blob deletion

    need to call `/api/admin/internal/trigger-gc?force=true`
2026-06-24 23:50:37 +02:00
Edouard Vanbelle 6b8e2ba49c test(drive): check quota calculus 2026-06-24 23:01:34 +02:00
Edouard Vanbelle b73f176024 feat(drive): permanent deletion per drive 2026-06-24 21:56:27 +02:00
Edouard Vanbelle 7d24015fc4 feat(drive): add drive deletion
- conditions: drive must be empty
    - deletion forbidden on main personal drive
2026-06-24 21:17:27 +02:00
Edouard Vanbelle 289cf19270 feat(drive): user can rename drive
- only owners can rename root folders name (aka the drive name)
    - add UI to rename drive's name
2026-06-24 02:03:21 +02:00
Edouard Vanbelle a5b24a7453 feat(drive): add /api/drive
- permit shared drive creation from oxicloud admin (for now)
    - prepare other personal drive creation (Not implemented), need to validate
    quota policies and strategy first
    - add hurl test to verify permissions
2026-06-23 23:50:57 +02:00
Edouard Vanbelle 184520c17a feat(drive): plug trash to drives 2026-06-23 22:04:20 +02:00
Edouard Vanbelle 6f1f44f962 feat(drive): prepare E2E tests on drive membership 2026-06-23 20:26:34 +02:00
Bradley Nelson 02335c0680 test(e2e): run the SvelteKit SPA Playwright suite in CI
The e2e CI job ran the legacy `scenarios/*` specs against the vanilla `static/`
frontend that upstream has since removed, so it could never pass. Point CI at
this repo's SvelteKit SPA suite (tests/e2e/spa) and wire up what it needs:

- CI: build the release binary with `--features plugins` (the admin Plugins-tab
  specs exercise the WASM runtime) and run `npm run test:coverage`, building the
  instrumented SPA with COVERAGE=1 VITE_E2E=1 so the server serves the
  data-testid-instrumented build the specs drive.
- Coverage harness: target 127.0.0.1 instead of `localhost` (which resolves to
  ::1 first on CI runners while the server binds IPv4, so readiness never
  connected) and poll `/ready` for webServer readiness; tee start-server-spa.sh
  output to a log surfaced by an always-run CI step for diagnostics.
- Files page: restore a persistent breadcrumb home link (buildCrumbs returns
  only the path folders, so there was no "go home" affordance), and fix the
  `?file=` deep-link race where the viewer→URL effect stripped the param before
  the listing loaded — a bookmarked preview link now opens the viewer.

All 101 spa specs pass locally.
2026-06-22 10:34:47 -06:00
Bradley Nelson e3823ce470 test(e2e): Playwright + Vitest coverage harness and test instrumentation
Add an end-to-end and unit test suite for the SvelteKit frontend:

- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
  codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
  ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
  public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
  middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
  v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.

Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
  setup so storage behaves identically across Node versions (Node 26 ships a
  native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
  so the dev shell and CI run the same toolchain versions.

Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
  (cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
  batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
  wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
  blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
  existing file id) instead of the stale 409 expectation.

Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
2026-06-22 00:05:06 -06:00
DioCrafts 0c40c69f9b perf(photos): ETag/304 conditional revalidation on the timeline
GET /api/photos sent only X-Next-Cursor — no ETag — so every gallery
re-mount rebuilt up to 500 PhotoDtos, serde-serialized the whole vector,
and shipped the full body even when nothing changed.

The handler now emits a lightweight content-derived ETag
(hash of before + limit + max(modified_at) + row count) and honours
If-None-Match, with Cache-Control: private, no-cache so the SPA's default
fetch cache mode always revalidates. An unchanged "navigate away and back"
becomes an empty 304 instead of a full rebuild + reserialize + transfer.
The DB query still runs (the cheap part); the win is skipping the DTO
build, serialization, and body bytes.

Proven end-to-end (throwaway Postgres + server, 7 images):
  1st GET (no If-None-Match)      -> 200  4586 bytes + ETag
  2nd GET (If-None-Match matches) -> 304     0 bytes
  3rd GET (If-None-Match stale)   -> 200  4586 bytes (correctly invalidated)
~655 B/photo, so a full 500-row first page saves ~320 KB + a 500-DTO
build/serialize per unchanged revalidation. Unlike a cold load this is the
common gallery-navigation path, so it hits real user-facing latency.

Regression test: tests/api/photos_etag.hurl (added to the api-test suite).
Methodology in benches/PHOTOS-ETAG.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 00:22:49 +02:00
Edouard Vanbelle 74b33744ed chore(test): disable storage_cleanup_check.sh until GC trigger lands 2026-06-19 16:06:38 +02:00
Edouard Vanbelle 55487187eb chore(ci): improve db migratiion to unserstand clash in CI 2026-06-19 16:06:38 +02:00
Edouard Vanbelle dd9e3b8868 feat(drive): test Tantivy lookup with drive
ensure that a user that don't havee permission to a drive cannot search elements in this drive
    note: current design: Index are associated to drive, so if a document / directory is shared,
    the shared resource will not be in index for targetted user
    design is explicitely as is to reduce complexity
2026-06-19 16:06:38 +02:00
Edouard Vanbelle e7f4826778 feat(drive): complete updated_by created_by 2026-06-19 16:06:38 +02:00
Edouard Vanbelle 16ea08b093 feat(drive): improve Drive model
now Drive is purely a metadata
    each drive has always a root folder
    this model minimize Oxicloud changes, and simplify
    the Drive name is simply the folder's root's name
    note: owner of Drive has more permission that an owner of the root folder
2026-06-19 16:06:37 +02:00
Edouard Vanbelle eab7a609b9 feat(drive): start implementation of drive
- add storage.drives
    - prepare migration phase
    - add created_by and updated_by on storage.folders
2026-06-19 16:06:37 +02:00
Edouard Vanbelle 0ef4c624c5 chore(load): start implementation of load tests
initial test from Ed's nuc:

    metric                                          pctl  baseline    current     delta    status
    -----------------------------------------------------------------------------------------------
    folder_cascade.list_depth1                      p50   0.3ms       0.3ms       -6.3%    ok
    folder_cascade.list_depth1                      p95   2.3ms       0.5ms       -75.7%   ok
    folder_cascade.list_depth1                      p99   4.7ms       2.5ms       -48.1%   ok
    folder_cascade.list_depth4                      p50   0.4ms       0.3ms       -10.0%   ok
    folder_cascade.list_depth4                      p95   0.9ms       0.6ms       -31.2%   ok
    folder_cascade.list_depth4                      p99   2.4ms       1.0ms       -56.7%   ok
    folder_cascade.list_depth8                      p50   0.3ms       0.3ms       -8.8%    ok
    folder_cascade.list_depth8                      p95   0.6ms       0.5ms       -22.2%   ok
    folder_cascade.list_depth8                      p99   1.9ms       0.5ms       -71.5%   ok
    folder_cascade.list_depth_deep                  p50   0.3ms       0.3ms       -5.0%    ok
    folder_cascade.list_depth_deep                  p95   0.6ms       0.5ms       -18.6%   ok
    folder_cascade.list_depth_deep                  p99   2.0ms       0.7ms       -67.2%   ok
    share_cascade_rebac.list_grants                 p50   0.4ms       0.3ms       -27.6%   ok
    share_cascade_rebac.list_grants                 p95   1.2ms       0.5ms       -57.0%   ok
    share_cascade_rebac.list_grants                 p99   1.7ms       1.1ms       -36.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p50   0.5ms       0.5ms       -11.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p95   1.1ms       0.7ms       -41.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p99   3.0ms       1.3ms       -58.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p50   0.5ms       0.5ms       -13.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p95   1.4ms       0.7ms       -50.5%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p99   2.2ms       1.1ms       -49.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p50   0.5ms       0.4ms       -16.0%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p95   0.9ms       0.7ms       -26.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p99   1.5ms       0.9ms       -40.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p50   0.5ms       0.4ms       -17.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p95   1.0ms       0.7ms       -31.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p99   1.6ms       0.8ms       -49.4%   ok
    subject_group_nested.fetch_as_member_depth1     p50   0.5ms       0.4ms       -8.4%    ok
    subject_group_nested.fetch_as_member_depth1     p95   0.6ms       0.6ms       -10.2%   ok
    subject_group_nested.fetch_as_member_depth1     p99   1.4ms       0.6ms       -55.2%   ok
    subject_group_nested.fetch_as_member_depth4     p50   0.5ms       0.5ms       -7.9%    ok
    subject_group_nested.fetch_as_member_depth4     p95   0.6ms       0.6ms       -4.0%    ok
    subject_group_nested.fetch_as_member_depth4     p99   0.7ms       0.6ms       -2.2%    ok
    subject_group_nested.fetch_as_member_depth8     p50   0.5ms       0.4ms       -8.9%    ok
    subject_group_nested.fetch_as_member_depth8     p95   0.5ms       0.6ms       +7.1%    ok
    subject_group_nested.fetch_as_member_depth8     p99   0.6ms       0.7ms       +10.3%   ok
    subject_group_nested.fetch_as_member_depth_deep p50   0.5ms       0.4ms       -7.6%    ok
    subject_group_nested.fetch_as_member_depth_deep p95   0.6ms       0.5ms       -11.9%   ok
    subject_group_nested.fetch_as_member_depth_deep p99   0.6ms       0.7ms       +8.9%    ok
2026-06-18 09:38:25 +02:00
Edouard Vanbelle 72129af0bd refactor(role): use grant only
- remove permission centric mode
    - finalize migration drop all tables with permissions
    - ensure roles are ENUM (owner is always displayed first)
2026-06-18 02:47:06 +02:00
Edouard Vanbelle f168c4578f feat(roles): prepare migration ReBAC to roles
prepare migration of permission to roles
    this simplify drastically database (permission are now simply defined in code)
    and will permit reuse of the same ReBAC engine to define owners of drives

    mapping:

    ```
        Role::Viewer => &[Permission::Read],
        Role::Commenter => &[Permission::Read, Permission::Comment],
        Role::Contributor => &[Permission::Read, Permission::Create],
        Role::Editor => &[
            Permission::Read,
            Permission::Comment,
            Permission::Create,
            Permission::Update,
        ],
        Role::Owner => &[
            Permission::Read,
            Permission::Comment,
            Permission::Create,
            Permission::Update,
            Permission::Share,
            Permission::Delete,
            Permission::Manage,
        ],
    ```
2026-06-18 02:47:06 +02:00
Dionisio Pozo b8a0018785 Merge pull request #473 from EdouardVanbelle/fix/nextcloud+webdav
fix(nextcloud+webdav) fix bugs found via end to end tests
2026-06-17 12:42:31 +02:00
Dionisio Pozo e5059eee51 Merge pull request #475 from BCNelson/bcn/plugins
Add M0 WASM plugin system (sandboxed, observe-only)
2026-06-17 12:41:49 +02:00
Dionisio Pozo 992d965545 Merge pull request #472 from EdouardVanbelle/feat/by-hash
feat(by-hash): allow /by-hash even if blob is trashed
2026-06-17 12:38:31 +02:00
Edouard Vanbelle 619c24e1e9 test(api): unpin folder-share file fetch; assert in/out-of-scope
The KNOWN BUG pin for `GET /api/s/{folder-token}/file/{file_id}` was
stale — the route now returns 200 + body for files inside the share's
subtree and 404 for anything outside it. Replaces the sidestep
comment with two positive assertions:

  - in-share: 200 + Content-Disposition references the file name
  - out-of-share (caller-owned file in a different folder): 404,
    matching "no such file" so the response can't be used to
    enumerate foreign file ids

Coverage now exercises the actual recipient-side download path that
NC desktop and web clients use; the file-share variant (item_type=file)
moves down to test 8b.

Adds a teardown DELETE for the outsider hello.txt so the next test in
the runner (permissions.hurl) can re-upload its own hello.txt into
admin's home folder without hitting the live-name unique index.
2026-06-17 09:32:37 +02:00
Bradley Nelson 803150635c add frontend 2026-06-16 21:26:36 -06:00
Bradley Nelson 87d68c5b6f init plugins 2026-06-16 17:57:57 -06:00
Edouard Vanbelle 8a53078ba7 fix(nc/webdav): drop Content-Length on HEAD when body is empty
handle_head was declaring `Content-Length: file.size` while writing
`Body::empty()` — on a keep-alive connection the client waits forever
for N bytes that never come. Hyper now derives Content-Length: 0 from
the actual body, which is honest about what's on the wire.

RFC 7231 §4.3.2 suggests HEAD return the same headers as GET, but
lying about Content-Length is worse than omitting it: NC and Sabre
clients use PROPFIND for size anyway, and curl -I (and any client
applying HEAD semantics) gets the same ETag/MIME/Last-Modified it
needs. Caught by the F6b test which uses `curl -X HEAD` to read the
current ETag before a conditional PUT.

Also adds `nc_status_propfind_depth0` to lib/dav_helpers.sh so the
F11/F11b assertions ("did the intermediate parent get auto-created?")
can compile.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle bacd5806d3 fix(webdav): enforce LOCK on every native mutator (RFC 4918 §9.10.4)
Extends the N2/PUT lock guard introduced earlier to the rest of the
native mutator surface. Same helper, same If: capture before body
consumption, same 423-on-reject shape:

  - handle_delete    : check source path
  - handle_proppatch : check source path
  - handle_move      : check source AND destination paths
  - handle_copy      : check destination path only (source isn't
                       modified by a copy)

The class-2 DAV advertisement in OPTIONS is now honest across the
full surface, not just PUT.

New tests N2c-N2f run while n-locked.txt is still LOCKed (before the
existing N3 UNLOCK). Each asserts 423 without the token and verifies
the operation didn't half-apply: file present after DELETE-423,
source untouched + no destination after MOVE-423, locked destination's
content unchanged after COPY-423.

Positive (with-token) coverage is implicit via the M-series happy-
path tests that exercise each method on unlocked resources — a
regression that hard-rejected every call would fail there too.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle af3ce24062 fix(nc/webdav): MKCOL on a missing parent → 409 (RFC 4918 §9.3.1)
Closes F11.

The NC MKCOL handler previously had `mkdir -p` semantics:
sending MKCOL on /a/b/c/ where neither a nor b exists silently
created both intermediates and returned 201. Sabre/DAV and the
actual NC server both 409 on that — our auto-create deviated.
NC desktop walks ancestors one MKCOL at a time during sync so
nothing real depended on the old behaviour.

Drop the segment-walking creation loop. New flow:
  target exists           → 405
  parent path missing     → 409
  parent ok, target new   → 201

The race-recovery branch for the loop's per-segment create is
also gone — single parent lookup, single create, no window.

Test F11 flipped from pinned-201 to strict 409 and asserts the
intermediate parent was not silently created. F11b and F11c added
as regression guards for the success path and the 'target already
exists' case.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle e2702ac673 fix(trash): cascade soft-delete and restore across folder subtree
Closes G9.

DELETE on a folder now flips is_trashed on every descendant folder
and file under it in a single CTE pipeline (lpath <@ root.lpath
covers the whole subtree via the GiST index). Previously only the
root row was flipped — descendants stayed live, directly addressable
via their full path, and confused desktop-sync tree walks that
expected the parent-collection 404 to imply the children were gone
too.

restore_from_trash mirrors the cascade: descendants where the
original_*_parent_id column is NULL are the ones we cascade-trashed,
so they get cascade-restored too. Descendants that were independently
trashed before the parent went to trash have original_*_parent_id
set, so they stay in trash and remain visible as top-level entries
in storage.trash_items.

No schema migration needed — both original_parent_id (folders) and
original_folder_id (files) were already nullable and already encoded
'where this came from when it was independently trashed'; using NULL
as the cascade-marker reuses that existing distinction cleanly.

Test G9 flipped from KNOWN BUG to assert every descendant 404s after
the parent DELETE; new G9b proves the inverse cascade by restoring
the trashed root and verifying every descendant comes back at its
original path.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle 5e1c99e227 fix(webdav): native MOVE/DELETE/COPY now resolve root-level paths
Closes M5 / M7 / M8a / M8b.

The optimized PathResolver and the read-side find_*_by_path queries
disagree on what counts as 'a path that hits a row'. After the drive-
refactor migration rewrote the path column to drop the
"My Folder - <user>/" prefix, files PUT through the WebDAV surface
stayed reachable by GET (legacy lookup) but vanished from the
optimized resolver (strict path-match). MOVE/DELETE/COPY 404'd on
every root-level file as a result.

Introduces resolve_or_legacy: optimized resolver first, then the
GET-style legacy lookups as a strict superset. Ownership is enforced
in both branches. handle_delete / handle_move / handle_copy each
collapsed from two near-identical resolver-only + legacy-only branches
into a single match using the helper — fewer lines, identical
semantics, root-level paths now resolve.

handle_copy also fixes M8b: copy_file_with_perms takes no destination
name, so a copy to a different filename in the same folder collided
with the source. After the copy, rename the new file when dest_name
differs from source name. Mirrors what handle_move already does.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle 5cb01b201d fix(nc/webdav): honour Overwrite on MOVE; restore-onto-existing → 412
Closes G4 / G5 / K5.

handle_move now resolves the destination once before the file/folder
dispatch and applies RFC 4918 §9.9.4:

  - Overwrite: F on a collision → 412 Precondition Failed, source
    untouched, destination untouched.
  - Overwrite: T (or absent) on a collision → delete the existing
    destination, then proceed → 204 No Content.
  - No collision → 201 Created (unchanged).

The same destination lookup powers the 201-vs-204 status decision, so
adding the precondition guard adds zero extra DB hits on the happy
path.

handle_restore now catches the unique-index collision out of
restore_item and returns 412 instead of letting it bubble as 500.
Mirrors the G4 semantics for the trashbin surface (restore has no
Overwrite header so the refusal is unconditional; client resolves by
renaming the live file first).

Sabre/DAV's CorePlugin and our test pins agreed independently — NC
clients expect this exact behavior, and the new G5b/G5c positive-case
tests guard against a regression that hard-rejected every MOVE.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle 9f2ebd0758 fix(webdav): enforce LOCK on native PUT (RFC 4918 §9.10.4)
Closes N2. The native WebDAV PUT handler now consults the lock
store before accepting a write: if the target path is exclusively
locked, the request must carry the lock token in its If: header
or the server returns 423 Locked. Without a matching token, the
body is never consumed — a rejected PUT no longer wastes the
upload bandwidth or hits the CDC ingester.

Two helpers are introduced so the same enforcement plugs into
the other mutator methods (delete/move/copy/proppatch) when their
fixes land:

  extract_if_header_tokens — angle-bracket-scoop view of If:
                             (sufficient for one-target writes;
                             full §10.4 tagged-list grammar would
                             only matter for multi-resource Ifs)
  enforce_native_lock      — Some(423) when locked + no/wrong
                             token, None otherwise

Test N2 flipped from pinned 204 to assert 423. Added N2b: same
PUT with the captured Lock-Token in If:(<...>) returns 204, so a
regression that hard-rejected every PUT would still fail loudly.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle 3fba933741 fix(nc/webdav): honour If-Match / If-None-Match on PUT (RFC 7232)
Closes F5/F6. The NC PUT handler now evaluates conditional
preconditions before body ingestion and returns 412 Precondition
Failed when they fail:

- If-None-Match: * on an existing target → 412 (create-if-absent)
- If-None-Match with matching ETag → 412 (weak compare)
- If-Match: * with no current representation → 412
- If-Match with no listed ETag strong-matching the current → 412

The lookup that drives the precondition reuses the same query the
handler already needed for the 201-vs-204 distinction, so this adds
no extra DB round-trip. Rejected requests skip body ingestion
entirely so a 412 doesn't waste megabytes of bandwidth + disk I/O.

Test F5/F6 flipped from 'pinned current 204' to assert 412, plus
mirror cases F5b/F6b/F6c/F6d covering the legitimate-success paths
so logical-operator regressions can't slip past silently.
2026-06-17 01:49:13 +02:00
Edouard Vanbelle 68abb891d7 feat(by-hash): allow /by-hash even if blob is trashed
- permit reuse of blob where file is trashed (by-hash and chunked)
    - add hurl test on /api/files/by-hash
    - add anti enumeration of blob (404 is always blob_not_owned_by_caller)
2026-06-16 22:41:55 +02:00
Edouard Vanbelle b5b252ba19 test(e2e): update playwright chrome snapshots 2026-06-16 15:36:43 +02:00