Skip to content

fix(remotefs): inspect the stat error in WinFS Remove and RemoveAll - #440

Open
kke wants to merge 4 commits into
mainfrom
fix/428-winfs-remove-stat-error
Open

fix(remotefs): inspect the stat error in WinFS Remove and RemoveAll#440
kke wants to merge 4 commits into
mainfrom
fix/428-winfs-remove-stat-error

Conversation

@kke

@kke kke commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Both decided whether a path is a directory with err == nil on a Stat call and never looked at the error, so any stat failure read as "not a directory". For RemoveAll on a populated directory that skipped the recursive delete, fell through to Remove, and ran the non-recursive rmdir instead: the tree survived and the caller was told "the directory is not empty" by the one function whose purpose is deleting non-empty directories, with nothing in the error mentioning the connection.

Both now branch on what the error actually says, following the os package:

  • path is a file -> del
  • path is a directory -> rmdir, recursive for RemoveAll
  • path does not exist -> error for Remove, nil for RemoveAll
  • stat failed at transport -> propagate, never fall through to del

The del is factored into removeFile so RemoveAll no longer reaches it via Remove, which re-stat'ed the same path.

Behaviour change: Remove on a missing path is now an error, matching os.Remove, where del previously exited zero and the call returned nil. Every in-tree caller either discards that error or removes a path it just created.

Prerequisite carried along: this also contains the one-line WinFS.Stat fix from #427, which stops an execution failure being reported as fs.ErrNotExist. It is required here rather than optional -- RemoveAll now treats ErrNotExist as "nothing to remove", so without it a dropped connection would return nil and turn today's loud failure into a silent one. Drop this hunk if #427 has already landed.

Fixes #428

@kke kke added the bug Something isn't working label Aug 20, 2026
@kke
kke requested a lite review from Copilot August 20, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes incorrect directory/file handling in the Windows remote filesystem implementation by ensuring WinFS.Remove and WinFS.RemoveAll properly inspect Stat errors (instead of treating any stat failure as “not a directory”), and by correcting WinFS.Stat so transport/exec failures are not misclassified as fs.ErrNotExist.

Changes:

  • Update WinFS.Stat to propagate command-execution/transport errors without wrapping them as fs.ErrNotExist.
  • Rework WinFS.Remove and WinFS.RemoveAll to branch on Stat outcomes (dir/file/missing/transport failure) and avoid incorrect fallthrough behavior.
  • Add focused unit tests covering missing-path semantics, directory recursion behavior, and transport failures for Remove/RemoveAll.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
remotefs/winfs.go Fixes Stat error classification and corrects Remove/RemoveAll branching to match os.Remove/os.RemoveAll semantics on Windows.
remotefs/winfs_test.go Adds regression tests for the corrected Stat/Remove/RemoveAll behavior, including transport-failure cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread remotefs/winfs.go Outdated
Comment thread remotefs/winfs.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

remotefs/writefileatomic_test.go:90

  • Test comment is now inaccurate: the cleanup defer calls RemoveAll (not Remove), so the explanatory comment should be updated to match the new behavior.
		// After a successful rename the temp path no longer exists; Remove is
		// still called by defer but the error is ignored.

@kke
kke force-pushed the fix/428-winfs-remove-stat-error branch from 0715ad5 to 59fda3d Compare August 20, 2026 13:36
@kke
kke requested a lite review from Copilot August 20, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

