|
1 | 1 | from click import command, option, style |
2 | | -from cloudinary_cli.utils.utils import normalize_list_params, \ |
3 | | - print_help_and_exit |
| 2 | +from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit |
4 | 3 | import cloudinary |
5 | 4 | from cloudinary_cli.utils.utils import run_tasks_concurrently |
6 | 5 | from cloudinary_cli.utils.api_utils import upload_file |
7 | | -from cloudinary_cli.utils.config_utils import load_config |
| 6 | +from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict |
8 | 7 | from cloudinary_cli.defaults import logger |
9 | | -from cloudinary_cli.core.search import execute_single_request, \ |
10 | | - handle_auto_pagination |
| 8 | +from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination |
11 | 9 |
|
12 | 10 | DEFAULT_MAX_RESULTS = 500 |
13 | 11 |
|
14 | 12 |
|
15 | 13 | @command("clone", |
16 | | - short_help="""Clone assets from one account to another.""", |
| 14 | + short_help="""Clone assets from one product environment to another.""", |
17 | 15 | help=""" |
18 | 16 | \b |
19 | | -Clone assets from one environment to another with/without tags and/or context (structured metadata is not currently supported). |
| 17 | +Clone assets from one product environment to another with/without tags and/or context (structured metadata is not currently supported). |
20 | 18 | Source will be your `CLOUDINARY_URL` environemnt variable but you also can specify a different source using `-c/-C` option. |
21 | 19 | Cloning restricted assets is also not supported currently. |
22 | 20 | Format: cld clone -T <target_environment> <command options> |
23 | 21 | `<target_environment>` can be a CLOUDINARY_URL or a saved config (see `config` command) |
24 | 22 | e.g. cld clone -T cloudinary://<api_key>:<api_secret>@<cloudname> -f tags,context |
25 | 23 | """) |
26 | | -@option("-T", "--target", |
| 24 | +@option("-t", "--target", |
27 | 25 | help="Tell the CLI the target environemnt to run the command on.") |
28 | | -@option("-F", "--force", is_flag=True, |
| 26 | +@option("-f", "--force", is_flag=True, |
29 | 27 | help="Skip confirmation.") |
30 | | -@option("-O", "--overwrite", is_flag=True, default=False, |
| 28 | +@option("-o", "--overwrite", is_flag=True, default=False, |
31 | 29 | help="Specify whether to overwrite existing assets.") |
32 | 30 | @option("-w", "--concurrent_workers", type=int, default=30, |
33 | 31 | help="Specify the number of concurrent network threads.") |
34 | | -@option("-f", "--fields", multiple=True, |
| 32 | +@option("-fi", "--fields", multiple=True, |
35 | 33 | help="Specify whether to copy tags and context.") |
36 | 34 | @option("-se", "--search_exp", default="", |
37 | 35 | help="Define a search expression.") |
38 | 36 | @option("--async", "async_", is_flag=True, default=False, |
39 | 37 | help="Generate asynchronously.") |
40 | 38 | @option("-nu", "--notification_url", |
41 | 39 | help="Webhook notification URL.") |
42 | | -def clone(target, force, overwrite, concurrent_workers, fields, search_exp, |
43 | | - async_, notification_url): |
| 40 | +def clone(target, force, overwrite, concurrent_workers, fields, search_exp, async_, notification_url): |
44 | 41 | if not target: |
45 | 42 | print_help_and_exit() |
46 | 43 |
|
47 | | - target_config = cloudinary.Config() |
48 | | - is_cloudinary_url = False |
49 | | - if target.startswith("cloudinary://"): |
50 | | - is_cloudinary_url = True |
51 | | - parsed_url = target_config._parse_cloudinary_url(target) |
52 | | - elif target in load_config(): |
53 | | - parsed_url = target_config._parse_cloudinary_url(load_config().get(target)) |
54 | | - else: |
55 | | - logger.error("The specified config does not exist or the " |
56 | | - "CLOUDINARY_URL scheme provided is invalid " |
57 | | - "(expecting to start with 'cloudinary://').") |
| 44 | + target_config = get_cloudinary_config(target) |
| 45 | + if not target_config: |
| 46 | + logger.error("The specified config does not exist or the CLOUDINARY_URL scheme provided is invalid" |
| 47 | + " (expecting to start with 'cloudinary://').") |
58 | 48 | return False |
59 | 49 |
|
60 | | - target_config._setup_from_parsed_url(parsed_url) |
61 | | - target_config_dict = {k: v for k, v in target_config.__dict__.items() |
62 | | - if not k.startswith("_")} |
63 | | - if is_cloudinary_url: |
64 | | - try: |
65 | | - cloudinary.api.ping(**target_config_dict) |
66 | | - except Exception as e: |
67 | | - logger.error(f"{e}. Please double-check your Cloudinary URL.") |
68 | | - return False |
69 | | - |
70 | | - source_cloudname = cloudinary.config().cloud_name |
71 | | - target_cloudname = target_config.cloud_name |
72 | | - if source_cloudname == target_cloudname: |
73 | | - logger.info("Target environment cannot be the " |
74 | | - "same as source environment.") |
75 | | - return True |
76 | | - |
77 | | - copy_fields = normalize_list_params(fields) |
78 | | - search = cloudinary.search.Search().expression(search_exp) |
79 | | - search.fields(['tags', 'context', 'access_control', |
80 | | - 'secure_url', 'display_name']) |
81 | | - search.max_results(DEFAULT_MAX_RESULTS) |
82 | | - res = execute_single_request(search, fields_to_keep="") |
83 | | - res = handle_auto_pagination(res, search, force, fields_to_keep="") |
| 50 | + if cloudinary.config().cloud_name == target_config.cloud_name: |
| 51 | + logger.error("Target environment cannot be the same as source environment.") |
| 52 | + return False |
| 53 | + |
| 54 | + source_assets = search_assets(force, search_exp) |
84 | 55 |
|
85 | 56 | upload_list = [] |
86 | | - for r in res.get('resources'): |
87 | | - updated_options, asset_url = process_metadata(r, overwrite, async_, |
88 | | - notification_url, |
89 | | - copy_fields) |
90 | | - updated_options.update(target_config_dict) |
| 57 | + for r in source_assets.get('resources'): |
| 58 | + updated_options, asset_url = process_metadata(r, overwrite, async_, notification_url, |
| 59 | + normalize_list_params(fields)) |
| 60 | + updated_options.update(config_to_dict(target_config)) |
91 | 61 | upload_list.append((asset_url, {**updated_options})) |
92 | 62 |
|
93 | | - logger.info(style(f'Copying {len(upload_list)} asset(s) to ' |
94 | | - f'{target_cloudname}', fg="blue")) |
95 | | - run_tasks_concurrently(upload_file, upload_list, |
96 | | - concurrent_workers) |
| 63 | + if not upload_list: |
| 64 | + logger.error(style(f'No assets found in {cloudinary.config().cloud_name}', fg="red")) |
| 65 | + return False |
| 66 | + |
| 67 | + logger.info(style(f'Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}', fg="blue")) |
| 68 | + |
| 69 | + run_tasks_concurrently(upload_file, upload_list, concurrent_workers) |
97 | 70 |
|
98 | 71 | return True |
99 | 72 |
|
100 | 73 |
|
| 74 | +def search_assets(force, search_exp): |
| 75 | + search = cloudinary.search.Search().expression(search_exp) |
| 76 | + search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name']) |
| 77 | + search.max_results(DEFAULT_MAX_RESULTS) |
| 78 | + |
| 79 | + res = execute_single_request(search, fields_to_keep="") |
| 80 | + res = handle_auto_pagination(res, search, force, fields_to_keep="") |
| 81 | + |
| 82 | + return res |
| 83 | + |
| 84 | + |
101 | 85 | def process_metadata(res, overwrite, async_, notification_url, copy_fields=""): |
102 | 86 | cloned_options = {} |
103 | 87 | asset_url = res.get('secure_url') |
|
0 commit comments