cloudsmith-cli 1.14.0__tar.gz → 1.15.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.
- {cloudsmith_cli-1.14.0/cloudsmith_cli.egg-info → cloudsmith_cli-1.15.0}/PKG-INFO +11 -1
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/README.md +10 -0
- cloudsmith_cli-1.15.0/cloudsmith_cli/cli/commands/download.py +620 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_download.py +169 -2
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/download.py +149 -37
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_download.py +195 -3
- cloudsmith_cli-1.15.0/cloudsmith_cli/data/VERSION +1 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0/cloudsmith_cli.egg-info}/PKG-INFO +11 -1
- cloudsmith_cli-1.14.0/cloudsmith_cli/cli/commands/download.py +0 -459
- cloudsmith_cli-1.14.0/cloudsmith_cli/data/VERSION +0 -1
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/LICENSE +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/MANIFEST.in +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/__main__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/command.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/auth.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/check.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/login.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/logout.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/main.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/move.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/push.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/status.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/tokens.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/commands/whoami.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/config.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/decorators.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/exceptions.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/saml.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/table.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/conftest.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_deny.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_licence.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_auth.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_check.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_entitlements.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_login.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_logout.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_main.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_mcp.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_package_commands.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_repos.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_tokens.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_upstream.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/commands/test_vulnerabilities.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/conftest.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_push.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_saml.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_utils.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/test_webserver.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/tests/utils.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/types.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/utils.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/validators.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/cli/webserver.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/distros.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/files.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/init.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/metrics.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/orgs.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/packages.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/quota.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/rates.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/repos.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/status.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/user.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/version.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/api/vulnerabilities.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/config.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/keyring.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/data.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/mcp/server.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/pagination.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/ratelimits.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/rest.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_init.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_keyring.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_rest.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/tests/test_version.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/utils.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/core/version.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/data/config.ini +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/data/credentials.ini +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/__init__.py +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/auth_error.html +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli/templates/auth_success.html +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/SOURCES.txt +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/not-zip-safe +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/requires.txt +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/setup.cfg +0 -0
- {cloudsmith_cli-1.14.0 → cloudsmith_cli-1.15.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsmith-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: Cloudsmith Command-Line Interface (CLI)
|
|
5
5
|
Home-page: https://github.com/cloudsmith-io/cloudsmith-cli
|
|
6
6
|
Author: Cloudsmith Ltd
|
|
@@ -333,6 +333,16 @@ cloudsmith download your-account/your-repo package-name --tag latest
|
|
|
333
333
|
# Combine tag with metadata filters
|
|
334
334
|
cloudsmith download your-account/your-repo package-name --tag stable --format deb --arch arm64
|
|
335
335
|
|
|
336
|
+
# Filter by filename (exact or glob pattern)
|
|
337
|
+
cloudsmith download your-account/your-repo package-name --filename '*.nupkg'
|
|
338
|
+
cloudsmith download your-account/your-repo package-name --filename 'mypackage-1.0.0.snupkg'
|
|
339
|
+
|
|
340
|
+
# Download all matching packages (when multiple packages share the same name/version)
|
|
341
|
+
cloudsmith download your-account/your-repo package-name --download-all
|
|
342
|
+
|
|
343
|
+
# Combine --download-all with --filename to download a subset
|
|
344
|
+
cloudsmith download your-account/your-repo package-name --download-all --filename '*.snupkg'
|
|
345
|
+
|
|
336
346
|
# Download all associated files (POM, sources, javadoc, etc.)
|
|
337
347
|
cloudsmith download your-account/your-repo package-name --all-files
|
|
338
348
|
|
|
@@ -277,6 +277,16 @@ cloudsmith download your-account/your-repo package-name --tag latest
|
|
|
277
277
|
# Combine tag with metadata filters
|
|
278
278
|
cloudsmith download your-account/your-repo package-name --tag stable --format deb --arch arm64
|
|
279
279
|
|
|
280
|
+
# Filter by filename (exact or glob pattern)
|
|
281
|
+
cloudsmith download your-account/your-repo package-name --filename '*.nupkg'
|
|
282
|
+
cloudsmith download your-account/your-repo package-name --filename 'mypackage-1.0.0.snupkg'
|
|
283
|
+
|
|
284
|
+
# Download all matching packages (when multiple packages share the same name/version)
|
|
285
|
+
cloudsmith download your-account/your-repo package-name --download-all
|
|
286
|
+
|
|
287
|
+
# Combine --download-all with --filename to download a subset
|
|
288
|
+
cloudsmith download your-account/your-repo package-name --download-all --filename '*.snupkg'
|
|
289
|
+
|
|
280
290
|
# Download all associated files (POM, sources, javadoc, etc.)
|
|
281
291
|
cloudsmith download your-account/your-repo package-name --all-files
|
|
282
292
|
|
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"""CLI/Commands - Download packages."""
|
|
2
|
+
|
|
3
|
+
# Copyright 2025 Cloudsmith Ltd
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ...core.download import (
|
|
10
|
+
get_download_url,
|
|
11
|
+
get_package_detail,
|
|
12
|
+
get_package_files,
|
|
13
|
+
resolve_all_packages,
|
|
14
|
+
resolve_auth,
|
|
15
|
+
resolve_package,
|
|
16
|
+
stream_download,
|
|
17
|
+
)
|
|
18
|
+
from .. import decorators, utils, validators
|
|
19
|
+
from ..exceptions import handle_api_exceptions
|
|
20
|
+
from ..utils import maybe_spinner
|
|
21
|
+
from .main import main
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@main.command()
|
|
25
|
+
@decorators.common_cli_config_options
|
|
26
|
+
@decorators.common_cli_output_options
|
|
27
|
+
@decorators.common_api_auth_options
|
|
28
|
+
@decorators.initialise_api
|
|
29
|
+
@click.argument(
|
|
30
|
+
"owner_repo", metavar="OWNER/REPO", callback=validators.validate_owner_repo
|
|
31
|
+
)
|
|
32
|
+
@click.argument("name", required=True)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--version",
|
|
35
|
+
help="Package version to download (e.g., '1.0.0'). If not specified, searches all versions.",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--format",
|
|
39
|
+
"format_filter",
|
|
40
|
+
help="Package format filter (e.g., 'deb', 'rpm', 'python', 'npm').",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--os", "os_filter", help="Operating system filter (e.g., 'ubuntu', 'centos')."
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--arch", "arch_filter", help="Architecture filter (e.g., 'amd64', 'arm64')."
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--tag",
|
|
50
|
+
"tag_filter",
|
|
51
|
+
help="Filter by package tag (e.g., 'latest', 'stable'). Use --format, --arch, --os for metadata filters.",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--filename",
|
|
55
|
+
"filename_filter",
|
|
56
|
+
help="Filter by package filename (e.g., 'mypackage.nupkg'). Supports glob patterns (e.g., '*.snupkg').",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--download-all",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
help="Download all matching packages instead of erroring on multiple matches.",
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--outfile",
|
|
65
|
+
type=click.Path(),
|
|
66
|
+
help="Output file path. If not specified, uses the package filename.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--overwrite/--no-overwrite",
|
|
70
|
+
default=False,
|
|
71
|
+
help="Overwrite existing files (default: fail if file exists).",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--all-files",
|
|
75
|
+
is_flag=True,
|
|
76
|
+
help="Download all associated files (POM, sources, javadoc, etc.) into a folder.",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--dry-run",
|
|
80
|
+
is_flag=True,
|
|
81
|
+
help="Show what would be downloaded without actually downloading.",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"-y",
|
|
85
|
+
"--yes",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
help="Automatically select the best match when multiple packages are found.",
|
|
88
|
+
)
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def download(
|
|
91
|
+
ctx,
|
|
92
|
+
opts,
|
|
93
|
+
owner_repo,
|
|
94
|
+
name,
|
|
95
|
+
version,
|
|
96
|
+
format_filter,
|
|
97
|
+
os_filter,
|
|
98
|
+
arch_filter,
|
|
99
|
+
tag_filter,
|
|
100
|
+
filename_filter,
|
|
101
|
+
download_all,
|
|
102
|
+
outfile,
|
|
103
|
+
overwrite,
|
|
104
|
+
all_files,
|
|
105
|
+
dry_run,
|
|
106
|
+
yes,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Download a package from a Cloudsmith repository.
|
|
110
|
+
|
|
111
|
+
This command downloads a package binary from a Cloudsmith repository. You can
|
|
112
|
+
filter packages by version, format, operating system, architecture, tags, and
|
|
113
|
+
filename.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
|
|
117
|
+
\b
|
|
118
|
+
# Download the latest version of 'mypackage'
|
|
119
|
+
cloudsmith download myorg/myrepo mypackage
|
|
120
|
+
|
|
121
|
+
\b
|
|
122
|
+
# Download a specific version
|
|
123
|
+
cloudsmith download myorg/myrepo mypackage --version 1.2.3
|
|
124
|
+
|
|
125
|
+
\b
|
|
126
|
+
# Download with filters and custom output name
|
|
127
|
+
cloudsmith download myorg/myrepo mypackage --format deb --arch amd64 --outfile my-package.deb
|
|
128
|
+
|
|
129
|
+
\b
|
|
130
|
+
# Download a package with a specific tag
|
|
131
|
+
cloudsmith download myorg/myrepo mypackage --tag latest
|
|
132
|
+
|
|
133
|
+
\b
|
|
134
|
+
# Download by filename (exact or glob pattern)
|
|
135
|
+
cloudsmith download myorg/myrepo TestSymbolPkg --filename '*.nupkg'
|
|
136
|
+
cloudsmith download myorg/myrepo TestSymbolPkg --filename 'TestSymbolPkg.1.0.24406.nupkg'
|
|
137
|
+
|
|
138
|
+
\b
|
|
139
|
+
# Download all matching packages (e.g., .nupkg and .snupkg with same name/version)
|
|
140
|
+
cloudsmith download myorg/myrepo TestSymbolPkg --version 1.0.24406 --download-all
|
|
141
|
+
|
|
142
|
+
\b
|
|
143
|
+
# Download all associated files (POM, sources, javadoc, etc.) for a Maven/NuGet package
|
|
144
|
+
cloudsmith download myorg/myrepo mypackage --all-files
|
|
145
|
+
|
|
146
|
+
\b
|
|
147
|
+
# Download all files to a custom directory
|
|
148
|
+
cloudsmith download myorg/myrepo mypackage --all-files --outfile ./my-package-dir
|
|
149
|
+
|
|
150
|
+
For private repositories, set: export CLOUDSMITH_API_KEY=your_api_key
|
|
151
|
+
|
|
152
|
+
If multiple packages match your criteria, you'll see a selection table unless
|
|
153
|
+
you use --yes to automatically select the best match (highest version, then newest),
|
|
154
|
+
or --download-all to download all matches.
|
|
155
|
+
|
|
156
|
+
When using --all-files, all associated files (such as POM files, sources, javadoc,
|
|
157
|
+
SBOM, etc.) will be downloaded into a folder named {package-name}-{version} unless
|
|
158
|
+
you specify a custom directory with --outfile.
|
|
159
|
+
"""
|
|
160
|
+
owner, repo = owner_repo
|
|
161
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
162
|
+
|
|
163
|
+
if not use_stderr:
|
|
164
|
+
click.echo(
|
|
165
|
+
f"Looking for package '{click.style(name, bold=True)}' in "
|
|
166
|
+
f"{click.style(owner, bold=True)}/{click.style(repo, bold=True)} ...",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Step 1: Authenticate
|
|
170
|
+
session, auth_headers, auth_source = resolve_auth(opts)
|
|
171
|
+
if opts.debug:
|
|
172
|
+
click.echo(f"Using authentication: {auth_source}", err=True)
|
|
173
|
+
|
|
174
|
+
# Step 2: Find package(s)
|
|
175
|
+
filter_kwargs = dict(
|
|
176
|
+
owner=owner,
|
|
177
|
+
repo=repo,
|
|
178
|
+
name=name,
|
|
179
|
+
version=version,
|
|
180
|
+
format_filter=format_filter,
|
|
181
|
+
os_filter=os_filter,
|
|
182
|
+
arch_filter=arch_filter,
|
|
183
|
+
tag_filter=tag_filter,
|
|
184
|
+
filename_filter=filename_filter,
|
|
185
|
+
)
|
|
186
|
+
packages = _find_packages(ctx, opts, filter_kwargs, download_all, yes, use_stderr)
|
|
187
|
+
|
|
188
|
+
# Step 3: Resolve download items (url + output path for each file)
|
|
189
|
+
download_items = _resolve_download_items(
|
|
190
|
+
ctx, opts, packages, owner, repo, all_files, outfile, use_stderr
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Step 4: Dry-run or download
|
|
194
|
+
if dry_run:
|
|
195
|
+
_display_dry_run(packages, download_items, all_files)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
_perform_downloads(
|
|
199
|
+
ctx,
|
|
200
|
+
opts,
|
|
201
|
+
packages,
|
|
202
|
+
download_items,
|
|
203
|
+
session,
|
|
204
|
+
auth_headers,
|
|
205
|
+
overwrite,
|
|
206
|
+
all_files,
|
|
207
|
+
use_stderr,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Step 2: Find packages
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _find_packages(
|
|
217
|
+
ctx: click.Context,
|
|
218
|
+
opts,
|
|
219
|
+
filter_kwargs: dict,
|
|
220
|
+
download_all: bool,
|
|
221
|
+
yes: bool,
|
|
222
|
+
use_stderr: bool,
|
|
223
|
+
) -> list:
|
|
224
|
+
"""Find matching packages using the API."""
|
|
225
|
+
if download_all:
|
|
226
|
+
context_msg = "Failed to find packages!"
|
|
227
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
228
|
+
with maybe_spinner(opts):
|
|
229
|
+
packages = resolve_all_packages(**filter_kwargs)
|
|
230
|
+
else:
|
|
231
|
+
context_msg = "Failed to find package!"
|
|
232
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
233
|
+
with maybe_spinner(opts):
|
|
234
|
+
packages = [resolve_package(**filter_kwargs, yes=yes)]
|
|
235
|
+
|
|
236
|
+
if not use_stderr:
|
|
237
|
+
click.secho("OK", fg="green")
|
|
238
|
+
|
|
239
|
+
return packages
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Step 3: Resolve download items
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _resolve_download_items(
|
|
248
|
+
ctx: click.Context,
|
|
249
|
+
opts,
|
|
250
|
+
packages: list,
|
|
251
|
+
owner: str,
|
|
252
|
+
repo: str,
|
|
253
|
+
all_files: bool,
|
|
254
|
+
outfile: str,
|
|
255
|
+
use_stderr: bool,
|
|
256
|
+
) -> list:
|
|
257
|
+
"""
|
|
258
|
+
Resolve each package into a list of download items.
|
|
259
|
+
|
|
260
|
+
Returns a list of dicts, each with keys:
|
|
261
|
+
filename, url, output_path, tag, is_primary, size, package_name,
|
|
262
|
+
package_version, status
|
|
263
|
+
"""
|
|
264
|
+
items = []
|
|
265
|
+
|
|
266
|
+
for pkg in packages:
|
|
267
|
+
if all_files:
|
|
268
|
+
items.extend(
|
|
269
|
+
_resolve_all_files_items(
|
|
270
|
+
ctx, opts, pkg, owner, repo, outfile, use_stderr
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
items.append(
|
|
275
|
+
_resolve_single_file_item(
|
|
276
|
+
ctx,
|
|
277
|
+
opts,
|
|
278
|
+
pkg,
|
|
279
|
+
owner,
|
|
280
|
+
repo,
|
|
281
|
+
outfile,
|
|
282
|
+
len(packages) > 1,
|
|
283
|
+
use_stderr,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return items
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _resolve_all_files_items(
|
|
291
|
+
ctx: click.Context,
|
|
292
|
+
opts,
|
|
293
|
+
pkg: dict,
|
|
294
|
+
owner: str,
|
|
295
|
+
repo: str,
|
|
296
|
+
outfile: str,
|
|
297
|
+
use_stderr: bool,
|
|
298
|
+
) -> list:
|
|
299
|
+
"""Resolve all sub-files for a single package (--all-files mode)."""
|
|
300
|
+
pkg_name = pkg.get("name", "unknown")
|
|
301
|
+
pkg_version = pkg.get("version", "unknown")
|
|
302
|
+
|
|
303
|
+
if not use_stderr:
|
|
304
|
+
click.echo("Getting package details ...", nl=False)
|
|
305
|
+
|
|
306
|
+
context_msg = f"Failed to get details for {pkg_name}!"
|
|
307
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
308
|
+
with maybe_spinner(opts):
|
|
309
|
+
detail = get_package_detail(owner=owner, repo=repo, identifier=pkg["slug"])
|
|
310
|
+
|
|
311
|
+
if not use_stderr:
|
|
312
|
+
click.secho("OK", fg="green")
|
|
313
|
+
|
|
314
|
+
sub_files = get_package_files(detail)
|
|
315
|
+
if not sub_files:
|
|
316
|
+
raise click.ClickException("No downloadable files found for this package.")
|
|
317
|
+
|
|
318
|
+
# Determine output directory
|
|
319
|
+
if outfile:
|
|
320
|
+
output_dir = os.path.abspath(outfile)
|
|
321
|
+
else:
|
|
322
|
+
output_dir = os.path.abspath(f"{pkg_name}-{pkg_version}")
|
|
323
|
+
|
|
324
|
+
if not os.path.exists(output_dir):
|
|
325
|
+
os.makedirs(output_dir)
|
|
326
|
+
elif not os.path.isdir(output_dir):
|
|
327
|
+
raise click.ClickException(
|
|
328
|
+
f"Output path '{output_dir}' exists but is not a directory."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
items = []
|
|
332
|
+
for f in sub_files:
|
|
333
|
+
items.append(
|
|
334
|
+
{
|
|
335
|
+
"filename": f["filename"],
|
|
336
|
+
"url": f["cdn_url"],
|
|
337
|
+
"output_path": _safe_join(output_dir, f["filename"]),
|
|
338
|
+
"tag": f.get("tag", "file"),
|
|
339
|
+
"is_primary": f.get("is_primary", False),
|
|
340
|
+
"size": f.get("size", 0),
|
|
341
|
+
"package_name": pkg_name,
|
|
342
|
+
"package_version": pkg_version,
|
|
343
|
+
"status": None,
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
return items
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _resolve_single_file_item(
|
|
350
|
+
ctx: click.Context,
|
|
351
|
+
opts,
|
|
352
|
+
pkg: dict,
|
|
353
|
+
owner: str,
|
|
354
|
+
repo: str,
|
|
355
|
+
outfile: str,
|
|
356
|
+
multi_package: bool,
|
|
357
|
+
use_stderr: bool,
|
|
358
|
+
) -> dict:
|
|
359
|
+
"""Resolve a single primary file for a package."""
|
|
360
|
+
pkg_name = pkg.get("name", "unknown")
|
|
361
|
+
pkg_version = pkg.get("version", "unknown")
|
|
362
|
+
|
|
363
|
+
download_url = get_download_url(pkg)
|
|
364
|
+
|
|
365
|
+
if not download_url:
|
|
366
|
+
# Fall back to detailed package info
|
|
367
|
+
if not use_stderr:
|
|
368
|
+
click.echo("Getting package details ...", nl=False)
|
|
369
|
+
context_msg = "Failed to get package details!"
|
|
370
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
371
|
+
with maybe_spinner(opts):
|
|
372
|
+
detail = get_package_detail(
|
|
373
|
+
owner=owner, repo=repo, identifier=pkg["slug"]
|
|
374
|
+
)
|
|
375
|
+
download_url = get_download_url(detail or pkg)
|
|
376
|
+
if not use_stderr:
|
|
377
|
+
click.secho("OK", fg="green")
|
|
378
|
+
|
|
379
|
+
# Determine output path
|
|
380
|
+
if outfile and not multi_package:
|
|
381
|
+
output_path = os.path.abspath(outfile)
|
|
382
|
+
elif multi_package:
|
|
383
|
+
output_dir = os.path.abspath(outfile) if outfile else os.path.abspath(".")
|
|
384
|
+
if not os.path.exists(output_dir):
|
|
385
|
+
os.makedirs(output_dir)
|
|
386
|
+
filename = pkg.get("filename") or f"{pkg_name}-{pkg_version}"
|
|
387
|
+
output_path = _safe_join(output_dir, filename)
|
|
388
|
+
elif pkg.get("filename"):
|
|
389
|
+
output_path = os.path.abspath(os.path.basename(pkg["filename"]))
|
|
390
|
+
else:
|
|
391
|
+
pkg_format = pkg.get("format", "bin")
|
|
392
|
+
extension = _get_extension_for_format(pkg_format)
|
|
393
|
+
output_path = os.path.abspath(f"{pkg_name}-{pkg_version}.{extension}")
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"filename": os.path.basename(output_path),
|
|
397
|
+
"url": download_url,
|
|
398
|
+
"output_path": output_path,
|
|
399
|
+
"tag": "file",
|
|
400
|
+
"is_primary": True,
|
|
401
|
+
"size": pkg.get("size", 0),
|
|
402
|
+
"package_name": pkg_name,
|
|
403
|
+
"package_version": pkg_version,
|
|
404
|
+
"status": None,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Step 4a: Dry-run display (shared by all paths)
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _display_dry_run(packages: list, download_items: list, all_files: bool) -> None:
|
|
414
|
+
"""Display what would be downloaded without actually downloading."""
|
|
415
|
+
click.echo()
|
|
416
|
+
click.echo(
|
|
417
|
+
f"Dry run - would download {len(download_items)} file(s) "
|
|
418
|
+
f"from {len(packages)} package(s):"
|
|
419
|
+
)
|
|
420
|
+
click.echo()
|
|
421
|
+
|
|
422
|
+
for item in download_items:
|
|
423
|
+
primary_marker = " (primary)" if item.get("is_primary") else ""
|
|
424
|
+
size = _format_file_size(item.get("size", 0))
|
|
425
|
+
tag = item.get("tag", "file")
|
|
426
|
+
click.echo(f" [{tag}] {item['filename']}{primary_marker} - {size}")
|
|
427
|
+
click.echo(f" Package: {item['package_name']} v{item['package_version']}")
|
|
428
|
+
click.echo(f" To: {item['output_path']}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
# Step 4b: Perform downloads
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _perform_downloads(
|
|
437
|
+
ctx: click.Context,
|
|
438
|
+
opts,
|
|
439
|
+
packages: list,
|
|
440
|
+
download_items: list,
|
|
441
|
+
session,
|
|
442
|
+
auth_headers: dict,
|
|
443
|
+
overwrite: bool,
|
|
444
|
+
all_files: bool,
|
|
445
|
+
use_stderr: bool,
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Download all resolved items and report results."""
|
|
448
|
+
total = len(download_items)
|
|
449
|
+
if not use_stderr:
|
|
450
|
+
click.echo(f"\nDownloading {total} file(s):")
|
|
451
|
+
click.echo()
|
|
452
|
+
|
|
453
|
+
results = []
|
|
454
|
+
|
|
455
|
+
for idx, item in enumerate(download_items, 1):
|
|
456
|
+
filename = item["filename"]
|
|
457
|
+
url = item["url"]
|
|
458
|
+
output_path = item["output_path"]
|
|
459
|
+
tag = item.get("tag", "file")
|
|
460
|
+
primary_marker = " (primary)" if item.get("is_primary") else ""
|
|
461
|
+
|
|
462
|
+
# Handle missing download URL as a skip, not a failure
|
|
463
|
+
if not url:
|
|
464
|
+
_echo_progress(
|
|
465
|
+
use_stderr, f"[{idx}/{total}] [{tag}] {filename}{primary_marker} ..."
|
|
466
|
+
)
|
|
467
|
+
_echo_status(use_stderr, " SKIPPED", fg="yellow")
|
|
468
|
+
results.append({**item, "status": "SKIPPED", "error": "No download URL"})
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
_echo_progress(
|
|
472
|
+
use_stderr, f"[{idx}/{total}] [{tag}] {filename}{primary_marker} ..."
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
context_msg = f"Failed to download {filename}!"
|
|
477
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
478
|
+
stream_download(
|
|
479
|
+
url=url,
|
|
480
|
+
outfile=output_path,
|
|
481
|
+
session=session,
|
|
482
|
+
headers=auth_headers,
|
|
483
|
+
overwrite=overwrite,
|
|
484
|
+
quiet=True,
|
|
485
|
+
)
|
|
486
|
+
_echo_status(use_stderr, " OK", fg="green")
|
|
487
|
+
results.append({**item, "status": "OK"})
|
|
488
|
+
except Exception as e: # pylint: disable=broad-except
|
|
489
|
+
_echo_status(use_stderr, " FAILED", fg="red")
|
|
490
|
+
results.append({**item, "status": "FAILED", "error": str(e)})
|
|
491
|
+
|
|
492
|
+
# Report results
|
|
493
|
+
_report_results(opts, packages, results, all_files)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _echo_progress(use_stderr: bool, message: str) -> None:
|
|
497
|
+
"""Print progress message to stdout or stderr."""
|
|
498
|
+
click.echo(message, nl=False, err=use_stderr)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _echo_status(use_stderr: bool, message: str, fg: str = None) -> None:
|
|
502
|
+
"""Print styled status message to stdout or stderr."""
|
|
503
|
+
if fg and not use_stderr:
|
|
504
|
+
click.secho(message, fg=fg)
|
|
505
|
+
elif use_stderr:
|
|
506
|
+
click.echo(message, err=True)
|
|
507
|
+
else:
|
|
508
|
+
click.echo(message)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _report_results(opts, packages: list, results: list, all_files: bool) -> None:
|
|
512
|
+
"""Build JSON output and print summary."""
|
|
513
|
+
success = [r for r in results if r["status"] == "OK"]
|
|
514
|
+
failed = [r for r in results if r["status"] == "FAILED"]
|
|
515
|
+
skipped = [r for r in results if r["status"] == "SKIPPED"]
|
|
516
|
+
|
|
517
|
+
json_output = {
|
|
518
|
+
"packages": [
|
|
519
|
+
{
|
|
520
|
+
"name": p.get("name"),
|
|
521
|
+
"version": p.get("version"),
|
|
522
|
+
"format": p.get("format"),
|
|
523
|
+
"filename": p.get("filename"),
|
|
524
|
+
"slug": p.get("slug"),
|
|
525
|
+
}
|
|
526
|
+
for p in packages
|
|
527
|
+
],
|
|
528
|
+
"files": [
|
|
529
|
+
{
|
|
530
|
+
"filename": r["filename"],
|
|
531
|
+
"path": r["output_path"],
|
|
532
|
+
"package": r["package_name"],
|
|
533
|
+
"version": r["package_version"],
|
|
534
|
+
"tag": r.get("tag", "file"),
|
|
535
|
+
"is_primary": r.get("is_primary", False),
|
|
536
|
+
"size": r.get("size", 0),
|
|
537
|
+
"status": r["status"],
|
|
538
|
+
**({"error": r["error"]} if "error" in r else {}),
|
|
539
|
+
}
|
|
540
|
+
for r in results
|
|
541
|
+
],
|
|
542
|
+
"summary": {
|
|
543
|
+
"total_packages": len(packages),
|
|
544
|
+
"total_files": len(results),
|
|
545
|
+
"success": len(success),
|
|
546
|
+
"failed": len(failed),
|
|
547
|
+
"skipped": len(skipped),
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if utils.maybe_print_as_json(opts, json_output):
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
click.echo()
|
|
555
|
+
if not failed and not skipped:
|
|
556
|
+
click.secho(f"All {len(success)} file(s) downloaded successfully!", fg="green")
|
|
557
|
+
else:
|
|
558
|
+
click.secho(f"Downloaded {len(success)}/{len(results)} file(s).", fg="yellow")
|
|
559
|
+
if failed:
|
|
560
|
+
click.echo("\nFailed files:")
|
|
561
|
+
for r in failed:
|
|
562
|
+
click.echo(f" - {r['filename']}: {r.get('error', 'Unknown error')}")
|
|
563
|
+
if skipped:
|
|
564
|
+
click.echo("\nSkipped files (no download URL):")
|
|
565
|
+
for r in skipped:
|
|
566
|
+
click.echo(f" - {r['filename']}")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
# Utilities
|
|
571
|
+
# ---------------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _safe_join(base_dir: str, filename: str) -> str:
|
|
575
|
+
"""Safely join base_dir and filename, preventing path traversal."""
|
|
576
|
+
safe_name = os.path.basename(filename)
|
|
577
|
+
if not safe_name:
|
|
578
|
+
raise click.ClickException(f"Invalid filename '{filename}'.")
|
|
579
|
+
result = os.path.join(base_dir, safe_name)
|
|
580
|
+
if not os.path.realpath(result).startswith(os.path.realpath(base_dir) + os.sep):
|
|
581
|
+
raise click.ClickException(
|
|
582
|
+
f"Filename '{filename}' resolves outside the target directory."
|
|
583
|
+
)
|
|
584
|
+
return result
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _get_extension_for_format(pkg_format: str) -> str:
|
|
588
|
+
"""Get appropriate file extension for package format."""
|
|
589
|
+
format_extensions = {
|
|
590
|
+
"deb": "deb",
|
|
591
|
+
"rpm": "rpm",
|
|
592
|
+
"python": "whl",
|
|
593
|
+
"npm": "tgz",
|
|
594
|
+
"maven": "jar",
|
|
595
|
+
"nuget": "nupkg",
|
|
596
|
+
"gem": "gem",
|
|
597
|
+
"go": "tar.gz",
|
|
598
|
+
"docker": "tar",
|
|
599
|
+
"helm": "tgz",
|
|
600
|
+
"raw": "bin",
|
|
601
|
+
"terraform": "zip",
|
|
602
|
+
}
|
|
603
|
+
return format_extensions.get(pkg_format.lower(), "bin")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _format_package_size(package: dict) -> str:
|
|
607
|
+
"""Format package size for display."""
|
|
608
|
+
size = package.get("size", 0)
|
|
609
|
+
return _format_file_size(size)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _format_file_size(size: int) -> str:
|
|
613
|
+
"""Format file size in bytes to human-readable format."""
|
|
614
|
+
if size == 0:
|
|
615
|
+
return "Unknown"
|
|
616
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
617
|
+
if size < 1024.0:
|
|
618
|
+
return f"{size:.1f} {unit}"
|
|
619
|
+
size /= 1024.0
|
|
620
|
+
return f"{size:.1f} PB"
|