Repository navigation
Preserve local changes when the server reports a conflicting version - #504
Open
leifrossau wants to merge 1 commit into
Open
leifrossau wants to merge 1 commit into
leifrossau wants to merge 1 commit into
Conversation
applyDelta overwrote the local metadata and cleared hasChanges whenever the server reported a newer version of a file, so edits that were not yet uploaded (still dirty, or fsynced with the upload still queued) were silently lost on the next delta poll. This is the "TODO check if local has changes and rename the server copy if so". File deltas are now decided by content hash instead of modification time, using a small pure package fs/conflict (unit tested offline): - identical content, or an echo of content we uploaded ourselves: skip - no local changes and no pending upload: overwrite as before, and drop the stale cached content if the file is not open - local changes and different remote content: keep the local version and save the server version as "name (conflict <host> YYYY-MM-DD HHMMSS).ext", downloaded and uploaded as a new item - local changes but no remote hash to compare against: keep local Folders keep accepting newer server metadata as before. To tell whether an upload is pending, the delta goroutine now reads the upload manager's state. The sessions map is still only written by the upload loop, but writes are guarded by a new RWMutex so other goroutines can read it. Fsync reserves its upload before clearing hasChanges, so the delta loop cannot observe a moment with neither local changes nor a pending upload while content is still waiting to be uploaded. The upload manager also remembers the last few content hashes it uploaded per item, so our own uploads coming back through the delta feed are not mistaken for conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
When the delta loop sees a newer server version of a file,
applyDeltaoverwrites the local inode's metadata and setshasChanges = false(right next to//TODO check if local has changes and rename the server copy if so). If the file has local edits that aren't uploaded yet (still dirty, or fsynced with the upload still queued), those edits are silently lost. Deltas are polled every 30 s, so editing the same file on two machines, or in the web UI while a local save is uploading, is enough to trigger it.Fix
For files, the decision is now made by content hash rather than modification time. The logic lives in a small pure package,
fs/conflict, so it can be unit tested without an account:name (conflict <host> YYYY-MM-DD HHMMSS).ext(downloaded and uploaded as a new item)Folders still just accept newer server metadata.
Supporting changes in the upload manager:
IsUploadPending(ids...)lets the delta goroutine ask whether an upload is queued or in flight.sessionsis still only written byuploadLoop, but those writes are now guarded by anRWMutexso other goroutines can read it. The upload manager never takes an inode lock while holding it.Fsyncreserves its upload before clearinghasChanges, so the delta loop never sees "no local changes and no pending upload" while content is still waiting to go out.UploadedHashes), so our own uploads coming back through the delta feed aren't treated as conflicts.hasChangesand pending uploads under the inode lock, to close the race with a concurrent write or fsync.Testing
go test ./fs/conflict/covers the decision table, the conflict-name format (extensions, dotfiles, multi-dot names, invalid hostname characters, same-second counter) and hostname sanitizing.go build ./...andgo vet ./...pass. I could not run the live-account suite; the existingTestDeltaContentChangeBothshould behave the same (same content while dirty → skip; clean file with different content → overwrite), but that is from reading the test, not from running it.Notes for reviewers
conflictBehavior=replaceand noIf-Match, so after a conflict the local version still replaces the server item as before; the server version survives as the conflict copy (and in OneDrive's version history).🤖 Generated with Claude Code