Commit Graph
6 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5 b4c1645c7a Address trash moves and deletes by path, not fileid
Trashing or emptying the trash failed on every scanned image with
"operation unsupported by this backend: fetch by fileid requires a
path".

RemoteId::Stable(oc:fileid) is an identity — it answers whether a file
is the same one after a move, and keys the thumbnail shards. It is not
an address: WebDAV exposes no fileid-addressable endpoint, so the
Nextcloud backend serves get/delete/move_to by path and rejects a bare
Stable. Both trash workers preferred the fileid whenever the catalog
knew one, so the unreachable Path fallback was the only arm that would
have worked, and the failure hit every properly-scanned image rather
than some edge case.

Address by path at both sites, and keep the fileid for what it is for:
the identity MOVE preserves, and the key the thumbnail cleanup uses.

Move::file_id was documented as "the id the MOVE addresses", which is
the wrong claim that seeded this; corrected, along with a note on
RemoteId itself so the distinction is stated where the type is defined.

Only the backend rejection was covered by a test. Added the positive
case, since that contract is what the call sites now depend on. The
workers build their own NextcloudBackend, so no test can reach the
call sites directly — closing that would mean injecting the backend,
which is left alone here.

Verified by build and test; not exercised against a live server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:39:35 +02:00
dtourolle 81e89de7ea Add remote move and mkdir; distinguish 403 from 401
Soft delete needs to move a photograph into the trash folder and back, and
the stable id must survive the trip. WebDAV MOVE is one request and preserves
oc:fileid; a copy-then-delete would allocate a new one, orphaning the
thumbnail shard entry and the sidecar mapping and turning a restore into a
full re-download. Overwrite: F, because a header that permits overwriting is
one that eventually does.

create_dir does MKCOL outermost-first and treats 405 — Nextcloud's answer for
an existing collection — as the goal state rather than an error. Nothing else
creates the trash folder, so without it the first trashed image of every
library fails with a 409 that reads like a permission problem.

PermissionDenied is now separate from AuthFailed. Folding 403 into 401 sent a
user to re-check a credential that was working perfectly, with reads
succeeding and only the write refused (observed against a real server). The
usual cause is an app password created without "Allow filesystem access" —
which signing in again will not fix.

Assisted-by: LLM
2026-08-09 20:55:04 +02:00
dtourolle 57f8d42a5c Record directory ETags only after actually listing them
The scan recorded a directory's ETag when it was *discovered* as a child of
another, not when its own contents were read. A scan that stopped early
therefore stored ETags for subtrees it had never listed; the next scan probed
those ETags, found them unchanged, and pruned folders whose contents had
never been seen. Their images never entered the catalog at all, and no later
scan would ever look again.

Each stack entry now carries the validator its parent reported, recorded only
once the directory has been listed. An interrupted scan re-reads that folder
next time — slower, and correct.

Two related fixes fall out. The scan root is now probed and recorded even
though no parent described it, without which the one-request no-op sync that
ETag pruning exists for could never fire at the root. And a pruned directory
keeps its recorded entry, rather than dropping out and forcing a full walk of
that subtree on the following scan.

Assisted-by: LLM
2026-08-09 20:42:39 +02:00
dtourolle c8bb08e661 Add folder scan with format selection; validate A3 on a real library
Library setup as the user described it: pick a folder, choose which RAW
types to look for, scan recursively.

  dr-types::FormatFilter  the tick-box selection, seeing through VFS
                          placeholder suffixes so a dehydrated CR2 still
                          matches as a CR2
  dr-sync::scan           recursive walk, Depth:1 per directory, pruning
                          unchanged subtrees where the backend propagates
                          directory ETags

Verified against nextcloud.tourolle.paris (34.0.2) on a real library:

  browse root      32 entries, 98ms
  scan PhotosRaw   17,185 RAW files in 334 directories, 34.1s
                   (7,836 CR2 + 9,349 DNG)
  range read       262KB of a 21.5MB DNG in 119ms — 1.22% of the file,
                   and enough to read "Canon EOS 6D | ISO 100"

