Skip to content

Prevent duplicate database records for the same file provider item - #10020

Merged
i2h3 merged 1 commit into
masterfrom
i2h3/fix/9695761
May 12, 2026
Merged

Prevent duplicate database records for the same file provider item#10020
i2h3 merged 1 commit into
masterfrom
i2h3/fix/9695761

Conversation

@i2h3

@i2h3 i2h3 commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Iron out logical-path duplicates in RealmItemMetadata and 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 different ocId and syncTime ~2h apart. RealmItemMetadata uses ocId as its primary key, so Realm's update: .all / .modified upserts dedupe by ocId alone. When the server returns the same logical path with a fresh ocId (restore-from-trash, another client recreating the item
during 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

  • New 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 different ocId. Skips in-flight rows (status ≠ normal) and lock files of local origin. Bumps syncTime on evicted rows so pendingWorkingSetChanges
      signals the framework.
    • cleanupPreexistingLogicalDuplicates() — one-shot startup pass: buckets every non-deleted, non-lock row by (account, serverUrl, fileName), keeps the row with the most recent syncTime per bucket (lexicographically greater ocId breaks ties), soft-deletes the rest. Opens a write transaction only when
      collisions exist — zero cost for clean databases.
  • FilesDatabaseManager.swift
    • addItemMetadata runs evictLogicalDuplicates inside its existing write transaction, so every caller of the function (Item create/modify/fetch/trash/lockfile, materialised observer, paginated enumeration via addItemMetadataPreservingLocalState, the trash-marking pass in Enumerator.swift) is covered.
    • depth1ReadUpdateItemMetadatas pre-evicts for every item in metadatasToCreate and metadatasToUpdate inside its existing transaction; the update path is included defensively for rename targets that collide with a third row.
    • addItemMetadataPreservingLocalState falls back to a logical-address lookup when the ocId lookup misses, and carries over keepDownloaded / downloaded / visitedDirectory / lockToken if exactly one candidate exists. Without this, an ocId rotation would silently drop the user's "Always keep
      downloaded" 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.
    • init calls cleanupPreexistingLogicalDuplicates() after Realm initialisation succeeds.

Edge cases

  • In-flight rows are never touched in either path; soft-deleting one would yank the file out from under a pending NSURLSession task. Skips are logged at error level for support visibility.
  • Lock files of local origin are excluded as both candidates and evictors, mirroring the existing exclusion in pendingWorkingSetChanges.
  • Same-ocId upserts remain a no-op for eviction (the upserted row is excluded from the collision query).
  • Working-set signalling fires only for rows the framework has previously been told about via enumeration. Duplicates that were never enumerated to the framework are still corrected in the database, but they don't surface as a "delete" event — which is correct, since the framework has no reference to delete.

Out of scope

Eviction inside renameItemMetadata and renameDirectoryAndPropagateToChildren is 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-induced
collision will heal on the next enumeration of the destination directory.

Risks

  1. Soft-deletion signals the framework via pendingWorkingSetChanges only 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.
  2. The transient window where addItemMetadata collides with an in-flight sibling leaves two live rows briefly; the next post-completion write heals it. Acceptable for a hotfix targeting persistent duplicates.
  3. The upload finalizer in Item+Modify.swift now correctly evicts the prior local-ocId row when the server assigns a new ocId — desired, but a behavioural change from the prior code that left both rows present.
  4. The medium-term Realm replacement should treat (account, serverUrl, fileName) as a unique constraint and handle ocId rotation at the schema level. This hotfix addresses the symptom, not the underlying race.

@i2h3 i2h3 added this to the 33.0.5 milestone May 12, 2026
@i2h3 i2h3 self-assigned this May 12, 2026
@i2h3 i2h3 added bug os: 🍎 macOS Apple macOS, formerly also known as OS X feature: 📁 file provider macOS File Provider Extension, more general also known as virtual file system. labels May 12, 2026
@github-project-automation github-project-automation Bot moved this to 🧭 Planning evaluation (don't pick) in 💻 Desktop Clients team May 12, 2026
@i2h3 i2h3 moved this from 🧭 Planning evaluation (don't pick) to 🏗️ In progress in 💻 Desktop Clients team May 12, 2026
@i2h3
i2h3 requested a review from Copilot May 12, 2026 11:43
@i2h3

i2h3 commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

/backport to stable-33.0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 addItemMetadata and depth1ReadUpdateItemMetadatas.
  • Improve local-state preservation on ocId rotation by falling back to a logical-address lookup in addItemMetadataPreservingLocalState, 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]>
@i2h3
i2h3 force-pushed the i2h3/fix/9695761 branch from ad3c65a to 658dbe8 Compare May 12, 2026 13:13
@github-actions

Copy link
Copy Markdown
Contributor

Artifact containing the AppImage: nextcloud-appimage-pr-10020.zip

Digest: sha256:6636a8b0f9aff5c1a2e7d265b314e37db9021bc997f7c2bb87a92bdbb60e61d4

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.

@sonarqubecloud

Copy link
Copy Markdown

@i2h3
i2h3 merged commit 6da6c95 into master May 12, 2026
20 of 21 checks passed
@i2h3
i2h3 deleted the i2h3/fix/9695761 branch May 12, 2026 14:33
@github-project-automation github-project-automation Bot moved this from 🏗️ In progress to ☑️ Done in 💻 Desktop Clients team May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug feature: 📁 file provider macOS File Provider Extension, more general also known as virtual file system. os: 🍎 macOS Apple macOS, formerly also known as OS X

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants