diff --git a/bpkg_manager/__init__.py b/bpkg_manager/__init__.py index 95e1042..28f3897 100644 --- a/bpkg_manager/__init__.py +++ b/bpkg_manager/__init__.py @@ -195,8 +195,10 @@ class BPKG_OT_install(SubprocMixin, bpy.types.Operator): install_path = pathlib.Path(bpy.utils.user_resource('SCRIPTS', 'addons', create=True)) self.log.debug("Using %s as install path", install_path) + import addon_utils + proc = multiprocessing.Process(target=subproc.download_and_install, - args=(self.pipe_subproc, self.package_url, install_path)) + args=(self.pipe_subproc, self.package_url, install_path, addon_utils.paths())) return proc def _subproc_progress(self, progress: subproc.Progress): diff --git a/bpkg_manager/subproc.py b/bpkg_manager/subproc.py index f0112ba..daa57b3 100644 --- a/bpkg_manager/subproc.py +++ b/bpkg_manager/subproc.py @@ -4,6 +4,7 @@ All the stuff that needs to run in a subprocess. import logging import pathlib +import shutil class Message: @@ -72,6 +73,49 @@ class Aborted(SubprocMessage): class InstallException(Exception): """Raised when there is an error during installation""" + +class InplaceBackup: + """Utility for moving a file out of the way by appending a '~'""" + + log = logging.getLogger('%s.inplace-backup' % __name__) + + def __init__(self, path: pathlib.Path): + self.path = path + self.backup() + + def backup(self): + """Move 'path' to 'path~'""" + self.backup_path = pathlib.Path(str(self.path) + '~') + if self.backup_path.exists(): + self.log.warning("Overwriting existing backup '{}'".format(self.backup_path)) + self._rm(self.backup_path) + + shutil.move(str(self.path), str(self.backup_path)) + + def restore(self): + """Move 'path~' to 'path'""" + if not self.backup_path: + raise RuntimeError("Can't restore file before backing it up") + + if self.path.exists(): + self.log.warning("Overwriting '{0}' with backup file".format(self.path)) + self._rm(self.backup_path) + + shutil.move(str(self.backup_path), str(self.path)) + + def remove(self): + """Remove 'path~'""" + self._rm(self.backup_path) + + def _rm(self, path: pathlib.Path): + """Just delete whatever is specified by `path`""" + if path.is_file(): + path.unlink() + elif path.is_dir(): + shutil.rmtree(str(path)) + + + def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> pathlib.Path: """Downloads the given package @@ -133,7 +177,7 @@ def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> return local_fpath -def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path): +def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path, searchpaths: list): """Extracts/moves package at `pkgpath` to `dest`""" import zipfile @@ -178,31 +222,33 @@ def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path): rootlist.append(f) return rootlist - conflicts = [f for f in root_files(file_to_extract.namelist()) if (dest / f).exists()] - if len(conflicts) > 0: - # TODO: handle this better than just dumping a list of all files - pipe_to_blender.send(FileConflictError("Installation would overwrite: %s" % conflicts, conflicts)) - raise InstallException + conflicts = [dest / f for f in root_files(file_to_extract.namelist()) if (dest / f).exists()] + backups = [] + for conflict in conflicts: + backups.append(InplaceBackup(conflict)) try: file_to_extract.extractall(str(dest)) except Exception as err: + for backup in backups: + backup.restore() pipe_to_blender.send(InstallError("Failed to extract zip file to '%s': %s" % (dest, err))) raise InstallException from err + for backup in backups: + backup.remove() + else: log.debug("Package is pyfile") dest_file = (dest / pkgpath.name) if dest_file.exists(): - pipe_to_blender.send(FileConflictError("Installation would overwrite %s" % dest_file, [dest_file])) - raise InstallException - - import shutil + backup = InplaceBackup(dest_file) try: shutil.copyfile(str(pkgpath), str(dest_file)) except Exception as err: + backup.restore() pipe_to_blender.send(InstallError("Failed to copy file to '%s': %s" % (dest, err))) raise InstallException from err @@ -217,13 +263,15 @@ def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path): return -def download_and_install(pipe_to_blender, package_url: str, install_dir: pathlib.Path): +def download_and_install(pipe_to_blender, package_url: str, install_path: pathlib.Path, search_paths: list): """Downloads and installs the given package.""" from . import cache log = logging.getLogger('%s.download_and_install' % __name__) + # log.debug(utils.user_resource('SCRIPTS', 'blorp')) + cache_dir = cache.cache_directory('downloads') downloaded = _download(pipe_to_blender, package_url, cache_dir) @@ -234,8 +282,10 @@ def download_and_install(pipe_to_blender, package_url: str, install_dir: pathlib # Only send success if _install doesn't throw an exception # Maybe not the best way to do this? (will also catch exceptions which occur during message sending) try: - _install(pipe_to_blender, downloaded, install_dir) + _install(pipe_to_blender, downloaded, install_path, search_paths) pipe_to_blender.send(Success()) + except InstallException as err: + log.debug("InstallException thrown") except: raise