Prevent duplicate database records for the same file provider item - #10020
Conversation
|
/backport to stable-33.0 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Hotfix to prevent Realm from accumulating multiple non-deleted RealmItemMetadata rows for the same logical file-provider item (same (account, serverUrl, fileName)) when ocId rotates, which can cause Finder to display “ 2” suffixed duplicates.
Changes:
- Add logical-address deduplication helpers (startup cleanup + per-write eviction) to soft-delete stale siblings.
- Run eviction before persisting new/updated metadatas in
addItemMetadataanddepth1ReadUpdateItemMetadatas. - Improve local-state preservation on
ocIdrotation by falling back to a logical-address lookup inaddItemMetadataPreservingLocalState, plus comprehensive tests.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
.../FilesDatabaseManagerTests.swift |
Adds tests covering eviction, startup cleanup, in-flight/lock-file exclusions, and local-state preservation on ocId rotation. |
.../FilesDatabaseManager.swift |
Calls startup cleanup after Realm init; evicts logical duplicates before writes; adds logical fallback for local-state preservation. |
.../FilesDatabaseManager+Deduplication.swift |
New deduplication implementation for per-write eviction and one-shot startup cleanup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ile provider item Relates to customer support ticket 9695761. Signed-off-by: Iva Horn <[email protected]>
|
Artifact containing the AppImage: nextcloud-appimage-pr-10020.zip Digest: To test this change/fix you can download the above artifact file, unzip it, and run it. Please make sure to quit your existing Nextcloud app and backup your data. |
|



Iron out logical-path duplicates in
RealmItemMetadataand prevent them from re-accumulating.Customers on unstable connections see arbitrary folders in the file provider appear renamed with a " 2" suffix. macOS does this when the file provider returns two siblings with identical names — the file system can only represent one. A customer Realm confirms the cause: two non-deleted rows at the same
(account, serverUrl, fileName)but differentocIdandsyncTime~2h apart.RealmItemMetadatausesocIdas its primary key, so Realm'supdate: .all/.modifiedupserts dedupe byocIdalone. When the server returns the same logical path with a freshocId(restore-from-trash, another client recreating the itemduring reconnect, the upload finalizer assigning a server-issued
ocId), a second row is inserted beside the first.No schema changes — Realm is being replaced medium-term. This is a focused code-only hotfix.
Changes
FilesDatabaseManager+Deduplication.swift— two helpers:evictLogicalDuplicates(of:in:now:)— within an active write transaction, soft-deletes every non-deleted row at the incoming row's logical address with a differentocId. Skips in-flight rows (status ≠ normal) and lock files of local origin. BumpssyncTimeon evicted rows sopendingWorkingSetChangessignals the framework.
cleanupPreexistingLogicalDuplicates()— one-shot startup pass: buckets every non-deleted, non-lock row by(account, serverUrl, fileName), keeps the row with the most recentsyncTimeper bucket (lexicographically greaterocIdbreaks ties), soft-deletes the rest. Opens a write transaction only whencollisions exist — zero cost for clean databases.
FilesDatabaseManager.swiftaddItemMetadatarunsevictLogicalDuplicatesinside its existing write transaction, so every caller of the function (Item create/modify/fetch/trash/lockfile, materialised observer, paginated enumeration viaaddItemMetadataPreservingLocalState, the trash-marking pass inEnumerator.swift) is covered.depth1ReadUpdateItemMetadataspre-evicts for every item inmetadatasToCreateandmetadatasToUpdateinside its existing transaction; the update path is included defensively for rename targets that collide with a third row.addItemMetadataPreservingLocalStatefalls back to a logical-address lookup when theocIdlookup misses, and carries overkeepDownloaded/downloaded/visitedDirectory/lockTokenif exactly one candidate exists. Without this, anocIdrotation would silently drop the user's "Always keepdownloaded" state — the regression First item in folder not marked for keeping downloaded #9923 originally fixed for the non-rotation case. The merge is conservative: when multiple candidates exist (i.e. the DB is already in the duplicated state), no merge is performed and eviction prunes both prior rows.
initcallscleanupPreexistingLogicalDuplicates()after Realm initialisation succeeds.Edge cases
NSURLSessiontask. Skips are logged aterrorlevel for support visibility.pendingWorkingSetChanges.ocIdupserts remain a no-op for eviction (the upserted row is excluded from the collision query).Out of scope
Eviction inside
renameItemMetadataandrenameDirectoryAndPropagateToChildrenis deliberately not part of this hotfix — the recursive directory rename has subtle correctness traps (children's destination addresses are computed in the same transaction) that aren't worth carrying here. Any rename-inducedcollision will heal on the next enumeration of the destination directory.
Risks
pendingWorkingSetChangesonly when the row has been previously enumerated to the framework. Duplicates that were never enumerated are healed in the DB but don't surface as a "delete" event.addItemMetadatacollides with an in-flight sibling leaves two live rows briefly; the next post-completion write heals it. Acceptable for a hotfix targeting persistent duplicates.Item+Modify.swiftnow correctly evicts the prior local-ocIdrow when the server assigns a newocId— desired, but a behavioural change from the prior code that left both rows present.(account, serverUrl, fileName)as a unique constraint and handleocIdrotation at the schema level. This hotfix addresses the symptom, not the underlying race.