diff --git a/scripts/addons_core/bl_pkg/bl_extension_notify.py b/scripts/addons_core/bl_pkg/bl_extension_notify.py index 5e58299b8b6..ea7578af0cc 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_notify.py +++ b/scripts/addons_core/bl_pkg/bl_extension_notify.py @@ -168,7 +168,7 @@ def sync_status_generator(repos_notify): cmd_batch_partial.append(partial( bl_extension_utils.repo_sync, directory=repo_item.directory, - remote_url=repo_item.remote_url, + remote_url=bl_extension_ops.url_params_append_defaults(repo_item.remote_url), online_user_agent=bl_extension_ops.online_user_agent_from_blender(), access_token=repo_item.access_token if repo_item.use_access_token else "", # Never sleep while there is no input, as this blocks Blender. diff --git a/scripts/addons_core/bl_pkg/bl_extension_ops.py b/scripts/addons_core/bl_pkg/bl_extension_ops.py index f8388bc93f9..4a2c070de58 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_ops.py +++ b/scripts/addons_core/bl_pkg/bl_extension_ops.py @@ -67,6 +67,10 @@ rna_prop_enable_on_install_type_map = { } +def url_params_append_defaults(url): + return bl_extension_utils.url_params_append_for_blender(url, blender_version=bpy.app.version) + + def rna_prop_repo_enum_local_only_itemf(_self, context): if context is None: result = [] @@ -1003,7 +1007,7 @@ class BlPkgRepoSync(Operator, _BlPkgCmdMixIn): partial( bl_extension_utils.repo_sync, directory=directory, - remote_url=repo_item.remote_url, + remote_url=url_params_append_defaults(repo_item.remote_url), online_user_agent=online_user_agent_from_blender(), access_token=repo_item.access_token, use_idle=is_modal, @@ -1080,7 +1084,7 @@ class BlPkgRepoSyncAll(Operator, _BlPkgCmdMixIn): cmd_batch.append(partial( bl_extension_utils.repo_sync, directory=repo_item.directory, - remote_url=repo_item.remote_url, + remote_url=url_params_append_defaults(repo_item.remote_url), online_user_agent=online_user_agent_from_blender(), access_token=repo_item.access_token, use_idle=is_modal, @@ -1208,7 +1212,7 @@ class BlPkgPkgUpgradeAll(Operator, _BlPkgCmdMixIn): cmd_batch.append(partial( bl_extension_utils.pkg_install, directory=repo_item.directory, - remote_url=repo_item.remote_url, + remote_url=url_params_append_defaults(repo_item.remote_url), pkg_id_sequence=pkg_id_sequence, online_user_agent=online_user_agent_from_blender(), access_token=repo_item.access_token, @@ -1308,7 +1312,7 @@ class BlPkgPkgInstallMarked(Operator, _BlPkgCmdMixIn): cmd_batch.append(partial( bl_extension_utils.pkg_install, directory=repo_item.directory, - remote_url=repo_item.remote_url, + remote_url=url_params_append_defaults(repo_item.remote_url), pkg_id_sequence=pkg_id_sequence, online_user_agent=online_user_agent_from_blender(), access_token=repo_item.access_token, @@ -1826,7 +1830,7 @@ class BlPkgPkgInstall(Operator, _BlPkgCmdMixIn): partial( bl_extension_utils.pkg_install, directory=directory, - remote_url=repo_item.remote_url, + remote_url=url_params_append_defaults(repo_item.remote_url), pkg_id_sequence=(pkg_id,), online_user_agent=online_user_agent_from_blender(), access_token=repo_item.access_token, diff --git a/scripts/addons_core/bl_pkg/bl_extension_utils.py b/scripts/addons_core/bl_pkg/bl_extension_utils.py index 461fd6af382..1fc75c3f8a4 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_utils.py +++ b/scripts/addons_core/bl_pkg/bl_extension_utils.py @@ -25,6 +25,7 @@ __all__ = ( # Public Stand-Alone Utilities. "pkg_theme_file_list", + "url_params_append_for_blender", "file_mtime_or_none", # Public API. @@ -277,6 +278,52 @@ def pkg_theme_file_list(directory: str, pkg_idname: str) -> Tuple[str, List[str] return theme_dir, theme_files +def _url_params_append(url: str, params: Dict[str, str]) -> str: + import urllib + import urllib.parse + + # Remove empty parameters. + params = {key: value for key, value in params.items() if value is not None and value != ""} + if not params: + return url + + # Parse the URL to get its scheme, domain, and query parameters. + parsed_url = urllib.parse.urlparse(url) + + # Combine existing query parameters with new parameters + existing_params = urllib.parse.parse_qsl(parsed_url.query) + all_params = dict(existing_params) + all_params.update(params) + + # Encode all parameters into a new query string + new_query = urllib.parse.urlencode(all_params) + + # Combine the scheme, netloc, path, and new query string to form the new URL + new_url = urllib.parse.urlunparse(( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + parsed_url.params, + new_query, + parsed_url.fragment, + )) + + return new_url + + +def url_params_append_for_blender(url: str, blender_version: Tuple[int, int, int]) -> str: + # `blender_version` is typically `bpy.app.version`. + + # While this won't cause errors, it's redundant to add this information to file URL's. + if url.startswith("file://"): + return url + + params = { + "blender_version": "{:d}.{:d}.{:d}".format(*blender_version), + } + return _url_params_append(url, params) + + # ----------------------------------------------------------------------------- # Public Repository Actions # diff --git a/scripts/addons_core/bl_pkg/cli/blender_ext.py b/scripts/addons_core/bl_pkg/cli/blender_ext.py index 6928e7cf022..bb02f7dcebb 100755 --- a/scripts/addons_core/bl_pkg/cli/blender_ext.py +++ b/scripts/addons_core/bl_pkg/cli/blender_ext.py @@ -676,6 +676,23 @@ def remote_url_get(url: str) -> str: return url +def remote_url_params_strip(url: str) -> str: + # Parse the URL to get its scheme, domain, and query parameters. + parsed_url = urllib.parse.urlparse(url) + + # Combine the scheme, netloc, path without any other parameters, stripping the URL. + new_url = urllib.parse.urlunparse(( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + None, # `parsed_url.params,` + None, # `parsed_url.query,` + None, # `parsed_url.fragment,` + )) + + return new_url + + # ----------------------------------------------------------------------------- # ZipFile Helpers @@ -1072,18 +1089,19 @@ def url_retrieve_exception_as_message( Provides more user friendly messages when reading from a URL fails. """ # These exceptions may occur when reading from the file-system or a URL. + url_strip = remote_url_params_strip(url) if isinstance(ex, FileNotFoundError): - return "{:s}: file-not-found ({:s}) reading {!r}!".format(prefix, str(ex), url) + return "{:s}: file-not-found ({:s}) reading {!r}!".format(prefix, str(ex), url_strip) if isinstance(ex, TimeoutError): - return "{:s}: timeout ({:s}) reading {!r}!".format(prefix, str(ex), url) + return "{:s}: timeout ({:s}) reading {!r}!".format(prefix, str(ex), url_strip) if isinstance(ex, urllib.error.URLError): if isinstance(ex, urllib.error.HTTPError): if ex.code == 403: - return "{:s}: HTTP error (403) access token may be incorrect, reading {!r}!".format(prefix, url) - return "{:s}: HTTP error ({:s}) reading {!r}!".format(prefix, str(ex), url) - return "{:s}: URL error ({:s}) reading {!r}!".format(prefix, str(ex), url) + return "{:s}: HTTP error (403) access token may be incorrect, reading {!r}!".format(prefix, url_strip) + return "{:s}: HTTP error ({:s}) reading {!r}!".format(prefix, str(ex), url_strip) + return "{:s}: URL error ({:s}) reading {!r}!".format(prefix, str(ex), url_strip) - return "{:s}: unexpected error ({:s}) reading {!r}!".format(prefix, str(ex), url) + return "{:s}: unexpected error ({:s}) reading {!r}!".format(prefix, str(ex), url_strip) def pkg_idname_is_valid_or_error(pkg_idname: str) -> Optional[str]: @@ -2328,6 +2346,9 @@ class subcmd_client: # Ensure a private directory so a local cache can be created. local_cache_dir = repo_local_private_dir_ensure_with_subdir(local_dir=local_dir, subdir="cache") + # Needed so relative paths can be properly calculated. + remote_url_strip = remote_url_params_strip(remote_url) + # TODO: this could be optimized to only lookup known ID's. json_data_pkg_info_map: Dict[str, Dict[str, Any]] = { pkg_info["id"]: pkg_info for pkg_info in pkg_repo_data.data @@ -2376,10 +2397,10 @@ class subcmd_client: # Remote path. if pkg_archive_url.startswith("./"): - if remote_url_has_filename_suffix(remote_url): - filepath_remote_archive = remote_url.rpartition("/")[0] + pkg_archive_url[1:] + if remote_url_has_filename_suffix(remote_url_strip): + filepath_remote_archive = remote_url_strip.rpartition("/")[0] + pkg_archive_url[1:] else: - filepath_remote_archive = remote_url.rstrip("/") + pkg_archive_url[1:] + filepath_remote_archive = remote_url_strip.rstrip("/") + pkg_archive_url[1:] else: filepath_remote_archive = pkg_archive_url