Repository navigation
fix(fs): enforce the no-overwrite contract on companion uploads - #74
Conversation
CopyFile delegates unconditionally to `drive9 fs cp`, which treats every drive9 destination as overwrite-enabled, so `ti fs cp --from-local` (and stdin uploads) silently replaced existing remote targets even with --overwrite at its documented default of false. Probe the destination with `drive9 fs stat` before uploading and fail with the same fs.target_exists error the direct copy path uses; --overwrite, --append, and --resume bypass the probe, and non-not-found probe errors fail closed. Fixes #72
qiffang
left a comment
There was a problem hiding this comment.
Review round 1 — exact head 97e7f0f4. Verdict: APPROVED.
Small, correct fix enforcing the documented --overwrite=false contract on companion uploads. (The PR is stacked on #69's mount-ready work; this review covers only #74's own commit 97e7f0f — the mount-ready lines are #69's, not part of this change.)
The fix is correct
drive9CopyFile delegates to drive9 fs cp, which always overwrites drive9 destinations, so ti's --overwrite=false was unenforced. ensureDrive9UploadTargetAbsent (called at the top of drive9CopyFile) closes that:
- Bypasses when
Overwrite || Append || Resume(those intentionally touch existing targets). - Only probes the two real upload shapes (stdin→remote, local→remote); download shapes have
target==""→ no probe. - Probes via
fs stat: target exists →fs.target_existsusage error (code 2) telling the user to pass--overwrite; not-found (isDrive9NotFound) → proceed; any other stat error → propagated (fails closed) — the correct safety direction, no silent overwrite on an ambiguous stat.
Tests
3 new tests, all pass locally and are pure-logic (no build tag / no mount/exec/GOOS gating, so Ubuntu CI runs them too): …RejectsExistingRemoteTarget (core), …GuardBypassedForOverwriteAppendAndResume (bypass guard), …GuardFailsClosedOnStatError (fail-closed on non-not-found). Good coverage of the accept/reject/bypass/fail-closed matrix.
Non-blocking note
There's an inherent stat-then-upload TOCTOU (a target created between the probe and the upload would still be overwritten). This is the standard best-effort semantics for a CLI no-overwrite check and can't be made atomic without a server-side conditional-create the delegated command doesn't expose — acceptable, just flagging.
Verification
go build ./...clean; 3 new tests pass locally.- MERGEABLE;
test+license/clachecks green.
No remaining blocker. LGTM.
Fixes #72
Problem
ti fs cp --from-local … --to-remote …(and stdin uploads) silently replaced an existing remote destination and exited 0 even though--overwritedefaults to false.Service.CopyFiledelegates unconditionally to the companion,drive9CopyArgshas no overwrite flag to forward, and the delegateddrive9 fs cptreats every drive9 destination as overwrite-enabled (objectCopyOverwritereturnstruefor anything butKindObject). The direct copy path already enforced the contract viaensureRemoteTargetCanWrite; the companion path bypassed it.Change
drive9CopyFilenow probes the destination withdrive9 fs statbefore local/stdin → remote uploads:fs.target_exists("remote target %q already exists; pass --overwrite to replace it"), the same error the direct path produces;--overwrite,--append, and--resumebypass the probe by design.ErrPathConflict), which surfaces as an error rather than a silent overwrite.Known limitation (shared with the direct-path guard): the probe and the copy are separate round trips, so two racing copies can still interleave.
Test
TestDrive9CopyUploadWithoutOverwriteRejectsExistingRemoteTarget— blocked copy returnsfs.target_existsand never invokesfs cp.TestDrive9CopyUploadGuardBypassedForOverwriteAppendAndResume— the three opt-ins still run the copy.TestDrive9CopyUploadGuardFailsClosedOnStatError— an inconclusive probe propagates instead of allowing the overwrite.TI_FAKE_DRIVE9_STAT_NOT_FOUND; the non-replayable-stream test now declares its destination absent.Full
go test ./...passes.