nextcloud / nextcloud/desktop

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

Open
#10,645 0 comments 0 reactions 1 assignee Claimed by @claucambra View on GitHub
Dominant language
C++
Stars
3.9k
Forks
1k
Avg merge
1d 17h
Merged PRs (30d)
123

Description

# 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.blend` → `foo.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:**

```
…>

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

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:**

```

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

`` — **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 `` — 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.