Repository navigation
Improve file operation safety Part 3 - #239
Merged
Merged
Conversation
Changing or clearing the explorer filter keeps only the items that were selected and visible, so items a filter hid while Ctrl+A or a shift range selected them never become selected when they show again; the index logic of the selection is a SelectionModel with native tests in the new nui-sftp-frontend-test
Uploads and downloads from the explorer share one flow in SideModel: the candidates, the transfer question, the existence check (FileEngine::existsBatchLocal for local destinations) and the overwrite questions decided by OverwriteDecisions, which accepts an absent destination without consent to overwrite and "All" only for existing ones; the question for a single download names the destination like the one for an upload
Renaming in the explorer is one flow in SideModel for both sides: the local side renames through FileEngine::renameLocal like the remote side through FileEngine::rename, both ask before replacing an existing item and show failures with the texts of their own side, the local side no longer with the remote side's texts
The side models share favorites, their context menu entries and directory navigation in SideModel, both go through the checked item paths so a name that is no single path segment is refused instead of navigated to or added; local listings and properties read entries with one directoryEntryOf, backendCompressionCode lives next to the archive codecs, and opening a remote file with or without watching is one function
Items dropped on the ".." entry go into the parent directory and items dropped or pasted onto a link to a directory go into it; a drop or paste onto a directory whose name is not valid UTF-8 is refused with a message instead of going elsewhere
Navigating normalizes the path with the side's path type, so a path typed with a trailing separator lists the directory without it and ".." leads to its parent; each side computes the parent with RemotePath::parent or LocalPath::parent, which gives a drive root like C:/ on Windows; a failed listing shows the listed directory and its items again, sorted, without listing it again on either side
Synchronize is offered only for directories whose names are valid UTF-8, so a sync never starts on the display name of a directory that cannot be named in a path
Listings move their entries into the explorer items instead of copying them, and the explorer sorts with one comparator that reads names as string views, puts the parent link first by rotating it to the front and orders directories before files; the order is NuiFileExplorer::sortItems with native tests
New items and renames check the name with the rules of their own side, so a remote name may contain a backslash and a Windows name is refused for the Windows reasons, and the invalid name dialog says which rule the name breaks; both sides ask for a new item through SideModel::promptNewItem with their own texts
Delete confirmations list the full path of every item on both sides, and a local listing reply reaches the explorer only through SideModel::onDirectoryListing, which drops a reply for a directory the side has left and shows a failed listing with the same dialog as the remote side
RPC replies and dialog answers of the side models, the file engine's channel creation, the frontend operation queue and opening a local file run only while their owner exists, so closing a session while a listing, a transfer request or a confirmation is pending no longer touches the closed session
Archive names are built with fmt, the side models fill places, path suggestions and bulk delete entries with algorithms, "All" in the non-empty directory question takes the remaining directories in one insert, and the archive transfer docs describe what the dialog does
The lock and metadata of a file tracking instance live in its hidden .nui-sftp-tracking directory, so a tracked remote file named metadata.json or <id>.lock no longer collides with them; instance directories of older builds are still recognized and cleaned up by their legacy lock and metadata, which get the death timestamp mirrored, and fileStates lists directories and leaves the bookkeeping out
An efsw watch handle no longer keeps the watcher of its instance alive, so destroying or cleaning up an instance stops its watcher before the listener goes away, and assigning a watch handle removes the watch it held
File tracking has pure, natively tested helpers for its bookkeeping: serverChanged compares a known server state with the current one, remotePathOfChange maps a local change, meaningOfMove tells a rename from a save through a temporary file, KnownServerContent holds what the server has and PendingUploads keeps one upload per path and holds back changes below a directory being uploaded
A confirm dialog that another one replaces runs its onClose without a button before the new one shows, so every caller hears exactly once how its dialog ended
FileTracking::createInstance replies with the canonical instance directory, spelled like the paths of the change events and of fileStates that the frontend maps onto it
File tracking keeps one upload per changed path from the change until the server's new state is recorded, so a save during an upload no longer asks falsely and the status shows Uploading for the whole upload; server-changed questions wait for each other and one replaced by another dialog clears its upload; a move over a known file (an editor saving through a temporary file) uploads the destination and removes the source's server copy instead of a failing rename; a new directory is uploaded once with the changes below it waiting for it; deletes are mirrored only for what the server is known to have; entries live in a map by instance id and every callback runs only while the panel exists
The opener judges risky files with shared, natively tested helpers and per-platform lists: Windows adds Java, Python, ClickOnce, MSIX/AppX and settings shortcuts to AssocIsDangerous, macOS adds location files, preference panes, screen savers and configuration profiles and judges and opens a symlink as its target, Linux counts an execute bit only for ELF or #! content and adds deb, rpm, flatpak and snap installers
RpcFilesystem::open reads the risky-file policy from an atomic snapshot that every state save refreshes, instead of reading the state cache from its strand while a save replaces it
The help texts name what the settings do: marking downloads covers every download and lists the Mark of the Web and quarantine effects (warnings, Office Protected View with macros blocked, PowerShell RemoteSigned, Gatekeeper) and no effect on Linux; the risky-file policy covers Open With and the Linux execute-bit rule; the PTY option says what a Windows session without a PTY cannot do
Process::spawn builds the child with one launcher call, the platform's default launcher with CREATE_NO_WINDOW and wide arguments on Windows, finds the executable in one const step, and its Windows exit wait holds the process only weakly
parseProgramOptions returns the help or a parse error with the help instead of printing it, a parse error exits with 1, and on Windows main attaches the console it was started from for --console or, lazily, for that message, so a mistyped option is reported
Dropped text is parsed by natively tested helpers: text/plain splits into trimmed lines so paths with spaces stay whole, a file URL with an empty or localhost authority names a local path and another host a UNC path on Windows only, the drive letter is unwrapped only on Windows and only for /X:, decoded paths with NUL or invalid UTF-8 are refused, and plain text counts only as an absolute path of the backend's platform
Saving all terminals of a closed session plans the numbered names with a natively tested NumberedSaves state object, gives every terminal its own attempts at a free name, and tells the user which names were written and which terminals could not be saved; the chain holds the save state, no longer the panel
Dialog headers are Title Case in English and the texts of this branch say what the code does: skipped sync entries are counted and explained in one short notice, block reasons name special files, Windows links, case and normalization, refusals and partial deletes say that nothing or everything else was deleted, the risky-file, Mark of the Web and file tracking texts read plainly in both languages, and the overwrite question drops its unused description
Toasts keep the line breaks of multi-line messages and show at most eight lines, the notification log has the rest
The backend helpers exposed for tests live in named namespaces like the others there: BulkTransferDetail::treeEntryPaths and ScanProgressDetail::sequentialScanProgress instead of a global Detail
Removed the includes of paths/native.hpp from the operation queue, session, bulk upload and transfer, delete operation and Windows drag and drop sources, none of which use it
The terminal exit callback names and the node command lines are built with fmt::format
FileTracking::discardContent removes declined content of a tracking instance: the watches end first, links are removed and never followed, paths outside the instance directory or in its bookkeeping are refused, and an instance left without content is destroyed
File tracking rows are found by their instance, and FileTrackingPanel::discard ends a watch like Stop and removes the declined download through FileTracking::discardContent
Declining to open a downloaded file that can run code removes the download: Open and Open and Watch end the watch, remove the local copy and end the tracking instance, a local file opened in place stays
The orphaned count of File Tracking follows Stop and declined downloads
Transfers inherit the source's mode when a profile does not set Inherit Permissions, like the seeded default profile: a downloaded and re-uploaded 0644 file stays 0644 and a script keeps its execute bits; Persistence::defaultInheritPermissions is the one default for the operation options, the seeded profile and the queue, and an explicit false still uses the configured or default modes
Inherit Permissions shows as on in the settings while a profile does not set it, the effective default, and engaging it starts from on; a disengageable setting can be given the value its unset state stands for with Setting::fallback
Transfers continue a partial temp file when a profile does not set Try Continue, like the seeded default profile, and the settings show Try Continue and Mark Downloads as Internet Files as on while unset; Persistence::defaultTryContinue and defaultMarkAsDownloaded are the one default for the operation options, the seeded profile, the archive download and the settings
markAsDownloaded has tests: it keeps the content, Windows reads back the internet zone, macOS the quarantine attribute and a missing file's error, and Linux leaves files unmarked
The command store tests compile on Windows: the read-only checks ask whether they run as root only where there is a root
The LocalPath JSON round trip test uses an absolute path of the platform it runs on, and the Linux execute-bit risk test does not run on Windows, where every file reads as executable
5cript
enabled auto-merge (squash)
October 16, 2026 14:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Safer file operations, part 3 of 3: explorer, file tracking, opener and defaults.
SelectionModel); new nativenui-sftp-frontend-testtarget..go to the parent and drops onto a directory link go into it; new names are checked by their own side's rules.nui-sftp-trackingdirectory; watch handles no longer keep watchers aliveProcess::spawnuses one launcher call; mistyped command line options are reported