remotefs/winfs.go:120

  • WinFS.Remove currently wraps Stat errors with fmt.Errorf("remove %s: %w", ...). When the path is missing (or Stat fails), callers doing errors.As(err, *fs.PathError) will see Op="stat" (from Stat) rather than Op="remove", which diverges from os.Remove semantics and from PosixFS.Remove (which returns a PathError with Op="remove" for ErrNotExist). Consider returning a PathError from Remove so the top-level operation matches the API contract while still preserving the underlying cause in the error chain.
	if err != nil {
		// Covers both a path that is genuinely absent -- an error for os.Remove --
		// and a stat that could not be performed. Neither may fall through to del:
		// a transport failure is not evidence of anything about the path.
		return fmt.Errorf("remove %s: %w", name, err)

kke added 4 commits August 20, 2026 17:07
Both decided whether a path is a directory with `err == nil` on a Stat call
and never looked at the error, so any stat failure read as "not a
directory". For RemoveAll on a populated directory that skipped the
recursive delete, fell through to Remove, and ran the non-recursive rmdir
instead: the tree survived and the caller was told "the directory is not
empty" by the one function whose purpose is deleting non-empty directories,
with nothing in the error mentioning the connection.

Both now branch on what the error actually says, following the os package:

  - path is a file            -> del
  - path is a directory       -> rmdir, recursive for RemoveAll
  - path does not exist       -> error for Remove, nil for RemoveAll
  - stat failed at transport  -> propagate, never fall through to del

The del is factored into removeFile so RemoveAll no longer reaches it via
Remove, which re-stat'ed the same path.

Behaviour change: Remove on a missing path is now an error, matching
os.Remove, where del previously exited zero and the call returned nil. Every
in-tree caller either discards that error or removes a path it just created.

RemoveAll treating ErrNotExist as "nothing to remove" is only safe because
the WinFS.Stat fix from #427 has landed: while an execution failure was
still reported as fs.ErrNotExist, a dropped connection would have returned
nil here and turned a loud failure into a silent one.

Fixes #428

Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
…emove

WinFS.Remove now errors on a missing path while PosixFS.Remove used rm -f
and returned nil, so the same call meant different things per platform.
remotefs/types.go documents the OS interface as modeled after the os
package, which makes the POSIX side the one that diverged: os.Remove errors
and os.RemoveAll does not.

Dropping -f is what changes it. rm still does not prompt for a
write-protected file, because it only prompts when stdin is a terminal, and
a missing path is now classified through isNotExist so callers get
fs.ErrNotExist rather than an opaque command failure.

Not changed, and stated in the doc comment rather than fixed: PosixFS.Remove
still refuses an empty directory, where os.Remove and WinFS.Remove remove
one. rm needs -d for that, which POSIX does not require it to have.

Until #429 lands, the fs.ErrNotExist classification here shares the
truncation sensitivity of the other isNotExist call sites -- a long enough
path pushes the reason out of the message it matches on.

Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
With Remove no longer tolerating a missing path, the cleanup defers in
WriteFileAtomic and Upload became a command that always fails: on the
success path the rename has already moved the temp file away. The error is
discarded at both sites, so nothing failed -- but every successful atomic
write and upload issued a failing rm, and any Tracer saw it.

A cleanup defer means "remove if present", which is os.RemoveAll's contract,
and RemoveAll returns nil for a missing path on both implementations. The
POSIX side is also a command cheaper on Windows than the Remove it replaces,
which stats before deleting.

TestWriteFileAtomicPosix now pins the choice, so reintroducing Remove there
fails rather than going unnoticed behind a discarded error.

The third such site, initTouch in posixfs.go, is left on Remove: its temp
file is never renamed away and is still there when the defer runs.

Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
Remove wrapped the Stat error with fmt.Errorf, so a caller doing
errors.As(err, **fs.PathError) read Op == "stat" -- an operation it never
invoked. That is worse than carrying no PathError at all: os.Remove
guarantees the Op names the call that failed, and a caller has no way to
tell a genuine stat failure from a Remove implemented with one. RemoveAll
had the same problem, and PosixFS.Remove only produced Op == "remove" on its
ErrNotExist path.

All four now return a *fs.PathError naming the call the caller made, with
new OpRemove and OpRemoveAll constants alongside the existing ones.

Two details:

  - pathErrorCause unwraps the PathError that Stat returns before rewrapping,
    so the path appears once rather than as "remove C:\x: stat C:\x: ...".
  - the WinFS del/rmdir helpers no longer repeat the path either; they name
    the command that failed and let their callers supply the path. A cause
    with no command prefix therefore means the stat was what failed.

requirePathErrorOp in patherror_test.go pins the Op on every failure shape
of both implementations. All six assertions fail against the previous code.

Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
@kke
kke force-pushed the fix/428-winfs-remove-stat-error branch from 59fda3d to 7ede34e Compare August 20, 2026 14:12
@kke
kke requested a lite review from Copilot August 24, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remotefs: WinFS.Remove and WinFS.RemoveAll treat any stat failure as "not a directory"

2 participants