Skip to content

fix(fs): enforce the no-overwrite contract on companion uploads - #74

Merged
qiffang merged 1 commit into
mainfrom
fix/fs-cp-overwrite-guard
Oct 7, 2026
Merged

qiffang merged 1 commit into
mainfrom
fix/fs-cp-overwrite-guard

Conversation

@mornyx

@mornyx mornyx commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #72

Problem

ti fs cp --from-local … --to-remote … (and stdin uploads) silently replaced an existing remote destination and exited 0 even though --overwrite defaults to false. Service.CopyFile delegates unconditionally to the companion, drive9CopyArgs has no overwrite flag to forward, and the delegated drive9 fs cp treats every drive9 destination as overwrite-enabled (objectCopyOverwrite returns true for anything but KindObject). The direct copy path already enforced the contract via ensureRemoteTargetCanWrite; the companion path bypassed it.

Change

  • drive9CopyFile now probes the destination with drive9 fs stat before local/stdin → remote uploads:
    • stat succeeds → fail with fs.target_exists ("remote target %q already exists; pass --overwrite to replace it"), the same error the direct path produces;
    • stat reports not found → proceed;
    • any other stat error → fail closed (never overwrite when existence is unknown).
  • --overwrite, --append, and --resume bypass the probe by design.
  • Remote → remote copies are unaffected: the server rejects an existing destination (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 returns fs.target_exists and never invokes fs cp.
  • TestDrive9CopyUploadGuardBypassedForOverwriteAppendAndResume — the three opt-ins still run the copy.
  • TestDrive9CopyUploadGuardFailsClosedOnStatError — an inconclusive probe propagates instead of allowing the overwrite.
  • Fake companion gains TI_FAKE_DRIVE9_STAT_NOT_FOUND; the non-replayable-stream test now declares its destination absent.

Full go test ./... passes.

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
@ti-chi-bot ti-chi-bot Bot added the size/L label Oct 6, 2026

@qiffang qiffang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_exists usage 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/cla checks green.

No remaining blocker. LGTM.

@qiffang
qiffang merged commit 4f01eba into main Oct 7, 2026
2 checks passed
@mornyx
mornyx deleted the fix/fs-cp-overwrite-guard branch October 7, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] fs cp overwrites an existing remote destination when --overwrite is false

2 participants