Skip to content

[Bug]: Renaming a synced file into an excluded name strands its replacement forever (silent data loss, UI shows synced) #10645

Description

@weiaoli2

Renaming a synced file into an excluded name strands the replacement forever — silent data loss, UI shows "up to date"

Summary

On the macOS File Provider client, if an application saves a file by renaming the
old version to a name that matches an entry in sync-exclude.lst and then writing
a new file under the original name — the standard "keep one backup" save pattern
used by Blender, and by many other applications — then:

  1. the rename to the excluded name is rejected with NSFileProviderErrorDomain -2005
    (cannotSynchronize)
    rather than being handled as "this item has left the
    synced set",
  2. macOS marks that operation permanentThrottling / next: never — it will
    never be retried,
  3. because the rename never lands, the original filename stays occupied on the
    server by the stale item
    , so the newly written file cannot be created — it sits
    at ⧗pathMatching with no server counterpart at all, indefinitely,
  4. the server keeps serving the second-newest version of the file forever, and
  5. the client UI reports everything as fully synced.

Every subsequent save repeats the cycle, so the divergence never heals and never
surfaces. The user's working file exists on exactly one disk.

This is not "a few files are behind". The affected files are permanently pinned to
the last version that synced before the exclude rule first matched, and nothing in
the client says so.

Environment

Client Nextcloud Desktop 34.0.1 (build 34.0.1.0), File Provider / VFS
macOS 15.7.4 (24G517)
Exclude rule *.blend1, added to the user sync-exclude.lst
Application Blender (saves foo.blend by moving it to foo.blend1, then writing a new foo.blend)

User exclude list — ~/Library/Containers/com.nextcloud.desktopclient/Data/Library/Preferences/Nextcloud/sync-exclude.lst:

]~$*
.htaccess
*.blend1        <-- the only relevant addition over the bundled list

Steps to reproduce

  1. Add *.blend1 to sync-exclude.lst and restart the client.
  2. Put foo.blend in the sync folder. Let it upload. Confirm it is on the server.
  3. Open it in Blender and save. Blender renames foo.blendfoo.blend1 and writes
    a new foo.blend.
  4. Wait. The client shows no error and reports the folder as synced.
  5. Download foo.blend from the server web UI and compare it to the local foo.blend.

Expected: the server has the file you just saved.

Actual: the server's foo.blend is byte-for-byte the previous version — it is
identical to the local foo.blend1. The version you saved is on the Mac only, and
will stay that way through every future save.

Verification on a live install

Head-21P-Classic28-v2.blend, saved in Blender on 22 Aug 11:38, downloaded from the
Nextcloud web UI on 23 Aug:

      6900123 bytes  sha256 5f8e3a3b40acea2b…  ~/Downloads/Head-21P-Classic28-v2.blend   <- from server
      6910454 bytes  sha256 7e3e1f3c91b5879e…  …/21P/HEAD/Head-21P-Classic28-v2.blend    <- local, current

DIFFERENT — size delta +10331 bytes, first differing byte at offset 32352

and the server's copy against the local backup file:

      6900123 bytes  sha256 5f8e3a3b40acea2b…  ~/Downloads/Head-21P-Classic28-v2.blend   <- from server
      6900123 bytes  sha256 5f8e3a3b40acea2b…  …/21P/HEAD/Head-21P-Classic28-v2.blend1   <- local backup

IDENTICAL

So the server is serving what Blender considers the backup of the previous save.

Root cause, from fileproviderctl dump

Two records for the same logical file. Names are elided by fileproviderctl;
H{19}2.blend1 is Head-21P-Classic28-v2.blend1, resolved by size.

The old item — server identity of …-v2.blend, now named .blend1 locally:

<s:docID(214739) p:fileID(71589552) n:"H{19}2.blend1" doc sz:6900123
   m:rw-%<70939778:SU:createdLocally> …>

docID(214739) fields:filename|structure content:watch
   sver:fileID(71589552)/H{19}2.blend  cver:72357176@7:sz:6900123
   ⧗throttling|⧗permanentThrottling>
 <-> <fp:✅ 00058984oc3ac1etlfsm …>

Note sver: — the server-side name is .blend, while the local name is now
.blend1. The pending operation syncs fields:filename|structure: it is the
rename, not a content upload. Its state is permanentThrottling.

The new item — the file the user actually saved:

<s:docID(214741) p:fileID(71589552) n:"H{19}2.blend" doc sz:6910454
   m:rw- …>

docID(214741) fields:filename|parentID|…|typeAndCreator content:watch ⧗pathMatching>
 <-> <fp:✅ <unknown>

<fp:✅ <unknown>no server counterpart exists. It is blocked at
⧗pathMatching because its name is still held on the server by docID(214739).

The operation that fails, and how:

i:docID(214739) update-item: 🛑 last:'1787369946 (-21h40min)' next:'never' count:1
   error:'NSError: FP -2005 "The operation couldn't be completed.
   (NSFileProviderErrorDomain error -2005.)" ' domain:cannotSynchronize

One attempt. next: never. Permanent.

The core defect

The same exclude rule produces two different errors from the extension, in the
same domain, minutes apart:

i:docID(214737) create-item: 🔶 … error:'NSError: FP -2010 …' domain:excludedFromSync   <- correct
i:docID(214739) update-item: 🛑 … next:'never' error:'NSError: FP -2005 …' domain:cannotSynchronize   <- wrong

create-item on an excluded name correctly returns -2010 excludedFromSync,
which macOS treats as benign and non-retryable — the item is simply out of scope.

update-item that renames an existing synced item into an excluded name returns
-2005 cannotSynchronize — a generic "the server refused this" — which macOS
treats as a hard failure and permanently throttles.

A rename out of the synced set is not a failure. It means the item has left the
synced set, and the correct propagation is to delete it server-side (as the
legacy non-VFS client does), which frees the filename so the replacement file can
be created. Rejecting the rename instead leaves the stale item squatting on the
real filename forever and orphans the new one.

Blast radius on this install

Six files, all following the same pattern, stuck between 17 and 85 hours, all with
next: never. Every one of them is a .blend whose current contents exist only on
this Mac:

Server-side name (sver) Local name now Stuck for
21P/HEAD/…Classic28-v2.blend .blend1 21h40m
21P/HEAD/…Classic25-v2.blend .blend1 22h07m
21P/HEAD/…Classic25-v1.blend .blend1 45h01m
21P/HEAD/…Round16-v1.blend .blend1 45h08m
21P/BACK/…Round30-v1.blend .blend1 45h12m
26P/HEAD/…Classic26-v1.0.blend .blend1 16h55m

All four .blend files in 21P/HEAD show <fp:✅ <unknown> — none of their current
contents exist on the server.

Throughout, the client UI showed the account as synced with no errors.

Why this is worse than an ordinary sync failure

  • It is silent. There is no error, no badge, no entry in the activity list.
  • It is permanent. next: never means no retry, ever, without intervention.
  • It looks like success. The file is present on the server, at the right path,
    with a plausible size and a recent-ish mtime. Only a byte comparison reveals that
    it is a generation behind.
  • It compounds. Each save strands another version; the gap between server and
    disk widens silently and indefinitely.
  • The excluded file is a backup file, so the natural assumption — "the backup
    doesn't sync, fine" — is exactly the assumption that hides the loss of the
    primary file.

Anyone who excludes an editor's backup-file pattern (*.blend1, *~, *.bak) is
exposed. Note that the bundled list already ships *~, which is precisely the
backup-file convention of a large family of Unix editors.

Suggested fix

Treat a rename whose destination matches the exclude list as the item leaving the
synced set:

  • propagate it as a delete of the server-side item (freeing the name), and
  • return -2010 excludedFromSync rather than -2005 for any excluded-path
    rejection, so macOS stops permanently throttling an item it could otherwise
    reconcile.

At minimum, -2005 … next:'never' on a user-initiated change should be surfaced in
the client UI. An error the OS has permanently given up on should never be invisible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions