Skip to content

fix(libsync): folder move or rename data loss.#9998

Open
camilasan wants to merge 8 commits into
masterfrom
bugfix/sync-move-data-loss
Open

fix(libsync): folder move or rename data loss.#9998
camilasan wants to merge 8 commits into
masterfrom
bugfix/sync-move-data-loss

Conversation

@camilasan

@camilasan camilasan commented May 7, 2026

Copy link
Copy Markdown
Member

Resolves

When a parent folder is renamed during sync, file changes inside it can be silently lost in 2 ways:

  1. Stale lock tokens: renaming a parent copies child journal records to the new path but keeps the old WebDAV lock token. The next upload sends it at the new URL and the server rejects it with 412/423.
    Clear _lockToken and _locked when writing a renamed child's journal record.
    And No 412/423 recovery: the error handler didn't clear the stale token, so every retry failed the same way.
    Clear the token from the journal on 412/423 and schedule rediscovery.
    fix(file-provider): invalidate lock tokens when file paths change.  #10135

  2. Path prefix bug: removeRecursively used startsWith(dir) instead of startsWith(dir + '/'), causing false prefix matches on sibling paths like A/BC when processing A/B.
    Use startsWith(dir + '/').
    See fix(file-provider): use slash prefix for child item queries. #10130

Steps to test it

Scenario 1 — Stale lock token after folder rename (the main symptom)

  • What it tests: commits 5d8e64c757 and f1a6079 (lock token clearing + 412/423 recovery)
  • Prerequisites: LibreOffice (or any WebDAV-locking editor), a Nextcloud server with file locking enabled, two clients or one client + the web UI.
  1. Sync a folder containing an .odt or .xlsx file, e.g. Work/report.odt.
  2. Open report.odt in LibreOffice — this acquires a WebDAV lock token stored in the journal.
  3. From the web UI or a second client, rename the parent folder: Work → Work2.
  4. Let the desktop client sync the rename (the folder appears as Work2 locally).
  5. In LibreOffice, make a change and Save.
  6. Save. LibreOffice may ask "The file has been changed since it was opened — save anyway?" Click Save anyway.
    See fix(file-provider): invalidate lock tokens when file paths change.  #10135
    ✅ Expected (with fix): save succeeds, Work2/report.odt contents do not get lost.
    🔴 Regression (without fix): the upload fails with 412 or 423, LibreOffice shows a save error, and every subsequent save attempt fails identically because the stale token is never cleared.

Scenario 2 — Path prefix matching (sibling folders not clobbered)

  • What it tests: commit c168186 (slash-prefix fix in removeRecursively)
  1. Create two sibling folders locally: D/ and D2/, each containing a file. Sync both.
  2. Rename D → E locally.
  3. Sync.
    ✅ Expected (with fix): D2/ and its contents remain fully intact after the sync.
    🔴 Regression (without fix): because "D2".startsWith("D") is true, the old logic could mark D2's entries as part of the renamed tree and remove them from the journal.

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@camilasan camilasan added this to the 33.0.5 milestone May 7, 2026
@camilasan camilasan self-assigned this May 7, 2026
@camilasan camilasan changed the title fix(file-provider): folder move or rename data loss. fix(libsync): folder move or rename data loss. May 7, 2026
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch 2 times, most recently from 1fef7ae to e14e53b Compare May 7, 2026 18:43
@sonarqubecloud

sonarqubecloud Bot commented May 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
75.0% Coverage on New Code (required ≥ 80%)
65 New Code Smells (required ≤ 0)
D Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@camilasan

Copy link
Copy Markdown
Member Author

/backport to stable-33.0

@camilasan camilasan modified the milestones: 33.0.5, 33.0.6 May 18, 2026
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch 4 times, most recently from f4873b5 to e5c6a8e Compare June 11, 2026 19:18
@camilasan camilasan marked this pull request as ready for review June 11, 2026 19:18
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch 8 times, most recently from 8db218a to 534aca4 Compare June 15, 2026 09:23
startsWith(deletedDir) without a trailing slash would match sibling
directories sharing a common prefix (e.g. "A/B" matching "A/BC"),
causing their journal records to be skipped in the failure-cleanup
loop. Append '/' before the comparison.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
WebDAV lock tokens are bound to the URL they were issued for. When a
parent directory is renamed, PropagateLocalRename and
PropagateRemoteMove both copy child journal records to the new path
while preserving the old token. Subsequent uploads send the stale
token and receive 412/423 from the server.

Clear _lockToken before writing the new-path record in both code paths.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
When a file path changes (due to a parent folder rename), any WebDAV
lock token stored in the journal becomes invalid because tokens are
bound to the original URL. A subsequent upload sends an If: header with
the stale token, and the server replies 412 (Precondition Failed) or
423 (Locked).

Clear the token from the journal on either status code so the next
sync retries without it. Schedule rediscovery on 412 since the server
state is uncertain.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
Cover the three bug classes fixed in the standard sync engine:

- testLockTokenClearedOnServerInitiatedRename: server renames a folder;
  verifies PropagateLocalRename clears the child's stale lock token and
  locked state in the journal.

- testLockedStateClearedOnClientInitiatedRename: client renames a folder
  while the child file is server-locked; verifies PropagateRemoteMove
  clears the locked state in the new-path journal record.

- testUpload412SchedulesRediscovery: upload gets a 412 response; verifies
  the file is rediscovered and successfully synced on the next attempt.

- testPostRenameBulkDeletePreservesFailedChildRename: folder and child file
  are both renamed, child MOVE fails; verifies the old-path journal record
  is preserved so the next sync can recover the file.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Camila Ayres <hello@camilasan.com>
Force remote rediscovery on both 412 and 423 upload errors so the lock
state cleared from the journal is refreshed from the server, not only the
bad etag on 412. Strengthen the 412 test to assert the parent folder etag
is invalidated.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Camila Ayres <hello@camilasan.com>
Gate the Vfs::WithSuffix data row on isVfsPluginAvailable, mirroring the
existing Vfs::WindowsCfApi handling, so the test skips instead of failing
when the suffix plugin is not built.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Camila Ayres <hello@camilasan.com>
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch 2 times, most recently from 08e21b5 to e22d5c0 Compare June 15, 2026 18:08
@github-actions

Copy link
Copy Markdown
Contributor

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

Digest: sha256:a9b71da5ada77806775fb9c019605a3d872d9e2abb04ceabcca431562506a528

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.

Add isPathInsideDeletedDir as an inline header helper and a unit test
verifying a sibling such as "A/BC" is not treated as a child of "A/B",
guarding the journal cleanup after a failed local remove. Use qWarning
instead of the deprecated QWARN in the testMovedWithError data rows.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Camila Ayres <hello@camilasan.com>
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch from e22d5c0 to 6923867 Compare June 15, 2026 18:21
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Maintainability Rating on New Code (required ≥ A)
28 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant