P2-3: GET, PUT, MKCOL, DELETE, MOVE — complete the Remote interface #23
Labels
No labels
data-integrity
engine
platform
procurement
remote
scaffold
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/cairn-desktop#23
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Depends on P2-2. Completes
remote.Remoteagainst a real Cairn for everything except resumable upload (P2-4).Files
internal/remote/webdav.go,internal/remote/webdav_test.goVerb mapping
Get(path)GETRangeheader for resumed downloadsPut(path, r, mod)PUTMkcol(path)MKCOL405 Method Not Allowedmeans it already exists — treat as successDelete(path)DELETE404means already gone — treat as successMove(from, to)MOVEDestination:header with a full absolute URL, andOverwrite: TThree things that will bite
MOVEneeds an absoluteDestinationURL, percent-encoded, not a relative path. Getting this wrong yields a confusing 400.Putmust return the newEntrywith its ETag. Some servers return the ETag in thePUTresponse header; some do not. If absent, follow with aPROPFINDon that path. Do not guess or leave it empty — the engine writes it into state, and an empty ETag means the next pass sees a phantom remote change.Deleteon a missing path andMkcolon an existing one both mean "the world is already how I want it". Returning an error would make the engine skip a path forever.Steps
httptestcovering: round-trip PUT→GET; ranged GET returns the right slice; MKCOL on an existing collection succeeds; DELETE of a missing path succeeds; MOVE sends an absolute encodedDestination; PUT with no ETag in the response falls back to PROPFIND.var _ Remote = (*Client)(nil)so the compiler proves the real client is substitutable forMemRemote.git commit -s -m "feat(remote): GET/PUT/MKCOL/DELETE/MOVE over WebDAV"Acceptance criteria
*Clientsatisfiesremote.Remote.Amendment — 2026-09-11: phase-1 hand-off (binding rulings Task 3 F5, Task 12 CV4; final review X2)
Task 3 F5 — address existing entries by their native name
GET,PUT,DELETEandMOVEon an existing entry use the native name #22 observed inthe listing, not a URL built from the NFC engine path. A byte-preserving server can store NFD or
other non-canonical bytes, and a rebuilt URL then returns 404. Under R2 that reads as "already
gone", which is one step from a deletion. Percent-encoding the engine path is only for names the
client itself creates. See the #22 amendment for the full ruling.
Task 12 CV4 — the MOVE semantics the engine relies on
(i) MOVE over an existing file. The engine needs this when a local rename lands on a name
that is already synced.
Decideemits it only when the destination is unchanged against itsstate row, so the overwrite replaces only bytes that are already synced. Keep
Overwrite: Tas specified above;
remote.godocuments file-onto-file overwrite. Satisfied, no change.(ii) ETag stability across MOVE. After the Task 12 F3 fix, the engine records the prior
row's metadata after a move, not a post-move Stat. Correctness therefore no longer depends on
whether the server keeps the ETag, and an unstable ETag costs one re-download per rename.
#29's integration run against
cairndrecords which behaviourcairndhas.(iii) Missing parent collections. Binding:
Client.Movecreates every missing ancestor collection oftobefore it sendsMOVE. Ituses
MKCOL, where405means the collection already exists, asPutdoes.Client.Movemaps a404source tofs.ErrNotExist(R2).Remote.Movedoc comment, so thatMemRemoteandClientshare one contract.RFC 4918 §9.9.4 has the server answer 409 for a missing intermediate collection. Without the
MKCOL chain, every rename into a new folder becomes a permanent pair of Skips.
Final review X2 — conditional requests close the race on the server
The engine now re-reads each copy just before it replaces or removes it. It compares a local
copy's size, mtime and FileID, and a server copy's presence and ETag, with the scan that decided
the operation. On a mismatch it skips that operation and changes nothing. This is commit
699ec0a(fix(sync): re-check a copy against the scan before replacing or removing it).That shrinks the window to the moment between the re-read and the request, but does not close
it. Close it on the server:
PUTover an existing object,DELETEandMOVE(source) sendIf-Match: "<the ETag the scan saw>".PUTorMOVEto a path the scan saw as absent sendsIf-None-Match: *(forPUT) orOverwrite: F(forMOVE).412 Precondition Failedmaps to a distinct error that the engine turns into the same"changed while syncing" Skip. It must not be a failure, and must not be retried as though
unconditional.
The engine has to pass the expected ETag in. Decide the shape here: conditional variants or an
options argument on
Remote. Implement the same semantics inMemRemoteso the phase-1 enginetests keep driving it. This is an engine change as well as a client change.
Done
What was built
Client.Get/GetFrom/Put/Mkcol/Delete/Moveover WebDAV, withvar _ Remote = (*Client)(nil).Precondition{ETag, Absent}on Put/Delete/Move (amendment X2), the same semantics in MemRemote,ErrPreconditionFailedon 412.%-name refusal, native-name addressing (F5/F7).Overwrite: T(self-move, subtree move, a collection on either side).Tests
TestEngineSyncsThroughClientinwebdav_test.go; engine conditional-write tests inrecheck_test.go; 3 MemRemote precondition tests; 12/12 mutations killed.cairnd-dev.sh(P2-R18): PUT/GET/MKCOL/DELETE/MOVE round trips,Overwrite: F→ 412, If-Match/If-None-Match confirmed ignored.go vetandgo test -count=1 ./...allok, coverage total 92.1%.Acceptance criteria
*Clientsatisfiesremote.Remote—var _ Remote = (*Client)(nil).TestEngineSyncsThroughClient, contract parity with MemRemote.Rulings
git commit -s, Co-Authored-By.Client.Get/Delete/Moveon a missing path give bothErrNotFoundandfs.ErrNotExist; MKCOL 405→nil.Remote.Put's ETag contract is one-way; "identical content ⇒ identical ETag" is a MemRemote-only property.<server>/dav/home/).%names refused as a per-file Skip.Overwrite: Fbut not If-Match — the engine's re-check is the real guard.cairnd-dev.shonly, never ~/Cairn, 192.168.10.249, or the owner's credentials.Overwrite: Twhenever!want.Absent" mutation survived every test with no dedicated row; fixed by adding "Move without Absent where nothing is at the destination" toTestClientSendsTheCallersPreconditions.Deferred
Implemented and reviewed by Claude (subagent-driven), landed on main after review and green CI.