Make install procedure overwrite existing addons
However, back them up first and restore them if anything goes wrong
This commit is contained in:
@@ -195,8 +195,10 @@ class BPKG_OT_install(SubprocMixin, bpy.types.Operator):
|
|||||||
install_path = pathlib.Path(bpy.utils.user_resource('SCRIPTS', 'addons', create=True))
|
install_path = pathlib.Path(bpy.utils.user_resource('SCRIPTS', 'addons', create=True))
|
||||||
self.log.debug("Using %s as install path", install_path)
|
self.log.debug("Using %s as install path", install_path)
|
||||||
|
|
||||||
|
import addon_utils
|
||||||
|
|
||||||
proc = multiprocessing.Process(target=subproc.download_and_install,
|
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
|
return proc
|
||||||
|
|
||||||
def _subproc_progress(self, progress: subproc.Progress):
|
def _subproc_progress(self, progress: subproc.Progress):
|
||||||
|
@@ -4,6 +4,7 @@ All the stuff that needs to run in a subprocess.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
class Message:
|
||||||
@@ -72,6 +73,49 @@ class Aborted(SubprocMessage):
|
|||||||
class InstallException(Exception):
|
class InstallException(Exception):
|
||||||
"""Raised when there is an error during installation"""
|
"""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:
|
def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> pathlib.Path:
|
||||||
"""Downloads the given package
|
"""Downloads the given package
|
||||||
|
|
||||||
@@ -133,7 +177,7 @@ def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) ->
|
|||||||
|
|
||||||
return local_fpath
|
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`"""
|
"""Extracts/moves package at `pkgpath` to `dest`"""
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
@@ -178,31 +222,33 @@ def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path):
|
|||||||
rootlist.append(f)
|
rootlist.append(f)
|
||||||
return rootlist
|
return rootlist
|
||||||
|
|
||||||
conflicts = [f for f in root_files(file_to_extract.namelist()) if (dest / f).exists()]
|
conflicts = [dest / f for f in root_files(file_to_extract.namelist()) if (dest / f).exists()]
|
||||||
if len(conflicts) > 0:
|
backups = []
|
||||||
# TODO: handle this better than just dumping a list of all files
|
for conflict in conflicts:
|
||||||
pipe_to_blender.send(FileConflictError("Installation would overwrite: %s" % conflicts, conflicts))
|
backups.append(InplaceBackup(conflict))
|
||||||
raise InstallException
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_to_extract.extractall(str(dest))
|
file_to_extract.extractall(str(dest))
|
||||||
except Exception as err:
|
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)))
|
pipe_to_blender.send(InstallError("Failed to extract zip file to '%s': %s" % (dest, err)))
|
||||||
raise InstallException from err
|
raise InstallException from err
|
||||||
|
|
||||||
|
for backup in backups:
|
||||||
|
backup.remove()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug("Package is pyfile")
|
log.debug("Package is pyfile")
|
||||||
dest_file = (dest / pkgpath.name)
|
dest_file = (dest / pkgpath.name)
|
||||||
|
|
||||||
if dest_file.exists():
|
if dest_file.exists():
|
||||||
pipe_to_blender.send(FileConflictError("Installation would overwrite %s" % dest_file, [dest_file]))
|
backup = InplaceBackup(dest_file)
|
||||||
raise InstallException
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(str(pkgpath), str(dest_file))
|
shutil.copyfile(str(pkgpath), str(dest_file))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
backup.restore()
|
||||||
pipe_to_blender.send(InstallError("Failed to copy file to '%s': %s" % (dest, err)))
|
pipe_to_blender.send(InstallError("Failed to copy file to '%s': %s" % (dest, err)))
|
||||||
raise InstallException from err
|
raise InstallException from err
|
||||||
|
|
||||||
@@ -217,13 +263,15 @@ def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path):
|
|||||||
return
|
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."""
|
"""Downloads and installs the given package."""
|
||||||
|
|
||||||
from . import cache
|
from . import cache
|
||||||
|
|
||||||
log = logging.getLogger('%s.download_and_install' % __name__)
|
log = logging.getLogger('%s.download_and_install' % __name__)
|
||||||
|
|
||||||
|
# log.debug(utils.user_resource('SCRIPTS', 'blorp'))
|
||||||
|
|
||||||
cache_dir = cache.cache_directory('downloads')
|
cache_dir = cache.cache_directory('downloads')
|
||||||
downloaded = _download(pipe_to_blender, package_url, cache_dir)
|
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
|
# 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)
|
# Maybe not the best way to do this? (will also catch exceptions which occur during message sending)
|
||||||
try:
|
try:
|
||||||
_install(pipe_to_blender, downloaded, install_dir)
|
_install(pipe_to_blender, downloaded, install_path, search_paths)
|
||||||
pipe_to_blender.send(Success())
|
pipe_to_blender.send(Success())
|
||||||
|
except InstallException as err:
|
||||||
|
log.debug("InstallException thrown")
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user