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." )
4445def 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
6464class 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
0 commit comments