cloudinary-cli 1.12.0__tar.gz → 1.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudinary_cli-1.12.0/cloudinary_cli.egg-info → cloudinary_cli-1.14.0}/PKG-INFO +34 -7
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/README.md +33 -6
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/cli_group.py +9 -3
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/__init__.py +1 -1
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/overrides.py +6 -1
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/search.py +2 -1
- cloudinary_cli-1.14.0/cloudinary_cli/modules/clone.py +245 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/sync.py +1 -1
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/video_player +5 -6
- cloudinary_cli-1.14.0/cloudinary_cli/utils/search_utils.py +163 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/utils.py +22 -0
- cloudinary_cli-1.14.0/cloudinary_cli/version.py +1 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0/cloudinary_cli.egg-info}/PKG-INFO +34 -7
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/SOURCES.txt +3 -0
- cloudinary_cli-1.14.0/test/test_modules/test_cli_clone.py +317 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_make.py +6 -0
- cloudinary_cli-1.14.0/test/test_search_utils.py +170 -0
- cloudinary_cli-1.12.0/cloudinary_cli/modules/clone.py +0 -112
- cloudinary_cli-1.12.0/cloudinary_cli/version.py +0 -1
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/LICENSE +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/MANIFEST.in +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/cli.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/admin.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/config.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/provisioning.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/uploader.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/core/utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/defaults.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/make.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/migrate.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/regen_derived.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/modules/upload_dir.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/samples/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/media_library_widget +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/product_gallery +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/html/upload_widget +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/node/upload +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/base +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/explicit +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/find_all_empty_folders +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/python/upload +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/templates/ruby/upload +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/api_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/config_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/file_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli/utils/json_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/dependency_links.txt +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/entry_points.txt +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/not-zip-safe +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/requires.txt +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/cloudinary_cli.egg-info/top_level.txt +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/requirements.txt +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/setup.cfg +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/setup.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/helper_test.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_api.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_config.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_samples.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_search_api.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_url.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_cli_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_file_utils.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/__init__.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_sync.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_modules/test_cli_upload_dir.py +0 -0
- {cloudinary_cli-1.12.0 → cloudinary_cli-1.14.0}/test/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudinary-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Summary: A command line interface for Cloudinary with full API support
|
|
5
5
|
Home-page: https://github.com/cloudinary/cloudinary-cli
|
|
6
6
|
Author: Cloudinary, Brian Luk
|
|
@@ -57,7 +57,7 @@ _**Note:** you can copy and paste your account environment variable from the Acc
|
|
|
57
57
|
api_secret: ***************<LAST_4_DIGITS>
|
|
58
58
|
private_cdn: <True|False>
|
|
59
59
|
```
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
If you get an error message when running `cld config`, you may need to add your Python installation to your $PATH. To do so, you can run `PATH="$PATH:/Library/Python/Versions/3.8/bin"` in your terminal, and add `export PATH="$PATH:/Library/Python/Versions/3.8/bin"` to your `/.bash_profile` or `~/.zshrc`.
|
|
62
62
|
|
|
63
63
|
## Quickstart
|
|
@@ -77,11 +77,38 @@ cld admin # Lists Admin API methods.
|
|
|
77
77
|
cld uploader # Lists Upload API methods.
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
## Docker Usage
|
|
81
|
+
|
|
82
|
+
The Cloudinary CLI is also available as a Docker image, perfect for containerized environments, CI/CD pipelines, or when you prefer not to install Python locally.
|
|
83
|
+
|
|
84
|
+
### Quick Docker Examples
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
# Check configuration
|
|
88
|
+
docker run --rm -e CLOUDINARY_URL cloudinary/cli config
|
|
89
|
+
|
|
90
|
+
# Upload files from your local machine
|
|
91
|
+
docker run --rm -e CLOUDINARY_URL \
|
|
92
|
+
-v /path/to/your/images:/app/images \
|
|
93
|
+
cloudinary/cli uploader upload /app/images/sample.jpg
|
|
94
|
+
|
|
95
|
+
# Search for assets
|
|
96
|
+
docker run --rm -e CLOUDINARY_URL \
|
|
97
|
+
cloudinary/cli search "resource_type:image" --max_results 5
|
|
98
|
+
|
|
99
|
+
# Interactive mode for multiple commands
|
|
100
|
+
docker run --rm -it -e CLOUDINARY_URL --entrypoint /bin/bash cloudinary/cli
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Docker Image**: `cloudinary/cli` • **Base**: Debian 12 • **Size**: ~175MB • **Multi-arch**: amd64, arm64
|
|
104
|
+
|
|
105
|
+
For comprehensive Docker usage, examples, and troubleshooting, see [DOCKER.md](DOCKER.md).
|
|
106
|
+
|
|
80
107
|
## Upload API
|
|
81
108
|
|
|
82
109
|
Enables you to run any methods that can be called through the upload API.
|
|
83
110
|
|
|
84
|
-
You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
|
|
111
|
+
You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
|
|
85
112
|
|
|
86
113
|
The basic syntax using the Upload API is as follows:
|
|
87
114
|
|
|
@@ -152,7 +179,7 @@ cld [cli options] url [command options] public_id [transformation]
|
|
|
152
179
|
|
|
153
180
|
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#url).
|
|
154
181
|
|
|
155
|
-
Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
|
|
182
|
+
Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
|
|
156
183
|
|
|
157
184
|
```
|
|
158
185
|
cld url -rt image -t upload -o sample w_500,e_cartoonify
|
|
@@ -187,9 +214,9 @@ Uploads a folder of assets, maintaining the folder structure.
|
|
|
187
214
|
cld [cli options] upload_dir [command options] [local_folder]
|
|
188
215
|
```
|
|
189
216
|
|
|
190
|
-
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
|
|
217
|
+
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
|
|
191
218
|
|
|
192
|
-
Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
|
|
219
|
+
Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
|
|
193
220
|
|
|
194
221
|
```
|
|
195
222
|
cld upload_dir -f my_images_on_cloudinary my_images
|
|
@@ -227,7 +254,7 @@ A configuration is a reference to a specified Cloudinary account or cloud name v
|
|
|
227
254
|
|
|
228
255
|
The `config` command displays the current configuration and lets you manage additional configurations.
|
|
229
256
|
|
|
230
|
-
You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
|
|
257
|
+
You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
|
|
231
258
|
|
|
232
259
|
For example, using the `-c` option:
|
|
233
260
|
|
|
@@ -32,7 +32,7 @@ _**Note:** you can copy and paste your account environment variable from the Acc
|
|
|
32
32
|
api_secret: ***************<LAST_4_DIGITS>
|
|
33
33
|
private_cdn: <True|False>
|
|
34
34
|
```
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
If you get an error message when running `cld config`, you may need to add your Python installation to your $PATH. To do so, you can run `PATH="$PATH:/Library/Python/Versions/3.8/bin"` in your terminal, and add `export PATH="$PATH:/Library/Python/Versions/3.8/bin"` to your `/.bash_profile` or `~/.zshrc`.
|
|
37
37
|
|
|
38
38
|
## Quickstart
|
|
@@ -52,11 +52,38 @@ cld admin # Lists Admin API methods.
|
|
|
52
52
|
cld uploader # Lists Upload API methods.
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Docker Usage
|
|
56
|
+
|
|
57
|
+
The Cloudinary CLI is also available as a Docker image, perfect for containerized environments, CI/CD pipelines, or when you prefer not to install Python locally.
|
|
58
|
+
|
|
59
|
+
### Quick Docker Examples
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Check configuration
|
|
63
|
+
docker run --rm -e CLOUDINARY_URL cloudinary/cli config
|
|
64
|
+
|
|
65
|
+
# Upload files from your local machine
|
|
66
|
+
docker run --rm -e CLOUDINARY_URL \
|
|
67
|
+
-v /path/to/your/images:/app/images \
|
|
68
|
+
cloudinary/cli uploader upload /app/images/sample.jpg
|
|
69
|
+
|
|
70
|
+
# Search for assets
|
|
71
|
+
docker run --rm -e CLOUDINARY_URL \
|
|
72
|
+
cloudinary/cli search "resource_type:image" --max_results 5
|
|
73
|
+
|
|
74
|
+
# Interactive mode for multiple commands
|
|
75
|
+
docker run --rm -it -e CLOUDINARY_URL --entrypoint /bin/bash cloudinary/cli
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Docker Image**: `cloudinary/cli` • **Base**: Debian 12 • **Size**: ~175MB • **Multi-arch**: amd64, arm64
|
|
79
|
+
|
|
80
|
+
For comprehensive Docker usage, examples, and troubleshooting, see [DOCKER.md](DOCKER.md).
|
|
81
|
+
|
|
55
82
|
## Upload API
|
|
56
83
|
|
|
57
84
|
Enables you to run any methods that can be called through the upload API.
|
|
58
85
|
|
|
59
|
-
You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
|
|
86
|
+
You can find documentation for each of the Upload API methods at [https://cloudinary.com/documentation/image_upload_api_reference](https://cloudinary.com/documentation/image_upload_api_reference).
|
|
60
87
|
|
|
61
88
|
The basic syntax using the Upload API is as follows:
|
|
62
89
|
|
|
@@ -127,7 +154,7 @@ cld [cli options] url [command options] public_id [transformation]
|
|
|
127
154
|
|
|
128
155
|
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#url).
|
|
129
156
|
|
|
130
|
-
Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
|
|
157
|
+
Example: generate a URL that displays the image in your media library that has the public ID of 'sample', with a width of 500 pixels and transformed using the cartoonify effect, then open this URL in a browser.
|
|
131
158
|
|
|
132
159
|
```
|
|
133
160
|
cld url -rt image -t upload -o sample w_500,e_cartoonify
|
|
@@ -162,9 +189,9 @@ Uploads a folder of assets, maintaining the folder structure.
|
|
|
162
189
|
cld [cli options] upload_dir [command options] [local_folder]
|
|
163
190
|
```
|
|
164
191
|
|
|
165
|
-
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
|
|
192
|
+
For details, see the [Cloudinary CLI documentation](https://cloudinary.com/documentation/cloudinary_cli#upload_dir).
|
|
166
193
|
|
|
167
|
-
Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
|
|
194
|
+
Example: upload the local folder, my_images, and all its contents and sub-folders to your Cloudinary folder my_images_on_cloudinary.
|
|
168
195
|
|
|
169
196
|
```
|
|
170
197
|
cld upload_dir -f my_images_on_cloudinary my_images
|
|
@@ -202,7 +229,7 @@ A configuration is a reference to a specified Cloudinary account or cloud name v
|
|
|
202
229
|
|
|
203
230
|
The `config` command displays the current configuration and lets you manage additional configurations.
|
|
204
231
|
|
|
205
|
-
You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
|
|
232
|
+
You can specify the environment variable of additional Cloudinary accounts either explicitly (`-c` option) or as a saved configuration (`-C` option).
|
|
206
233
|
|
|
207
234
|
For example, using the `-c` option:
|
|
208
235
|
|
|
@@ -14,7 +14,7 @@ from cloudinary_cli.version import __version__ as cli_version
|
|
|
14
14
|
CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], terminal_width=shutil.get_terminal_size()[0])
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
17
|
+
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
|
|
18
18
|
@click.help_option()
|
|
19
19
|
@click.version_option(cli_version, prog_name="Cloudinary CLI",
|
|
20
20
|
message=f"%(prog)s, version %(version)s\n"
|
|
@@ -24,10 +24,11 @@ CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], termina
|
|
|
24
24
|
help="""Tell the CLI which account to run the command on by specifying an account environment variable."""
|
|
25
25
|
)
|
|
26
26
|
@click.option("-C", "--config_saved",
|
|
27
|
-
help="""Tell the CLI which account to run the command on by specifying a saved configuration - see
|
|
27
|
+
help="""Tell the CLI which account to run the command on by specifying a saved configuration - see
|
|
28
28
|
`config` command.""")
|
|
29
29
|
@click_log.simple_verbosity_option(logger)
|
|
30
|
-
|
|
30
|
+
@click.pass_context
|
|
31
|
+
def cli(ctx, config, config_saved):
|
|
31
32
|
if config:
|
|
32
33
|
refresh_cloudinary_config(config)
|
|
33
34
|
elif config_saved:
|
|
@@ -40,4 +41,9 @@ def cli(config, config_saved):
|
|
|
40
41
|
if not is_valid_cloudinary_config():
|
|
41
42
|
logger.warning("No Cloudinary configuration found.")
|
|
42
43
|
|
|
44
|
+
# If no subcommand was invoked, show help and exit with code 0
|
|
45
|
+
if ctx.invoked_subcommand is None:
|
|
46
|
+
click.echo(ctx.get_help())
|
|
47
|
+
ctx.exit(0)
|
|
48
|
+
|
|
43
49
|
return True
|
|
@@ -8,7 +8,7 @@ from cloudinary_cli.core.provisioning import provisioning
|
|
|
8
8
|
from cloudinary_cli.core.utils import url, utils
|
|
9
9
|
from cloudinary_cli.core.overrides import resolve_command
|
|
10
10
|
|
|
11
|
-
setattr(click.
|
|
11
|
+
setattr(click.Group, "resolve_command", resolve_command)
|
|
12
12
|
|
|
13
13
|
commands = [
|
|
14
14
|
config,
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
from click.parser import split_opt
|
|
2
1
|
from click.utils import make_str
|
|
3
2
|
from cloudinary import api, uploader
|
|
4
3
|
from cloudinary.uploader import upload as original_upload
|
|
5
4
|
from cloudinary.utils import cloudinary_url as original_cloudinary_url
|
|
5
|
+
from cloudinary_cli.utils.utils import split_opt
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
# overrides click.MultiCommand.resolve_command
|
|
9
9
|
def resolve_command(self, ctx, args):
|
|
10
10
|
# Patch the `resolve_command` function to enable simple commands (eg. cld resource)
|
|
11
11
|
# Only core commands from API and modules are registered (eg. cld admin)
|
|
12
|
+
|
|
13
|
+
# Handle empty args (when CLI is invoked with no arguments)
|
|
14
|
+
if not args:
|
|
15
|
+
return None, None, []
|
|
16
|
+
|
|
12
17
|
cmd_name = make_str(args[0])
|
|
13
18
|
original_cmd_name = cmd_name
|
|
14
19
|
|
|
@@ -6,6 +6,7 @@ from cloudinary_cli.defaults import logger
|
|
|
6
6
|
from cloudinary_cli.utils.json_utils import write_json_to_file, print_json
|
|
7
7
|
from cloudinary_cli.utils.utils import write_json_list_to_csv, confirm_action, whitelist_keys, \
|
|
8
8
|
normalize_list_params
|
|
9
|
+
from cloudinary_cli.utils.search_utils import parse_aggregate
|
|
9
10
|
|
|
10
11
|
DEFAULT_MAX_RESULTS = 500
|
|
11
12
|
|
|
@@ -100,7 +101,7 @@ def _perform_search(query, with_field, fields, sort_by, aggregate, max_results,
|
|
|
100
101
|
if sort_by:
|
|
101
102
|
search.sort_by(*sort_by)
|
|
102
103
|
if aggregate:
|
|
103
|
-
search.aggregate(aggregate)
|
|
104
|
+
search.aggregate(parse_aggregate(aggregate))
|
|
104
105
|
if next_cursor:
|
|
105
106
|
search.next_cursor(next_cursor)
|
|
106
107
|
if ttl:
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from click import command, option, style, argument
|
|
2
|
+
from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit
|
|
3
|
+
import cloudinary
|
|
4
|
+
from cloudinary.auth_token import _digest
|
|
5
|
+
from cloudinary_cli.utils.utils import run_tasks_concurrently
|
|
6
|
+
from cloudinary_cli.utils.api_utils import upload_file
|
|
7
|
+
from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict
|
|
8
|
+
from cloudinary_cli.defaults import logger
|
|
9
|
+
from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination
|
|
10
|
+
import time
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAX_RESULTS = 500
|
|
14
|
+
ALLOWED_TYPE_VALUES = ("upload", "private", "authenticated")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@command("clone",
|
|
18
|
+
short_help="""Clone assets from one product environment to another.""",
|
|
19
|
+
help="""
|
|
20
|
+
\b
|
|
21
|
+
Clone assets from one product environment to another with/without tags and/or context (structured metadata is not currently supported).
|
|
22
|
+
Source will be your `CLOUDINARY_URL` environment variable but you also can specify a different source using the `-c/-C` option.
|
|
23
|
+
Format: cld clone <target_environment> <command options>
|
|
24
|
+
`<target_environment>` can be a CLOUDINARY_URL or a saved config (see `config` command)
|
|
25
|
+
Example 1 (Copy all assets including tags and context using CLOUDINARY URL):
|
|
26
|
+
cld clone cloudinary://<api_key>:<api_secret>@<cloudname> -fi tags,context
|
|
27
|
+
Example 2 (Copy all assets with a specific tag via a search expression using a saved config):
|
|
28
|
+
cld clone <config_name> -se "tags:<tag_name>"
|
|
29
|
+
""")
|
|
30
|
+
@argument("target")
|
|
31
|
+
@option("-F", "--force", is_flag=True,
|
|
32
|
+
help="Skip confirmation.")
|
|
33
|
+
@option("-ow", "--overwrite", is_flag=True, default=False,
|
|
34
|
+
help="Specify whether to overwrite existing assets.")
|
|
35
|
+
@option("-w", "--concurrent_workers", type=int, default=30,
|
|
36
|
+
help="Specify the number of concurrent network threads.")
|
|
37
|
+
@option("-fi", "--fields", multiple=True,
|
|
38
|
+
help=("Specify whether to copy tags and/or context. "
|
|
39
|
+
"Valid options: `tags,context`."))
|
|
40
|
+
@option("-se", "--search_exp", default="",
|
|
41
|
+
help="Define a search expression to filter the assets to clone.")
|
|
42
|
+
@option("--async", "async_", is_flag=True, default=False,
|
|
43
|
+
help="Clone the assets asynchronously.")
|
|
44
|
+
@option("-nu", "--notification_url",
|
|
45
|
+
help="Webhook notification URL.")
|
|
46
|
+
@option("-ue", "--url_expiry", type=int, default=3600,
|
|
47
|
+
help=("URL expiration duration in seconds. Only relevant if cloning "
|
|
48
|
+
"restricted assets with an auth_key configured. "
|
|
49
|
+
"If you do not provide an auth_key, "
|
|
50
|
+
"a private download URL is generated which may incur additional "
|
|
51
|
+
"bandwidth costs."))
|
|
52
|
+
def clone(target, force, overwrite, concurrent_workers, fields,
|
|
53
|
+
search_exp, async_, notification_url, url_expiry):
|
|
54
|
+
target_config, auth_token = _validate_clone_inputs(target)
|
|
55
|
+
if not target_config:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
source_assets = search_assets(search_exp, force)
|
|
59
|
+
if not source_assets:
|
|
60
|
+
return False
|
|
61
|
+
if not isinstance(source_assets, dict) or not source_assets.get('resources'):
|
|
62
|
+
logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red"))
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
upload_list = _prepare_upload_list(
|
|
66
|
+
source_assets, target_config, overwrite, async_,
|
|
67
|
+
notification_url, auth_token, url_expiry, fields
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
logger.info(style(f"Copying {len(upload_list)} asset(s) from "
|
|
71
|
+
f"{cloudinary.config().cloud_name} to "
|
|
72
|
+
f"{target_config.cloud_name}", fg="blue"))
|
|
73
|
+
|
|
74
|
+
run_tasks_concurrently(upload_file, upload_list, concurrent_workers)
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_clone_inputs(target):
|
|
80
|
+
if not target:
|
|
81
|
+
print_help_and_exit()
|
|
82
|
+
|
|
83
|
+
target_config = get_cloudinary_config(target)
|
|
84
|
+
if not target_config:
|
|
85
|
+
logger.error("The specified config does not exist or the "
|
|
86
|
+
"CLOUDINARY_URL scheme provided is invalid "
|
|
87
|
+
"(expecting to start with 'cloudinary://').")
|
|
88
|
+
return None, None
|
|
89
|
+
|
|
90
|
+
if cloudinary.config().cloud_name == target_config.cloud_name:
|
|
91
|
+
logger.error("Target environment cannot be the same "
|
|
92
|
+
"as source environment.")
|
|
93
|
+
return None, None
|
|
94
|
+
|
|
95
|
+
auth_token = cloudinary.config().auth_token
|
|
96
|
+
if auth_token:
|
|
97
|
+
# It is important to validate auth_token if provided as this prevents
|
|
98
|
+
# customer from having to re-run the command as well as
|
|
99
|
+
# saving Admin API calls and time.
|
|
100
|
+
try:
|
|
101
|
+
cloudinary.utils.generate_auth_token(acl="/image/*")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"{e} - auth_token validation failed. "
|
|
104
|
+
"Please double-check your auth_token parameters.")
|
|
105
|
+
return None, None
|
|
106
|
+
|
|
107
|
+
return target_config, auth_token
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _prepare_upload_list(source_assets, target_config, overwrite, async_,
|
|
111
|
+
notification_url, auth_token, url_expiry, fields):
|
|
112
|
+
upload_list = []
|
|
113
|
+
for r in source_assets.get('resources'):
|
|
114
|
+
updated_options, asset_url = process_metadata(r, overwrite, async_,
|
|
115
|
+
notification_url,
|
|
116
|
+
auth_token, url_expiry,
|
|
117
|
+
normalize_list_params(fields))
|
|
118
|
+
updated_options.update(config_to_dict(target_config))
|
|
119
|
+
upload_list.append((asset_url, {**updated_options}))
|
|
120
|
+
return upload_list
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def search_assets(search_exp, force):
|
|
124
|
+
search_exp = _normalize_search_expression(search_exp)
|
|
125
|
+
if not search_exp:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
search = cloudinary.search.Search().expression(search_exp)
|
|
129
|
+
search.fields(['tags', 'context', 'access_control',
|
|
130
|
+
'secure_url', 'display_name', 'format'])
|
|
131
|
+
search.max_results(DEFAULT_MAX_RESULTS)
|
|
132
|
+
|
|
133
|
+
res = execute_single_request(search, fields_to_keep="")
|
|
134
|
+
res = handle_auto_pagination(res, search, force, fields_to_keep="")
|
|
135
|
+
|
|
136
|
+
return res
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _normalize_search_expression(search_exp):
|
|
140
|
+
"""
|
|
141
|
+
Ensures the search expression has a valid 'type' filter.
|
|
142
|
+
|
|
143
|
+
- If no expression is given, a default is created.
|
|
144
|
+
- If 'type' filters exist, they are validated.
|
|
145
|
+
- If no 'type' filters exist, the default is appended.
|
|
146
|
+
"""
|
|
147
|
+
default_types_str = " OR ".join(f"type:{t}" for t in ALLOWED_TYPE_VALUES)
|
|
148
|
+
|
|
149
|
+
if not search_exp:
|
|
150
|
+
return default_types_str
|
|
151
|
+
|
|
152
|
+
# Use a simple regex to find all 'type' filters
|
|
153
|
+
found_types = re.findall(r"\btype\s*[:=]\s*(\w+)", search_exp)
|
|
154
|
+
|
|
155
|
+
if not found_types:
|
|
156
|
+
# No 'type' filter found, so append the default
|
|
157
|
+
return f"{search_exp} AND ({default_types_str})"
|
|
158
|
+
|
|
159
|
+
# A 'type' filter was found, so validate it
|
|
160
|
+
invalid_types = {t for t in found_types if t not in ALLOWED_TYPE_VALUES}
|
|
161
|
+
|
|
162
|
+
if invalid_types:
|
|
163
|
+
error_msg = ", ".join(f"type:{t}" for t in invalid_types)
|
|
164
|
+
logger.error(
|
|
165
|
+
f"Unsupported type(s) in search expression: {error_msg}. "
|
|
166
|
+
f"Only {', '.join(ALLOWED_TYPE_VALUES)} types allowed."
|
|
167
|
+
)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
# All found types are valid, so return the original expression
|
|
171
|
+
return search_exp
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def process_metadata(res, overwrite, async_, notification_url, auth_token, url_expiry, copy_fields=None):
|
|
175
|
+
if copy_fields is None:
|
|
176
|
+
copy_fields = []
|
|
177
|
+
asset_url = _get_asset_url(res, auth_token, url_expiry)
|
|
178
|
+
cloned_options = _build_cloned_options(res, overwrite, async_, notification_url, copy_fields)
|
|
179
|
+
|
|
180
|
+
return cloned_options, asset_url
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _get_asset_url(res, auth_token, url_expiry):
|
|
184
|
+
if not (isinstance(res.get('access_control'), list) and
|
|
185
|
+
len(res.get('access_control')) > 0 and
|
|
186
|
+
isinstance(res['access_control'][0], dict) and
|
|
187
|
+
res['access_control'][0].get("access_type") == "token"):
|
|
188
|
+
return res.get('secure_url')
|
|
189
|
+
|
|
190
|
+
reso_type = res.get('resource_type')
|
|
191
|
+
del_type = res.get('type')
|
|
192
|
+
pub_id = res.get('public_id')
|
|
193
|
+
file_format = res.get('format')
|
|
194
|
+
|
|
195
|
+
if auth_token:
|
|
196
|
+
# Raw assets already have the format in the public_id
|
|
197
|
+
pub_id_format = pub_id if reso_type == "raw" else f"{pub_id}.{file_format}"
|
|
198
|
+
return cloudinary.utils.cloudinary_url(
|
|
199
|
+
pub_id_format,
|
|
200
|
+
type=del_type,
|
|
201
|
+
resource_type=reso_type,
|
|
202
|
+
auth_token={"duration": url_expiry},
|
|
203
|
+
secure=True,
|
|
204
|
+
sign_url=True
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Use private url if no auth_token provided
|
|
208
|
+
return cloudinary.utils.private_download_url(
|
|
209
|
+
pub_id,
|
|
210
|
+
file_format,
|
|
211
|
+
resource_type=reso_type,
|
|
212
|
+
type=del_type,
|
|
213
|
+
expires_at=int(time.time()) + url_expiry
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_cloned_options(res, overwrite, async_, notification_url, copy_fields):
|
|
218
|
+
# 1. Start with mandatory options
|
|
219
|
+
cloned_options = {
|
|
220
|
+
'overwrite': overwrite,
|
|
221
|
+
'async': async_,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# 2. Copy fields from source asset. Some are standard, others are from user input.
|
|
225
|
+
fields_to_copy = {'public_id', 'type', 'resource_type', 'access_control'}.union(copy_fields)
|
|
226
|
+
cloned_options.update({field: res.get(field) for field in fields_to_copy})
|
|
227
|
+
|
|
228
|
+
# 3. Handle fields that are added only if they have a truthy value
|
|
229
|
+
if res.get('display_name'):
|
|
230
|
+
cloned_options['display_name'] = res['display_name']
|
|
231
|
+
|
|
232
|
+
# This is required to put the asset in the correct asset_folder
|
|
233
|
+
# when copying from a fixed to DF (dynamic folder) cloud as if
|
|
234
|
+
# you just pass a `folder` param to a DF cloud, it will append
|
|
235
|
+
# this to the `public_id` and we don't want this.
|
|
236
|
+
if res.get('folder'):
|
|
237
|
+
cloned_options['asset_folder'] = res['folder']
|
|
238
|
+
elif res.get('asset_folder'):
|
|
239
|
+
cloned_options['asset_folder'] = res['asset_folder']
|
|
240
|
+
|
|
241
|
+
if notification_url:
|
|
242
|
+
cloned_options['notification_url'] = notification_url
|
|
243
|
+
|
|
244
|
+
# 4. Clean up any None values before returning
|
|
245
|
+
return {k: v for k, v in cloned_options.items() if v is not None}
|
|
@@ -300,7 +300,7 @@ class SyncDir:
|
|
|
300
300
|
|
|
301
301
|
def _local_candidates(self, candidate_path):
|
|
302
302
|
filename, extension = path.splitext(candidate_path)
|
|
303
|
-
r = re.compile(f"({candidate_path}|{filename}
|
|
303
|
+
r = re.compile(f"({candidate_path}|{filename} \\(\\d+\\){extension})")
|
|
304
304
|
# sort local files by base name (without ext) for accurate results.
|
|
305
305
|
return dict(sorted({f: self.local_files[f]["etag"] for f in filter(r.match, self.local_files.keys())}.items(),
|
|
306
306
|
key=lambda f: path.splitext(f[0])[0]))
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
|
|
3
3
|
<head>
|
|
4
|
-
<link
|
|
4
|
+
<link rel="stylesheet"
|
|
5
|
+
href="https://cdn.jsdelivr.net/npm/cloudinary-video-player@latest/dist/cld-video-player.min.css">
|
|
5
6
|
</head>
|
|
6
7
|
|
|
7
8
|
<body>
|
|
8
|
-
|
|
9
|
-
<script
|
|
10
|
-
|
|
11
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/dashjs/2.6.3/dash.all.min.js" type="text/javascript"></script>
|
|
12
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-dash/2.9.2/videojs-dash.min.js" type="text/javascript"></script>
|
|
9
|
+
|
|
10
|
+
<script
|
|
11
|
+
src="https://cdn.jsdelivr.net/npm/cloudinary-video-player@latest/dist/cld-video-player.light.min.js"></script>
|
|
13
12
|
|
|
14
13
|
<video
|
|
15
14
|
style="min-width:100vw;min-height:100vh;max-width:100vw;max-height:100vh"
|