That last line is assumption A3 validated on real data. Cataloguing this
library by whole-file fetch would move roughly 370GB; the range path
moves a few MB.

Pruning is capability-gated rather than assumed: with per-entry ETags a
probe costs a request and proves nothing about children, so it is skipped
entirely. A test asserts zero probes in that case.

Still unresolved: /core/preview returns 400 for every parameter
combination tried, including on a JPEG the server reports as having a
preview. Not a request-shape bug — it fails identically bare. Recorded
rather than worked around; ARCH §6.7 already treats server previews as
opportunistic, so nothing depends on it.
2026-08-09 12:22:31 +02:00
dtourolle f8a718f42e Add Nextcloud connector; reject VFS as a transfer mechanism
Investigated using the Nextcloud desktop client's Virtual Files as a
cache instead of talking to the server directly. Measured on this
machine (client 4.0.7): the configured folder holds 121,785 placeholders
against 10,267 materialised files, including 7,037 CR2 and 9,411 DNG.

Three findings, each independently disqualifying:

  - Linux VFS is *suffix* mode. A dehydrated IMG.CR2 exists only as
    IMG.CR2.nextcloud holding one byte; the real name is absent.
  - Reading a placeholder does not hydrate it. dd of the first 256KB
    returned 1 byte, the stub was unchanged, and the real name never
    appeared. There is no FUSE layer — the stub is an inert marker.
  - Even with hydration the granularity is wrong: VFS has two states,
    1 byte or all bytes, and the preview tier needs a ~256KB prefix of
    a 27MB file. That is ~100x what FR-NC-3 requires.

Recorded as ARCH §9.0. Coexistence is still supported: dr-types now
recognises *.nextcloud stubs, and the viewer lists them as "not
downloaded" rather than as corrupt files or not at all.

So the connector talks to the server directly, as D7 specified.
Implemented: Login Flow v2, PROPFIND with oc:fileid and nc:has-preview,
ETag pruning via a Depth:0 probe, range GET with local slicing when the
server ignores the header, conditional PUT, and /core/preview with
forceIcon=false. delta() returns Unsupported and says why.

Chunked upload v2 is not implemented yet — put() rejects bodies over
5MB explicitly rather than silently truncating.

Two bugs found by testing: my hand-computed epoch in a date test was a
day out (the parser was right), and quick-xml reaches EOF on truncated
input without erroring, so unbalanced elements needed an explicit check
— a half-parsed multistatus must not look like an empty directory.

83 tests passing.
2026-08-09 09:09:27 +02:00
dtourolle 0f202fd3f9 Add requirements traceability gate and Gitea pipelines
Ports JellyTau's traceability tooling to Rust, carrying across the bug it
was repaired for. That gate divided a traced count by frozen literal
denominators; the requirements file outgrew them and it reported 158%
coverage, so it could never fail its own threshold.

Two rules, both enforced by the extractor's own tests:

  - denominators parsed from docs/requirements.md at run time
  - coverage is |traced ∩ defined| / |defined|, never a raw traced count

The gate additionally fails hard on a misconfigured run — zero
requirements parsed or zero files scanned — rather than reporting a
plausible 0%, and on any orphan tag naming a requirement that does not
exist.

Adapted for DarkRoom: IDs are FR-CAT-1 / NFR-P13 / FR-DEV-3a shapes
rather than JellyTau's fixed three digits, and decisions (D), spikes (S),
milestone items (M) and test ids remain taggable while being excluded
from the denominator — counting them inflated it by 25.

Also adds dr-sync: the RemoteBackend trait and capability model, so the
Nextcloud connector is one implementation rather than the only shape the
engine understands. No mature Nextcloud crate exists (reqwest_dav is too
thin), so the connector will be hand-rolled over reqwest per D7.

Gitea workflows follow the same style: containerised, commented with the
reasoning, desktop and Android on every push, plus a CI check that no
core/ crate depends on the UI toolkit (ARCH §6.5a).

Coverage today: 13.3% (19/143). 50 tests passing.
2026-08-09 08:01:32 +02:00