Skip to content

Commit 2c6f9ed

Browse files
rebase commit on new master
1 parent 9a989d0 commit 2c6f9ed

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

cloudinary_cli/modules/clone.py

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit
33
import cloudinary
44
from cloudinary_cli.utils.utils import run_tasks_concurrently
5-
from cloudinary_cli.utils.api_utils import upload_file
6-
from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict
5+
from cloudinary_cli.utils.api_utils import upload_file, handle_api_command
6+
from cloudinary_cli.utils.json_utils import print_json
7+
from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict, config_to_tuple_list
78
from cloudinary_cli.defaults import logger
89
from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination
910

@@ -32,7 +33,7 @@
3233
@option("-w", "--concurrent_workers", type=int, default=30,
3334
help="Specify the number of concurrent network threads.")
3435
@option("-fi", "--fields", multiple=True,
35-
help="Specify whether to copy tags and/or context. Valid options: `tags,context`.")
36+
help="Specify whether to copy tags and/or context. Valid options: `tags,context,metadata`.")
3637
@option("-se", "--search_exp", default="",
3738
help="Define a search expression to filter the assets to clone.")
3839
@option("--async", "async_", is_flag=True, default=False,
@@ -54,19 +55,31 @@ def clone(target, force, overwrite, concurrent_workers, fields, search_exp, asyn
5455
return False
5556

5657
source_assets = search_assets(force, search_exp)
58+
if 'metadata' in fields:
59+
source_metadata = list_metadata_items("metadata_fields")
60+
if source_metadata.get('metadata_fields'):
61+
target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config))
62+
fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields")
63+
source_metadata_rules = list_metadata_items("metadata_rules")
64+
if source_metadata_rules.get('metadata_rules'):
65+
target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config))
66+
rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name")
67+
else:
68+
logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow"))
69+
else:
70+
logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow"))
5771

5872
upload_list = []
5973
for r in source_assets.get('resources'):
6074
updated_options, asset_url = process_metadata(r, overwrite, async_, notification_url,
6175
normalize_list_params(fields))
6276
updated_options.update(config_to_dict(target_config))
6377
upload_list.append((asset_url, {**updated_options}))
64-
6578
if not upload_list:
66-
logger.error(style(f'No assets found in {cloudinary.config().cloud_name}', fg="red"))
79+
logger.error(style(f"No assets found in {cloudinary.config().cloud_name}", fg="red"))
6780
return False
6881

69-
logger.info(style(f'Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}', fg="blue"))
82+
logger.info(style(f"Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}", fg="blue"))
7083

7184
run_tasks_concurrently(upload_file, upload_list, concurrent_workers)
7285

@@ -75,7 +88,7 @@ def clone(target, force, overwrite, concurrent_workers, fields, search_exp, asyn
7588

7689
def search_assets(force, search_exp):
7790
search = cloudinary.search.Search().expression(search_exp)
78-
search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name'])
91+
search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name','metadata'])
7992
search.max_results(DEFAULT_MAX_RESULTS)
8093

8194
res = execute_single_request(search, fields_to_keep="")
@@ -84,6 +97,80 @@ def search_assets(force, search_exp):
8497
return res
8598

8699

100+
def list_metadata_items(method_key, *options):
101+
api_method_name = 'list_' + method_key
102+
params = [api_method_name]
103+
if options:
104+
options = options[0]
105+
res = handle_api_command(params, (), options, None, None, None,
106+
doc_url="", api_instance=cloudinary.api,
107+
api_name="admin",
108+
auto_paginate=True,
109+
force=True, return_data=True)
110+
res.get(method_key, []).sort(key=lambda x: x["external_id"])
111+
112+
return res
113+
114+
115+
def create_metadata_item(api_method_name, item, *options):
116+
params = (api_method_name, item)
117+
if options:
118+
options = options[0]
119+
res = handle_api_command(params, (), options, None, None, None,
120+
doc_url="", api_instance=cloudinary.api,
121+
api_name="admin",
122+
return_data=True)
123+
124+
return res
125+
126+
127+
def deep_diff(obj_source, obj_target):
128+
diffs = {}
129+
for k in set(obj_source.keys()).union(obj_target.keys()):
130+
if obj_source.get(k) != obj_target.get(k):
131+
diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)}
132+
133+
return diffs
134+
135+
136+
def compare_create_metadata_items(json_source, json_target, target_config, key, id_field = "external_id"):
137+
list_source = {item[id_field]: item for item in json_source.get(key, [])}
138+
list_target = {item[id_field]: item for item in json_target.get(key, [])}
139+
140+
only_in_source = list(list_source.keys() - list_target.keys())
141+
common = list_source.keys() & list_target.keys()
142+
143+
if not len(only_in_source):
144+
logger.info(style(f"{(' '.join(key.split('_')))} in {dict(target_config)['cloud_name']} and in {cloudinary.config().cloud_name} are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow"))
145+
else:
146+
logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(target_config)['cloud_name']}", fg="blue"))
147+
148+
for key_field in only_in_source:
149+
if key == 'metadata_fields':
150+
try:
151+
res = create_metadata_item('add_metadata_field', list_source[key_field],target_config)
152+
logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green"))
153+
except Exception as e:
154+
logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red"))
155+
else:
156+
try:
157+
res = create_metadata_item('add_metadata_rule', list_source[key_field],target_config)
158+
logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green"))
159+
except Exception as e:
160+
logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red"))
161+
162+
163+
diffs = {}
164+
for id_ in common:
165+
if list_source[id_] != list_target[id_]:
166+
diffs[id_] = deep_diff(list_source[id_], list_target[id_])
167+
168+
return {
169+
"only_in_json_source": only_in_source,
170+
"differences": diffs
171+
}
172+
173+
87174
def process_metadata(res, overwrite, async_, notification_url, copy_fields=""):
88175
cloned_options = {}
89176
asset_url = res.get('secure_url')
@@ -96,6 +183,8 @@ def process_metadata(res, overwrite, async_, notification_url, copy_fields=""):
96183
cloned_options['tags'] = res.get('tags')
97184
if "context" in copy_fields:
98185
cloned_options['context'] = res.get('context')
186+
if "metadata" in copy_fields:
187+
cloned_options['metadata'] = res.get('metadata')
99188
if res.get('folder'):
100189
# This is required to put the asset in the correct asset_folder
101190
# when copying from a fixed to DF (dynamic folder) cloud as if

cloudinary_cli/utils/config_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def get_cloudinary_config(target):
6767
def config_to_dict(config):
6868
return {k: v for k, v in config.__dict__.items() if not k.startswith("_")}
6969

70+
def config_to_tuple_list(config):
71+
return [(k, v) for k, v in config.__dict__.items() if not k.startswith("_")]
72+
7073
def show_cloudinary_config(cloudinary_config):
7174
obfuscated_config = config_to_dict(cloudinary_config)
7275

0 commit comments

Comments
 (0)