Conversation
`SyncDir._local_candidates` matched remote names to local files with `re.match` on an unescaped pattern. `re.match` checks only the start of the name, so the remote name `notes` matched the local `notes.txt`. In dynamic folder mode, with two remote copies named `notes` and a `.cld-sync` entry `notes.txt -> notes`, `sync --push` then: - counted `notes.txt` as in sync and skipped its upload; - deleted both remote copies. The file was missing from Cloudinary until the next run. Escape the name parts and use `fullmatch`. Add an offline unit test for the matching and a mocked sync-level test for the deletion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TalLevAmi
marked this pull request as ready for review
September 27, 2026 07:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cld sync --pushcan delete every remote copy of a file and upload nothing, so the file is missing from Cloudinary until the next run.SyncDir._local_candidates(cloudinary_cli/modules/sync.py) builds the patternf"({candidate_path}|{filename} \(\d+\){extension})"and uses it withre.match.re.matchchecks only the start of the name, so the remote namenotesmatches the local filenotes.txt. The name is also not escaped, so+,[or(in a file name act as regex syntax.How the data is lost, in dynamic folder mode:
.cld-syncholdsnotes.txt -> notes(a raw file loses its extension in the display name, see Fixsyncfor raw files whose display name has no extension #124).notes.notes.txtand gets the keynotes.txt. The second copy getsnotes (1).notes, so sync treats both remote copies as unique remote files and deletes them. It findsnotes.txtin the remote list, so it reports "Skipping 1" and uploads nothing.Brief Summary of Changes
_local_candidates: escape the name parts withre.escape, and usefullmatchso only the whole name matches.test/test_modules/test_cli_sync.py: a new offline classTestCLISyncDuplicateNamesOffline. It needs no cloud.test_local_candidates_exact_matchtests the matching function:notesagainstnotes.txt,notesXandnotes (1).txt, and escaping (a+b.jpgagainstaab.jpg).test_sync_push_does_not_delete_all_duplicates_of_synced_filerunssync --pushwith mocked API calls and the.cld-syncmapping. It fails when both remote copies are deleted and nothing is uploaded.What does this PR address?
Are tests included?
Reviewer, please note:
master: pushnotes.txt, upload one more copy namednotes, then runsync --push -F. Result: "Skipping 1", "Deleted 2 resources", no upload, and 0 assets left on Cloudinary. The dry run said "Would delete 2 resources" next to "Skipping 1", so it did not show that the file would be missing.notes: the dry run says "Would delete 3" and "would upload notes.txt". The real run deletes 3 and uploads 1, so one copy stays.syncfor raw files whose display name has no extension #124: Fixsyncfor raw files whose display name has no extension #124 keeps the extension of raw files, which removes the most common trigger (notes.txt -> notes). This PR fixes the matching itself, which is wrong with or without Fixsyncfor raw files whose display name has no extension #124. The two PRs do not touch the same lines.notes (1)andnotes (2), and nevernotes, so they are deleted and uploaded again on each push. This causes extra traffic, but no data loss.Checklist:
Tests:
test/test_modules/test_cli_sync.py(including the live sync tests),test/test_file_utils.pyandtest/test_utils.py: 54 passed. The two new tests fail onmaster(the sync-level test reportsdeleted ['pid_a2', 'pid_a1'], uploaded []) and pass with the fix. CI runs the full suite.🤖 Generated with Claude Code