Skip to content

Upload and Watch & Watch Rearchitecture - #254

Merged
5cript merged 48 commits into
mainfrom
feat/upload-and-watch
Oct 23, 2026
Merged

5cript merged 48 commits into
mainfrom
feat/upload-and-watch

Conversation

@5cript

@5cript 5cript commented Oct 23, 2026

Copy link
Copy Markdown
Owner
  • Upload and Watch: a new entry on the local side of the file explorer uploads a file or directory, then mirrors its local changes to the server.
  • Open and Watch through a file tracker per session: the backend tracker owns every watch and decides what each change does on the server. The file tracking panel only shows that state and forwards the user's input.
  • Watch options and server-change questions: re-upload on change, mirror moves and deletes, and check the server first. When the server's copy changed meanwhile, the user chooses Replace or Keep.
  • Efficient sync: a change is uploaded once it has settled (judged by events and by the file's size and time). Uploads and deletes are batched into bulk operations. A new directory goes up once, and later only what the server lacks.
  • Upload lifecycle: deleting a file while it uploads stops the upload and removes its partial file. Renaming it continues the upload under the new name from the partial file. Moves between directories become renames on the server.
  • Watches that end on their own: a lost connection ends the session's watches and tells the user why. A downloaded copy that was deleted or renamed away ends its watch after 5 s. A closing session hands its copies to the orphan cleanup.
  • Progress that counts files: "Syncing x of y" counts the entries a directory upload carries, and nothing is counted twice when files are reported late.
  • No deletes on doubt: a path that cannot be checked (Windows long paths, access errors, pending deletion) never costs its server copy. The destination is compared before anything is sent. A directory moved away, to the trash for example, keeps its server copy.
  • Recovery from lost watch events: when efsw reports missed changes, the tracker compares what is there with what the server is known to have. A watched directory deleted on Windows or macOS (told by its identity on the volume) is deleted on the server with its whole tree.

5cript added 30 commits October 22, 2026 02:27
…afely

- 87abe59 freed a WatcherFSEvents on removeWatch while its dispatch queue still ran the callback,
  watch tests crashed in process() and sendFileAction on macOS. 1.7.2 waits for it.
- 1.7.2 waits forever in ~FileWatcherWin32 for the read of a watch whose directory was deleted.
  The fork's 627eb0d frees watches without a read in flight, nothing else differs from 1.7.2.
- handleFileAction takes the old name as std::string const& now.
- Windows opens a std::ifstream's file without FILE_SHARE_DELETE, this one opens it with full sharing
- a watched file could not be renamed, deleted or saved atomically by an editor while its upload ran
- UploadOperationOptions::sourceMayVanish: a file or directory that is gone when the queue starts the upload
  completes it without a transfer instead of failing, also inside a bulk
- For an owner that mirrors deletions apart, like a watch whose files come and go quickly
- OperationQueue::discardResumeBackup: a reconnect does not queue the operation again, for an owner that decides
  anew what goes to the server
- ComparedState::mustStillExist: a path seen present that is gone now counts as changed since the compare, so an
  upload does not recreate a file the server lost behind its owner's back. A sync leaves it off.
- OperationQueue::addUploadOperation takes the comparedDestination of UploadOperation: what a file or link must find
  at the destination to replace it, a directory ignores it
…rror

- Every backend error rings the notification bell; ChangedSinceCompare is the compare doing its job and
  its owner reports it, like the sync dialog's list
- Upload and download finalize, a bulk's skipped entry and the queue's failed operation use the level
…f the queue

- OperationQueue::leaveChangedDestinationsToOwner: such an end completes the operation as canceled with
  its error, a bulk leaves those entries out of its failed ones and keeps them in its indexed failures
- For an owner that tells the user itself; sync does not register, its cards still show those entries as failed
- The change filter and what reports changes need it without the temporary instances
…ttled

- One save shows as several watcher events (inotify reports the writes and the close). Content changes of a file
  are held back until it stayed unchanged for 300 ms, only content changes of the same file merge
- Every other change goes at once, after the held back changes at or below its path and where it moved from,
  which the path order finds without looking at the others
- ReportedChange and ChangeSink describe a change of a watch for whoever reports it; isAtOrBelow compares paths
…h service

- DirectoryWatchService: one efsw watcher for plain watches, one of its own for each recursive watch (efsw holds
  its lock while it walks a tree) and for every watch on macOS (efsw rebuilds a shared FSEvents stream whenever a
  watch comes or goes and drops the events on their way). A directory is watched one way only, its subscribers
  share the watch; InstanceWatch is a subscription
- DirectoryWatcher hands a watch's changes to its strand, filtered and stated on efsw's thread. On macOS it drops
  what FSEvents replays from before the watch began and the modification efsw reports before every removal, and
  gives the watched directory its spelling back where efsw precomposes names (eventSpelling)
- ChangeReporter drops noise (isTransientChange) and settles content changes before a change reaches its sink
- TemporaryDirectoryInstance reports to a sink instead of the frontend. TempDirInstanceManager is a plain class on
  the strand of the sessions, owned by the session manager, which answers the orphan count and the cleanup; its
  file states are typed and list only the requested subtree
- The panel's Open and Watch called the instance RPCs that are gone now, it works again through the backend tracker
- LocalItemWatch watches an item the user has locally: its directory flat, a directory item recursively, only the
  item's changes reach its sink and noise is dropped on efsw's thread
- It follows renames within its directory once they settled, case-only ones and ones FSEvents merges included,
  and takes the item back when an editor writes it anew at the old name; a removal waits until the item stays
  away, a delete and recreate or a Windows rename reported as a removal first is no removal
- A rename onto another watch or a temporary name ends it; changes inside a renamed directory move along, and a
  directory moved out of its directory stops being watched once its last changes arrived
- On macOS the item's own NFD name is matched to efsw's precomposed events
- LocalWatchManager owns them and refuses overlapping watches and the temporary downloads directory, comparing on
  disk (DiskPlace) and folding case where the file system does, with localized refusals
- KnownServerContent: what the server has of a tracked item by local path, searched by path prefix, moved along with
  renames, with the directories that left with their content kept until it turns up elsewhere; on macOS it finds
  a known name spelled in the other Unicode normalization, in every script
- PendingUploads: the uploads on their way per path, what changed again, moved or was deleted meanwhile, and the
  kind a path has at its latest change
- ServerOperationOrder: an operation waits until those decided before it on the same path, above or below it ended
- Move meanings, remote paths of changes and server changes as plain functions
- Open and Watch downloads into an instance of its own and watches the copy there; Upload and Watch watches a local
  item and uploads it under its canonical name. No two watches upload to the same remote path or one below the
  other, and a downloaded copy never moves or deletes the server's file
- Each change decides what the server gets: uploads, renames and deletes follow local edits, renames and removals
  as the watch's options say, in the order decided per path, also while an upload of the path is on its way
- A file goes up with what the tracker last knew of its destination; when the server changed meanwhile the user
  is asked and the upload keeps its place in the order. Directories and links are compared before their upload
- Paths deleted and written anew, paths that turned into the other kind, directories moved within the item,
  symbolic links and macOS spellings in the other normalization keep the server copy right
- Counts the changes on their way and shows its progress at most every 200 ms
- TrackerRemote is what it needs of the server and TrackerView what it shows, both faked in its tests
- BatchingRemote sends what the tracker queues within 100 ms of quiet (1 s at most, 500 entries) as one bulk upload
  and one bulk delete, each tracker callback hears the end of its own path with the server's state after it
- A lone change, links, directories and the first upload of a watch keep their single operation
…onId>

- The session's remote queues the tracker's uploads, renames and deletes on its operation queue with the vanishing
  source allowed, the destination compared and its stop left to the tracker, bulks without a resume backup, and
  stats what an upload left on an SFTP channel's strand
- Requests open, checkUploadAndWatch, uploadAndWatch, stop, setOption, discard and answer; FrontendTrackerView pushes
  the entries, questions, progress and orphan count
- The session manager hands every session the instances, the local watches and the default options from the
  settings; FileTracker::stopAll ends the watches a reloaded frontend left behind
…d input

- The panel lists the tracker's entries in a stable row order, asks its questions, shows the orphaned downloads
  and a "Syncing done of total" bar with how many changes wait for an answer
- Open and Watch on the remote side is one request to the tracker; the hover hints say what it does, and the Move
  and Delete Remote switches of a downloaded copy are off with a tooltip that says why
- A reload ends the watches of the frontend before it through FileTracker::stopAll
- The panel's own tracking state and its RPCs to the instances are gone
- Uploads a file or directory into the listed remote directory, or into the one directory selected on the remote
  side (contextMenuItems gets the other side's selection), and has the tracker watch it from then on
- The tracker refuses an overlapping watch before the upload confirmation, which Upload shares
- The hover hint names where the item goes
…letions

- d71ed84 on 5cript/efsw: five fork commits on 627eb0d (e733a67, fix/inotify-nameless-and-win32-removed-watch) and a
  signed pick of upstream's d5d9828 (SpartanJ/efsw PR #221).
- inotify: an event without a name took the next event's bytes as its name, a tar extract reported Modified dNN/\x01.
- inotify: the thread waits with poll(), select() cannot take a descriptor past FD_SETSIZE. A watcher whose descriptor
  was 1024 or higher reported nothing, or aborted the process with _FORTIFY_SOURCE.
- Win32: deletions held for ReportCrossDirectoryMoves are reported when the watched directory's read ends, and before
  an addition of their path, instead of being dropped or coming late.
- Win32: the next read is armed before the listener runs. A read the delete-pending watched directory refuses is
  reported as missed file actions, and the watch lets the directory go.
- Tracker::stopAll ends the instances of watched and only opened copies, a download on its way opens nothing
- Session::stop stops the file tracking before the tracker goes
- An instance with nothing left in it goes with its directory
…view why

- Tracker::connectionLost ends the watches like stopAll, then shows them as ended (SharedData::EndedWatches)
- TrackerView::showEnded, sent as FileTracker::<id>::onEntriesEnded
- SessionManager::removeSession(connectionLost) calls Session::onConnectionLost before stop()
- The tracker's single operations keep no resume backup, like its bulks: a reconnect queued the first upload of an
  Upload and Watch directory again
- The copy has copyReturnTime (5 s) to come back, saves that delete it or move it aside keep the watch
- The view shows the end as WatchEndReason::CopyGone, the instance goes when nothing is left in it
- The file tracking panel turns onEntriesEnded into a warning: a copy deleted or renamed, or the connection lost
- Language keys fileTracking.watchEndedCopyGone and watchesEndedConnectionLost (en_US, de_DE)
- Utility::checkedSymlinkStatus: status, nothing only for ENOENT/ENOTDIR/ERROR_FILE_NOT_FOUND/ERROR_PATH_NOT_FOUND
- Windows: std::filesystem's not found is looked at again through the \\?\ form, past MAX_PATH a path that exists
  is ERROR_FILENAME_EXCED_RANGE, an unreachable server keeps its own error
- The second look tells there (its status), gone again (nothing) and reachable only as \\?\ (ERROR_INVALID_NAME):
  git deletes and writes files anew, GetFileAttributesW found the new one in between (watch e2e WIN3-N1)
- A path pending deletion on Windows counts as gone: libc++'s remove leaves it so while a watch holds a handle, it
  and what is below it read as access denied, STATUS_DELETE_PENDING tells it from a real denial
- Detail::checkedSymlinkStatus takes the looks from a reader, so a test can make the first one miss
- A server delete only goes when the local path is gone or checked, else it is skipped and logged as an error
- A change whose path cannot be checked is not uploaded, nor taken as gone or as another kind (W6: Windows long paths)
…ents

- Windows reports the writes to a file kept open only when someone else opens or closes it, an upload does both:
  each upload of a slowly written file caused the next one (UD10: 7 uploads of 64 MiB, Linux and macOS 1)
- An upload whose source may vanish and is gone already is refused as FileNotFound, which is no error worth logging
- addBulkUploadOperation takes sourceMayVanish for each entry, the tracker's gathered uploads set it
- The error log capture of the queue tests lives in a header of its own
5cript added 17 commits October 23, 2026 22:05
…nchanged

- UploadOperationOptions::onSent hears of each regular file once it replaced the destination, with its size and
  modification time when those stayed the same from before it was opened
- A file that changed while it was read and a continued upload come without a state, a link is not told
- addUploadOperation and addBulkUploadOperation hand it to every file they upload, below a directory too
…e reported

- An upload's end tells which files it wrote, and in which local state (size, exact modification time) when they
  stayed unchanged
- A change of a file the server got in the state it still has is not uploaded again: the late reports of files a
  directory upload already carried, on Linux, macOS and Windows alike
- A change of a directory the server has uploads only what it lacks below it instead of the whole directory
- What a directory upload wrote that was gone locally by its end is remembered, a file without its state and the
  directory itself as the server tells, so their deletions follow (watch e2e G2: a checkout removed moved-dir while
  its upload sent m0.txt and m1.txt, the server kept them)
- A deletion below a directory upload is looked at again once the upload ended, which may have sent the path before
…rogress

- An upload of a directory counts as the entries below it when it goes, as the late reports of its content when those
  are more, and as what it left on the server once it ended, so "Syncing done of total" counts files
- The changes that waited below a directory still count after its upload took them along, the reports that arrive
  meanwhile count when they go
- A directory that changed again counts no extra change, what it lacks afterwards counts on its own (watch e2e G3 on
  macOS: the total started at 22 per directory of 21 entries)
- A change of a path gone by the time it would go counts as nothing done, its deletion counts on its own (watch e2e
  G3: Windows counted the modified directories its removal had taken)
- The view hears at once that nothing is on its way any more, together with the rows that stopped uploading, after a
  lost connection too
…s why

- watch e2e W3 on Windows: each refused name showed the dialog and a red error notification
… teardown

- cancel() and cancelAll() tell the owners of what they removed, the tracker freed nothing before
- The work loop takes a finished operation out of its queue before its owner hears of it
…a new name

- UploadOperation::partialFileOnCancel: a cancel keeps the temporary file it wrote to for a later upload, or removes
  it, a temporary file it did not open itself always stays
- OperationQueue::cancelUpload cancels an upload with its partial file kept or discarded, discardPartialUpload removes
  one that a cancelled upload kept
- UploadOperationOptions::adoptPartialOf: the upload of a file under its new name takes over the temporary file of the
  upload cancelled under its old name and continues it
- A cancel runs on the strand of the transfer: closing the local file from the queue's thread cut a big-file read
  short, which logged an error and took the flag of the opened partial file, so the stop removed nothing
- The removal stays queued behind the close of the file on the server
- The cancel and finalize logs name the source before the destination
- A file deleted or moved over another during its upload stops it, its partial file goes
- A file renamed during its upload, its first one too, stops it and goes on under the new name from the partial file
- A partial file that no upload continues goes in the server order, a file edited since its stop goes anew
- A file whose server copy was not renamed is uploaded under its new name
- Only an upload sent as a file stops: on Windows a removed directory's last modification arrived after it was gone
  and counted as a file's, which stopped the directory's upload
- Recursive watches turn on efsw's ReportCrossDirectoryMoves: one move with the absolute old path where the platform
  tells it, FSEvents pairs the two halves only when they arrive together
- A file or directory moved to another directory of the item is renamed on the server, not deleted and uploaded anew
… events

- DirectoryWatchService handles handleMissedFileActions on the shared and the own watchers
- DirectoryWatcher lets them past its filters, reportMissed feeds one in tests
- Gone, its removal is held back like a reported one; its own watch losing them sends the Missed change along
- There, the sink gets the Missed change of the directory, or of the item when its directory's watch lost them
- Windows reports a Missed change of its own for a watched directory that turned delete-pending
…w's thread

- Windows drops the buffered changes of a watched directory being deleted when efsw reads them too slowly
- One worker strand per DirectoryWatcher keeps the order, the strand of the handler stays free for the RPCs
…wn to have

- Below a directory that is there, what is gone is deleted like reported and what changed is uploaded
- A watched directory gone or delete-pending goes with its known tree, deepest first, as reported deletions would, on
  Windows only: only a deletion ends the read of a Windows watch, a move keeps it. Elsewhere it may have gone to the
  trash and keeps its server copy, like any directory that left with its content
- A test holds efsw's thread up by a busy millisecond at every event while the watched directory is removed (a sleep
  takes a tick on Windows): in the VM 9 of 10 runs left 1 to 7 entries without the rescan, 30 of 30 pass with it
- The removal tests of larger trees do not run on macOS: FSEvents reports the deletions of the first entries of a
  watched directory removed with a larger tree, 36 to 55 of 72 server entries went
- lstat of /.vol/<dev>/<ino>: ENOENT once deleted, found after a move (also into ~/.Trash), on APFS and HFS+ only
- fsgetpath() still returned the old path of a deleted directory for over a second after its last descriptor closed
- FSEvents stops reporting below a watched directory removed with its content (`rm -rf`): its whole server tree
  stayed, of a larger tree 36 to 55 of 72 server entries went
- Not found on its volume, its known tree goes like one whose deletions were lost; moved away (trash), it stays
- The removal tests of a watched directory run on macOS too, one moved away keeps its server tree everywhere
@5cript
5cript enabled auto-merge (rebase) October 23, 2026 21:24
@5cript
5cript merged commit 40fbf43 into main Oct 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant