fix(remotefs): inspect the stat error in WinFS Remove and RemoveAll - #440
fix(remotefs): inspect the stat error in WinFS Remove and RemoveAll#440kke wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
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.Statto propagate command-execution/transport errors without wrapping them asfs.ErrNotExist. - Rework
WinFS.RemoveandWinFS.RemoveAllto branch onStatoutcomes (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.
There was a problem hiding this comment.
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.
0715ad5 to
59fda3d
Compare
There was a problem hiding this comment.
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)
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>
59fda3d to
7ede34e
Compare
Both decided whether a path is a directory with
err == nilon 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:
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