1010from cloudinary_cli .utils .api_utils import query_cld_folder , upload_file , download_file
1111from cloudinary_cli .utils .file_utils import walk_dir , delete_empty_dirs , get_destination_folder
1212from cloudinary_cli .utils .json_utils import print_json
13- from cloudinary_cli .utils .utils import logger , run_tasks_concurrently , confirm_action
13+ from cloudinary_cli .utils .utils import logger , run_tasks_concurrently , confirm_action , get_user_action
1414
1515DELETE_ASSETS_BATCH_SIZE = 100
1616
2424@option ("--pull" , help = "Pull changes from your Cloudinary folder to your local folder." , is_flag = True )
2525@option ("-w" , "--concurrent_workers" , type = int , default = 30 , help = "Specify number of concurrent network threads." )
2626@option ("-F" , "--force" , is_flag = True , help = "Skip confirmation when deleting files." )
27- def sync (local_folder , cloudinary_folder , push , pull , concurrent_workers , force ):
27+ @option ("-K" , "--keep-unique" , is_flag = True , help = "Keep unique files in the destination folder." )
28+ def sync (local_folder , cloudinary_folder , push , pull , concurrent_workers , force , keep_unique ):
2829 if push == pull :
2930 raise Exception ("Please use either the '--push' OR '--pull' options" )
3031
31- sync_dir = SyncDir (local_folder , cloudinary_folder , concurrent_workers , force )
32+ sync_dir = SyncDir (local_folder , cloudinary_folder , concurrent_workers , force , keep_unique )
3233
3334 if push :
3435 sync_dir .push ()
@@ -39,11 +40,12 @@ def sync(local_folder, cloudinary_folder, push, pull, concurrent_workers, force)
3940
4041
4142class SyncDir :
42- def __init__ (self , local_dir , remote_dir , concurrent_workers , force ):
43+ def __init__ (self , local_dir , remote_dir , concurrent_workers , force , keep_deleted ):
4344 self .local_dir = local_dir
4445 self .remote_dir = remote_dir
4546 self .concurrent_workers = concurrent_workers
4647 self .force = force
48+ self .keep_unique = keep_deleted
4749
4850 self .verbose = logger .getEffectiveLevel () < logging .INFO
4951
@@ -72,14 +74,16 @@ def _get_out_of_sync_file_names(self, common_file_names):
7274 out_of_sync_file_names = set ()
7375 for f in common_file_names :
7476 if self .local_files [f ]['etag' ] != self .remote_files [f ]['etag' ]:
77+ logger .warning (f"{ f } is out of sync" )
78+ logger .debug (f"Local etag: { self .local_files [f ]['etag' ]} . Remote etag: { self .remote_files [f ]['etag' ]} " )
7579 out_of_sync_file_names .add (f )
7680 continue
7781 logger .debug (f"{ f } is in sync" )
7882
7983 return out_of_sync_file_names
8084
8185 def push (self ):
82- if not self ._delete_unique_remote_files ():
86+ if not self ._handle_unique_remote_files ():
8387 logger .info ("Aborting..." )
8488 return False
8589
@@ -101,14 +105,10 @@ def push(self):
101105
102106 run_tasks_concurrently (upload_file , uploads , self .concurrent_workers )
103107
104- def _delete_unique_remote_files (self ):
105- if not len (self .unique_remote_file_names ):
106- return True
107-
108- if not (self .force or confirm_action (
109- f"Running this command will delete { len (self .unique_remote_file_names )} remote files. "
110- f"Continue? (y/N)" )):
111- return False
108+ def _handle_unique_remote_files (self ):
109+ handled = self ._handle_files_deletion (len (self .unique_remote_file_names ), "remote" )
110+ if handled is not None :
111+ return handled
112112
113113 logger .info (f"Deleting { len (self .unique_remote_file_names )} resources "
114114 f"from Cloudinary folder '{ self .remote_dir } '" )
@@ -140,14 +140,12 @@ def _delete_unique_remote_files(self):
140140 return True
141141
142142 def pull (self ):
143- if not self ._delete_unique_local_files ():
144- logger .info ("Aborting..." )
143+ if not self ._handle_unique_local_files ():
145144 return False
146145
147146 files_to_pull = self .unique_remote_file_names | self .out_of_sync_file_names
148147
149148 logger .info (f"Downloading { len (files_to_pull )} files from Cloudinary" )
150-
151149 downloads = []
152150 for file in files_to_pull :
153151 remote_file = self .remote_files [file ]
@@ -158,14 +156,10 @@ def pull(self):
158156
159157 run_tasks_concurrently (download_file , downloads , self .concurrent_workers )
160158
161- def _delete_unique_local_files (self ):
162- if not len (self .unique_local_file_names ):
163- return True
164-
165- if not (self .force or confirm_action (
166- f"Running this command will delete { len (self .unique_local_file_names )} local files. "
167- f"Continue? (y/N)" )):
168- return False
159+ def _handle_unique_local_files (self ):
160+ handled = self ._handle_files_deletion (len (self .unique_local_file_names ), "local" )
161+ if handled is not None :
162+ return handled
169163
170164 logger .info (f"Deleting { len (self .unique_local_file_names )} local files..." )
171165 for file in self .unique_local_file_names :
@@ -177,3 +171,39 @@ def _delete_unique_local_files(self):
177171 delete_empty_dirs (self .local_dir )
178172
179173 return True
174+
175+ def _handle_files_deletion (self , num_files , location ):
176+ if not num_files :
177+ logger .debug ("No files found for deletion." )
178+ return True
179+
180+ decision = self ._handle_files_deletion_decision (num_files , location )
181+
182+ if decision is True :
183+ logger .info (f"Keeping { num_files } { location } files..." )
184+ return True
185+ elif decision is False :
186+ logger .info ("Aborting..." )
187+ return False
188+
189+ return decision
190+
191+ def _handle_files_deletion_decision (self , num_files , location ):
192+ if self .keep_unique :
193+ return True
194+
195+ if self .force :
196+ return None
197+
198+ decision = get_user_action (
199+ f"Running this command will delete { num_files } { location } files.\n "
200+ f"To keep the files and continue partial sync, please choose k.\n "
201+ f"Continue? (y/k/N)" ,
202+ {
203+ "y" : None ,
204+ "k" : True ,
205+ "default" : False
206+ }
207+ )
208+
209+ return decision
0 commit comments