fix(sftp): preserve concurrent files during local publication - #3286
Conversation
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de4412c9d6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb9e4ee4be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b32ec68fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (transfer.publishedLocalIdentity) { | ||
| try { | ||
| const currentStat = await fs.promises.lstat(transfer.targetPath); | ||
| if (!currentStat.isFile() || stableLocalFileIdentity(currentStat) !== transfer.publishedLocalIdentity) { |
There was a problem hiding this comment.
Compare published identities in the same representation
publishLocalFileExclusive() returns { dev, ino, size }, and both staged-transfer callbacks store that object in transfer.publishedLocalIdentity, while stableLocalFileIdentity(currentStat) returns a string. This strict comparison therefore always fails for real promoted downloads, so resumable and SCP downloads never receive the source mtime and subsequent size+mtime skip-unchanged checks repeatedly transfer them; normalize the returned or stored identity before comparing.
Useful? React with 👍 / 👎.
| const currentStat = await fs.promises.lstat(transfer.targetPath); | ||
| if (!currentStat.isFile() || stableLocalFileIdentity(currentStat) !== transfer.publishedLocalIdentity) { |
There was a problem hiding this comment.
Follow supported destination symlinks for the identity check
When a download targets an existing symlink, inspectLocalPromotionTarget() deliberately resolves and publishes to its regular-file target, but this lstat() observes the symlink itself. Consequently currentStat.isFile() is false and mtime preservation is skipped even after the identity representation is corrected, causing symlink-targeted folder downloads to miss skip-unchanged matches; verify the resolved target (while retaining the concurrent-change protection) instead.
Useful? React with 👍 / 👎.
| // own timestamps (Codex P2 on the commit boundary). | ||
| if (transfer.publishedLocalIdentity) { | ||
| try { | ||
| const currentStat = await fs.promises.lstat(transfer.targetPath); |
There was a problem hiding this comment.
Stamp the verified inode rather than its pathname
If another process replaces the destination after this identity check resolves but before fs.promises.utimes() executes, the pathname-based call still stamps that concurrent file with the transfer source's timestamps. The fresh evidence is that the newly added guard performs lstat and utimes as separate pathname operations, leaving the same check-to-use window; open and verify a handle before applying times through that handle, or stamp the prepared inode before publication.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec1868a069
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d6c487fc6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7639bd83d5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
The automatic Codex fix was preserved, but it did not pass verification. The PR remains draft for maintainer review. The candidate patch and verification report were preserved as codex-fix-patch-34015273615. View this run: https://github.com/binaricat/Netcatty/actions/runs/34015273615 |
Summary
Local download publication or rollback could overwrite a file another process had just saved. Later pathname-based timestamp updates could also modify a concurrent replacement. Publication and restoration now share one exclusive operation, preserving recovery artifacts on conflicts.
Type of Change
Related Issue (optional)
Related to overwrite and metadata safety in #3186. Reporter permission behavior is not claimed fully reproduced.
Changes Made
Screenshots / Demo
Real filesystem regressions cover publication/restoration replacement races, late cancellation, timestamps, mode 000, final-path replacement and close failure. Publication/bridge/SCP focused checks passed; a real loopback download matched its source.
Testing
npm run dev)npm run lint)npm test)npm run generate:capability-tools)All five audit branches were combined in an isolated checkout: 11,469 passed, 0 failed, 18 skipped. Lint and production build passed. Each fix and its follow-ups received independent reviews. These are local results; GitHub checks report their current state separately.
The exclusive-copy fallback can expose partial bytes while copying; it preserves recoverable data but is not atomic visibility. Physical FAT/exFAT hardware was unavailable.
Full evidence and architecture comparison: SFTP audit report.
Checklist