Skip to content

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

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

fix(libsync): folder move or rename data loss.#9998
camilasan wants to merge 5 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. Three bugs compound to cause this:

  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 5 times, most recently from ecd2eee to 18e5916 Compare June 12, 2026 09:24
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>
@camilasan camilasan force-pushed the bugfix/sync-move-data-loss branch from 18e5916 to 8adb02e Compare June 12, 2026 09:33
@github-actions

Copy link
Copy Markdown
Contributor

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

Digest: sha256:b45f7f74466b8914db533a0bc89a4d99729105275d9c879cd8d42e65d1f5ee6a

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

Quality Gate Failed Quality Gate failed

Failed conditions
28 New Code Smells (required ≤ 0)
E 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

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