Merge branch 'blender-v4.2-release'
This commit is contained in:
@@ -315,6 +315,7 @@ def extenion_repos_files_clear(directory, _):
|
||||
import os
|
||||
from .bl_extension_utils import (
|
||||
scandir_with_demoted_errors,
|
||||
rmtree_with_fallback_or_error,
|
||||
PKG_MANIFEST_FILENAME_TOML,
|
||||
REPO_LOCAL_PRIVATE_DIR,
|
||||
)
|
||||
@@ -324,21 +325,15 @@ def extenion_repos_files_clear(directory, _):
|
||||
return
|
||||
|
||||
if os.path.isdir(path := os.path.join(directory, REPO_LOCAL_PRIVATE_DIR)):
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except Exception as ex:
|
||||
print("Failed to remove files", ex)
|
||||
if (error := rmtree_with_fallback_or_error(path)) is not None:
|
||||
print("Failed to remove \"{:s}\", error ({:s})".format(path, error))
|
||||
|
||||
for entry in scandir_with_demoted_errors(directory):
|
||||
if not entry.is_dir():
|
||||
continue
|
||||
path = entry.path
|
||||
if not os.path.exists(os.path.join(path, PKG_MANIFEST_FILENAME_TOML)):
|
||||
continue
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except Exception as ex:
|
||||
print("Failed to remove files", ex)
|
||||
if (error := rmtree_with_fallback_or_error(path)) is not None:
|
||||
print("Failed to remove \"{:s}\", error ({:s})".format(path, error))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -30,6 +30,8 @@ __all__ = (
|
||||
"url_append_query_for_blender",
|
||||
"url_parse_for_blender",
|
||||
"file_mtime_or_none",
|
||||
"scandir_with_demoted_errors",
|
||||
"rmtree_with_fallback_or_error",
|
||||
|
||||
# Public API.
|
||||
"json_from_filepath",
|
||||
@@ -192,6 +194,20 @@ def scandir_with_demoted_errors(path: str) -> Generator[os.DirEntry[str], None,
|
||||
print("Error: scandir", ex)
|
||||
|
||||
|
||||
def rmtree_with_fallback_or_error(
|
||||
path: str,
|
||||
*,
|
||||
remove_file: bool = True,
|
||||
remove_link: bool = True,
|
||||
) -> Optional[str]:
|
||||
from .cli.blender_ext import rmtree_with_fallback_or_error as fn
|
||||
return fn(
|
||||
path,
|
||||
remove_file=remove_file,
|
||||
remove_link=remove_link,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Call JSON.
|
||||
#
|
||||
|
||||
@@ -441,6 +441,67 @@ def scandir_recursive(
|
||||
yield from scandir_recursive_impl(path, path, filter_fn=filter_fn)
|
||||
|
||||
|
||||
def rmtree_with_fallback_or_error(
|
||||
path: str,
|
||||
*,
|
||||
remove_file: bool = True,
|
||||
remove_link: bool = True,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Remove a directory, with optional fallbacks to removing files & links.
|
||||
Use this when a directory is expected, but there is the possibility
|
||||
that there is a file or symbolic-link which should be removed instead.
|
||||
|
||||
Intended to be used user managed files,
|
||||
where removal is required and we can't be certain of the kind of file.
|
||||
|
||||
On failure, a string will be returned containing the first error.
|
||||
"""
|
||||
# Note that `shutil.rmtree` has link detection that doesn't match `os.path.islink` exactly,
|
||||
# so use it's callback that raises a link error and remove the link in that case.
|
||||
errors = []
|
||||
|
||||
shutil.rmtree(path, onexc=lambda *args: errors.append(args))
|
||||
|
||||
# Happy path (for practically all cases).
|
||||
if not errors:
|
||||
return None
|
||||
|
||||
is_file = False
|
||||
is_link = False
|
||||
|
||||
for err_type, _err_path, ex in errors:
|
||||
if isinstance(ex, NotADirectoryError):
|
||||
if err_type is os.rmdir:
|
||||
is_file = True
|
||||
if isinstance(ex, OSError):
|
||||
if err_type is os.path.islink:
|
||||
is_link = True
|
||||
|
||||
do_unlink = False
|
||||
if is_file:
|
||||
if remove_file:
|
||||
do_unlink = True
|
||||
if is_link:
|
||||
if remove_link:
|
||||
do_unlink = True
|
||||
|
||||
if do_unlink:
|
||||
# Replace errors with the failure state of `os.unlink`.
|
||||
errors.clear()
|
||||
try:
|
||||
os.unlink(path)
|
||||
except Exception as ex:
|
||||
errors.append((os.unlink, path, ex))
|
||||
|
||||
if errors:
|
||||
# Other information may be useful but it's too verbose to forward to user messages
|
||||
# and is more for debugging purposes.
|
||||
return str(errors[0][2])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_paths_expand_iter(
|
||||
path: str,
|
||||
path_list: Sequence[str],
|
||||
@@ -3041,8 +3102,13 @@ class subcmd_client:
|
||||
filepath_local_pkg_temp = filepath_local_pkg + "@"
|
||||
|
||||
# It's unlikely this exist, nevertheless if it does - it must be removed.
|
||||
if os.path.isdir(filepath_local_pkg_temp):
|
||||
shutil.rmtree(filepath_local_pkg_temp)
|
||||
if os.path.exists(filepath_local_pkg_temp):
|
||||
if (error := rmtree_with_fallback_or_error(filepath_local_pkg_temp)) is not None:
|
||||
message_warn(
|
||||
msg_fn,
|
||||
"Failed to remove temporary directory for \"{:s}\": {:s}".format(manifest.id, error),
|
||||
)
|
||||
return False
|
||||
|
||||
directories_to_clean.append(filepath_local_pkg_temp)
|
||||
|
||||
@@ -3062,7 +3128,13 @@ class subcmd_client:
|
||||
|
||||
is_reinstall = False
|
||||
if os.path.isdir(filepath_local_pkg):
|
||||
shutil.rmtree(filepath_local_pkg)
|
||||
if (error := rmtree_with_fallback_or_error(filepath_local_pkg)) is not None:
|
||||
message_warn(
|
||||
msg_fn,
|
||||
"Failed to remove existing directory for \"{:s}\": {:s}".format(manifest.id, error),
|
||||
)
|
||||
return False
|
||||
|
||||
is_reinstall = True
|
||||
|
||||
os.rename(filepath_local_pkg_temp, filepath_local_pkg)
|
||||
@@ -3337,27 +3409,27 @@ class subcmd_client:
|
||||
|
||||
packages_valid = []
|
||||
|
||||
error = False
|
||||
has_error = False
|
||||
for pkg_idname in packages:
|
||||
# As this simply removes the directories right now,
|
||||
# validate this path cannot be used for an unexpected outcome,
|
||||
# or using `../../` to remove directories that shouldn't.
|
||||
if (pkg_idname in {"", ".", ".."}) or ("\\" in pkg_idname or "/" in pkg_idname):
|
||||
message_error(msg_fn, "Package name invalid \"{:s}\"".format(pkg_idname))
|
||||
error = True
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# This will be a directory.
|
||||
filepath_local_pkg = os.path.join(local_dir, pkg_idname)
|
||||
if not os.path.isdir(filepath_local_pkg):
|
||||
message_error(msg_fn, "Package not found \"{:s}\"".format(pkg_idname))
|
||||
error = True
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
packages_valid.append(pkg_idname)
|
||||
del filepath_local_pkg
|
||||
|
||||
if error:
|
||||
if has_error:
|
||||
return False
|
||||
|
||||
# Ensure a private directory so a local cache can be created.
|
||||
@@ -3368,10 +3440,9 @@ class subcmd_client:
|
||||
with CleanupPathsContext(files=files_to_clean, directories=()):
|
||||
for pkg_idname in packages_valid:
|
||||
filepath_local_pkg = os.path.join(local_dir, pkg_idname)
|
||||
try:
|
||||
shutil.rmtree(filepath_local_pkg)
|
||||
except Exception as ex:
|
||||
message_error(msg_fn, "Failure to remove \"{:s}\" with error ({:s})".format(pkg_idname, str(ex)))
|
||||
|
||||
if (error := rmtree_with_fallback_or_error(filepath_local_pkg)) is not None:
|
||||
message_error(msg_fn, "Failure to remove \"{:s}\" with error ({:s})".format(pkg_idname, error))
|
||||
continue
|
||||
|
||||
message_status(msg_fn, "Removed \"{:s}\"".format(pkg_idname))
|
||||
@@ -3383,13 +3454,10 @@ class subcmd_client:
|
||||
if user_dir:
|
||||
filepath_user_pkg = os.path.join(user_dir, pkg_idname)
|
||||
if os.path.isdir(filepath_user_pkg):
|
||||
shutil.rmtree(filepath_user_pkg)
|
||||
try:
|
||||
shutil.rmtree(filepath_user_pkg)
|
||||
except Exception as ex:
|
||||
if (error := rmtree_with_fallback_or_error(filepath_user_pkg)) is not None:
|
||||
message_error(
|
||||
msg_fn,
|
||||
"Failure to remove \"{:s}\" user files with error ({:s})".format(pkg_idname, str(ex)),
|
||||
"Failure to remove \"{:s}\" user files with error ({:s})".format(pkg_idname, error),
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user