Skip to content

Commit d8576c9

Browse files
Add support for --keep-unique option in sync
1 parent d6d6471 commit d8576c9

File tree

2 files changed

+82
-29
lines changed

2 files changed

+82
-29
lines changed

cloudinary_cli/modules/sync.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from cloudinary_cli.utils.api_utils import query_cld_folder, upload_file, download_file
1111
from cloudinary_cli.utils.file_utils import walk_dir, delete_empty_dirs, get_destination_folder
1212
from 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

1515
DELETE_ASSETS_BATCH_SIZE = 100
1616

@@ -24,11 +24,12 @@
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

4142
class 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

cloudinary_cli/utils/utils.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,32 @@ def run_tasks_concurrently(func, tasks, concurrent_workers):
117117
thread_pool.starmap(func, tasks)
118118

119119

120-
def confirm_action(message):
121-
r = input(message)
122-
if r.lower() != 'y':
123-
return False
120+
def confirm_action(message="Continue? (y/N)"):
121+
"""
122+
Confirms whether the user wants to continue.
123+
124+
:param message: The message to the user.
125+
:type message: string
126+
127+
:return: Boolean indicating whether user wants to continue.
128+
:rtype bool
129+
"""
130+
return get_user_action(message, {"y": True, "default": False})
131+
132+
133+
def get_user_action(message, options):
134+
"""
135+
Reads user input and returns value specified in options.
136+
137+
In case user specified unknown option, returns default value.
138+
If default value is not set, returns None
139+
140+
:param message: The message for user.
141+
:type message: string
142+
:param options: Options mapping.
143+
:type options: dict
124144
125-
return True
145+
:return: Value according to the user selection.
146+
"""
147+
r = input(message).lower()
148+
return options.get(r, options.get("default"))

0 commit comments

Comments
 (0)