Skip to content

Commit 9d36e09

Browse files
authored
Add support for --dry-run option in sync command
1 parent 58d25fb commit 9d36e09

File tree

2 files changed

+67
-12
lines changed

2 files changed

+67
-12
lines changed

cloudinary_cli/modules/sync.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@
4141
@option("-o", "--optional_parameter", multiple=True, nargs=2, help="Pass optional parameters as raw strings.")
4242
@option("-O", "--optional_parameter_parsed", multiple=True, nargs=2,
4343
help="Pass optional parameters as interpreted strings.")
44+
@option("--dry-run", is_flag=True, help="Simulate the sync operation without making any changes.")
4445
def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent_workers, force, keep_unique,
45-
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed):
46+
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run):
4647
if push == pull:
4748
raise UsageError("Please use either the '--push' OR '--pull' options")
4849

4950
sync_dir = SyncDir(local_folder, cloudinary_folder, include_hidden, concurrent_workers, force, keep_unique,
50-
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed)
51-
51+
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run)
5252
result = True
5353
if push:
5454
result = sync_dir.push()
@@ -63,7 +63,7 @@ def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent
6363

6464
class SyncDir:
6565
def __init__(self, local_dir, remote_dir, include_hidden, concurrent_workers, force, keep_deleted,
66-
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed):
66+
deletion_batch_size, folder_mode, optional_parameter, optional_parameter_parsed, dry_run):
6767
self.local_dir = local_dir
6868
self.remote_dir = remote_dir.strip('/')
6969
self.user_friendly_remote_dir = self.remote_dir if self.remote_dir else '/'
@@ -72,6 +72,7 @@ def __init__(self, local_dir, remote_dir, include_hidden, concurrent_workers, fo
7272
self.force = force
7373
self.keep_unique = keep_deleted
7474
self.deletion_batch_size = deletion_batch_size
75+
self.dry_run = dry_run
7576

7677
self.folder_mode = folder_mode or get_folder_mode()
7778

@@ -115,16 +116,16 @@ def __init__(self, local_dir, remote_dir, include_hidden, concurrent_workers, fo
115116
local_file_names = self.local_files.keys()
116117
remote_file_names = self.remote_files.keys()
117118
"""
118-
Cloudinary is a very permissive service. When uploading files that contain invalid characters,
119-
unicode characters, etc, Cloudinary does the best effort to store those files.
120-
121-
Usually Cloudinary sanitizes those file names and strips invalid characters. Although it is a good best effort
122-
for a general use case, when syncing local folder with Cloudinary, it is not the best option, since directories
119+
Cloudinary is a very permissive service. When uploading files that contain invalid characters,
120+
unicode characters, etc, Cloudinary does the best effort to store those files.
121+
122+
Usually Cloudinary sanitizes those file names and strips invalid characters. Although it is a good best effort
123+
for a general use case, when syncing local folder with Cloudinary, it is not the best option, since directories
123124
will be always out-of-sync.
124-
125+
125126
In addition in dynamic folder mode Cloudinary allows having identical display names for differrent files.
126-
127-
To overcome this limitation, cloudinary-cli keeps .cld-sync hidden file in the sync directory that contains a
127+
128+
To overcome this limitation, cloudinary-cli keeps .cld-sync hidden file in the sync directory that contains a
128129
mapping of the diverse file names. This file keeps tracking of the files and allows syncing in both directions.
129130
"""
130131

@@ -168,6 +169,12 @@ def push(self):
168169
if not files_to_push:
169170
return True
170171

172+
if self.dry_run:
173+
logger.info("Dry run mode enabled. The following files would be uploaded:")
174+
for file in files_to_push:
175+
logger.info(f"{file}")
176+
return True
177+
171178
logger.info(f"Uploading {len(files_to_push)} items to Cloudinary folder '{self.user_friendly_remote_dir}'")
172179

173180
options = {
@@ -215,6 +222,14 @@ def pull(self):
215222
if not files_to_pull:
216223
return True
217224

225+
logger.info(f"Preparing to download {len(files_to_pull)} items from Cloudinary folder ")
226+
227+
if self.dry_run:
228+
logger.info("Dry run mode enabled. The following files would be downloaded:")
229+
for file in files_to_pull:
230+
logger.info(f"{file}")
231+
return True
232+
218233
logger.info(f"Downloading {len(files_to_pull)} files from Cloudinary")
219234
downloads = []
220235
for file in files_to_pull:
@@ -348,6 +363,10 @@ def _handle_unique_remote_files(self):
348363

349364
# Each batch is further chunked by a deletion batch size that can be specified by the user.
350365
for deletion_batch in chunker(batch, self.deletion_batch_size):
366+
if self.dry_run:
367+
logger.info(f"Dry run mode enabled. Would delete {len(deletion_batch)} resources:\n" +
368+
"\n".join(deletion_batch))
369+
continue
351370
res = api.delete_resources(deletion_batch, invalidate=True, resource_type=attrs[0], type=attrs[1])
352371
num_deleted = Counter(res['deleted'].values())["deleted"]
353372
if self.verbose:
@@ -395,6 +414,9 @@ def _handle_unique_local_files(self):
395414
logger.info(f"Deleting {len(self.unique_local_file_names)} local files...")
396415
for file in self.unique_local_file_names:
397416
full_path = path.abspath(self.local_files[file]['path'])
417+
if self.dry_run:
418+
logger.info(f"Dry run mode enabled. Would delete '{full_path}'")
419+
continue
398420
remove(full_path)
399421
logger.info(f"Deleted '{full_path}'")
400422

test/test_modules/test_cli_sync.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,36 @@ def test_cli_sync_duplicate_file_names_dynamic_folder_mode(self):
184184
self.assertEqual(0, result.exit_code)
185185
self.assertIn("Skipping 12 items", result.output)
186186
self.assertIn("Done!", result.output)
187+
188+
189+
@retry_assertion
190+
def test_cli_sync_push_dry_run(self):
191+
self._upload_sync_files(TEST_FILES_DIR)
192+
193+
# wait for indexing to be updated
194+
time.sleep(self.GRACE_PERIOD)
195+
196+
result = self.runner.invoke(cli, ['sync', '--push', '-F', self.LOCAL_PARTIAL_SYNC_DIR, self.CLD_SYNC_DIR, '--dry-run'])
197+
198+
# check that no files were uploaded
199+
self.assertEqual(0, result.exit_code)
200+
self.assertIn("Dry run mode enabled. The following files would be uploaded:", result.output)
201+
self.assertIn("Done!", result.output)
202+
203+
204+
@retry_assertion
205+
def test_cli_sync_pull_dry_run(self):
206+
self._upload_sync_files(TEST_FILES_DIR)
207+
208+
# wait for indexing to be updated
209+
time.sleep(self.GRACE_PERIOD)
210+
211+
shutil.copytree(self.LOCAL_PARTIAL_SYNC_DIR, self.LOCAL_SYNC_PULL_DIR)
212+
213+
result = self.runner.invoke(cli, ['sync', '--pull', '-F', self.LOCAL_SYNC_PULL_DIR, self.CLD_SYNC_DIR, '--dry-run'])
214+
215+
# check that no files were downloaded
216+
self.assertEqual(0, result.exit_code)
217+
self.assertIn("Dry run mode enabled. The following files would be downloaded:", result.output)
218+
self.assertIn("Done!", result.output)
219+

0 commit comments

Comments
 (0)