Basic install function
This commit is contained in:
@@ -182,12 +182,21 @@ class BPKG_OT_install(SubprocMixin, bpy.types.Operator):
|
||||
self.msg_handlers = {
|
||||
subproc.Progress: self._subproc_progress,
|
||||
subproc.DownloadError: self._subproc_download_error,
|
||||
subproc.InstallError: self._subproc_install_error,
|
||||
subproc.FileConflictError: self._subproc_conflict_error,
|
||||
subproc.Success: self._subproc_success,
|
||||
subproc.Aborted: self._subproc_aborted,
|
||||
}
|
||||
|
||||
import pathlib
|
||||
|
||||
# TODO: We need other paths besides this one on subprocess end, so it might be better to pass them all at once.
|
||||
# For now, just pass this one.
|
||||
install_path = pathlib.Path(bpy.utils.user_resource('SCRIPTS', 'addons', create=True))
|
||||
self.log.debug("Using %s as install path", install_path)
|
||||
|
||||
proc = multiprocessing.Process(target=subproc.download_and_install,
|
||||
args=(self.pipe_subproc, self.package_url,))
|
||||
args=(self.pipe_subproc, self.package_url, install_path))
|
||||
return proc
|
||||
|
||||
def _subproc_progress(self, progress: subproc.Progress):
|
||||
@@ -197,12 +206,20 @@ class BPKG_OT_install(SubprocMixin, bpy.types.Operator):
|
||||
self.report({'ERROR'}, 'Unable to download package: %s' % error.description)
|
||||
self.quit()
|
||||
|
||||
def _subproc_install_error(self, error: subproc.InstallError):
|
||||
self.report({'ERROR'}, 'Unable to install package: %s' % error.message)
|
||||
self.quit()
|
||||
|
||||
def _subproc_conflict_error(self, error: subproc.FileConflictError):
|
||||
self.report({'ERROR'}, 'Unable to install package: %s' % error.message)
|
||||
self.quit()
|
||||
|
||||
def _subproc_success(self, success: subproc.Success):
|
||||
self.report({'INFO'}, 'Package downloaded successfully')
|
||||
self.report({'INFO'}, 'Package installed successfully')
|
||||
self.quit()
|
||||
|
||||
def _subproc_aborted(self, aborted: subproc.Aborted):
|
||||
self.report({'ERROR'}, 'Package download aborted per your request')
|
||||
self.report({'ERROR'}, 'Package installation aborted per your request')
|
||||
self.quit()
|
||||
|
||||
def report_process_died(self):
|
||||
|
@@ -31,6 +31,27 @@ class Progress(SubprocMessage):
|
||||
def __init__(self, progress: float):
|
||||
self.progress = progress
|
||||
|
||||
class SubprocError(SubprocMessage):
|
||||
"""Superclass for all fatal error messages sent from the subprocess."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
|
||||
class SubprocWarning(SubprocMessage):
|
||||
"""Superclass for all non-fatal warning messages sent from the subprocess."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
|
||||
class InstallError(SubprocError):
|
||||
"""Sent when there was an error installing something."""
|
||||
|
||||
class FileConflictError(InstallError):
|
||||
"""Sent when installation would overwrite existing files."""
|
||||
|
||||
def __init__(self, message: str, conflicts: list):
|
||||
self.message = message
|
||||
self.conflicts = conflicts
|
||||
|
||||
class DownloadError(SubprocMessage):
|
||||
"""Sent when there was an error downloading something."""
|
||||
@@ -48,6 +69,9 @@ class Aborted(SubprocMessage):
|
||||
"""Sent as response to Abort message."""
|
||||
|
||||
|
||||
class InstallException(Exception):
|
||||
"""Raised when there is an error during installation"""
|
||||
|
||||
def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> pathlib.Path:
|
||||
"""Downloads the given package
|
||||
|
||||
@@ -77,6 +101,8 @@ def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) ->
|
||||
log.warning('Server did not send content length, cannot report progress.')
|
||||
content_length = float('inf')
|
||||
|
||||
# TODO: check if there's enough disk space.
|
||||
|
||||
# TODO: get filename from Content-Disposition header, if available.
|
||||
# TODO: use urllib.parse to parse the URL.
|
||||
local_filename = package_url.split('/')[-1] or 'download.tmp'
|
||||
@@ -105,11 +131,74 @@ def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) ->
|
||||
# leave 30% "progress" for installation of the package.
|
||||
pipe_to_blender.send(Progress(downloaded_length / content_length))
|
||||
|
||||
pipe_to_blender.send(Success())
|
||||
return local_fpath
|
||||
|
||||
def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path):
|
||||
"""Extracts/moves package at `pkgpath` to `dest`"""
|
||||
import zipfile
|
||||
|
||||
def download_and_install(pipe_to_blender, package_url: str):
|
||||
pipe_to_blender.send(Progress(0.0))
|
||||
|
||||
if not pkgpath.is_file():
|
||||
log.error("File to install isn't a file: %s", pkgpath)
|
||||
pipe_to_blender.send(InstallError("Package is not a file"))
|
||||
raise InstallException
|
||||
|
||||
if not dest.is_dir():
|
||||
log.error("Destination for install isn't a directory: %s", dest)
|
||||
pipe_to_blender.send(InstallError("Destination is not a directory"))
|
||||
raise InstallException
|
||||
|
||||
# TODO: check to make sure addon/package isn't already installed elsewhere
|
||||
|
||||
# The following is adapted from `addon_install` in bl_operators/wm.py
|
||||
|
||||
# check to see if the file is in compressed format (.zip)
|
||||
if zipfile.is_zipfile(pkgpath):
|
||||
try:
|
||||
file_to_extract = zipfile.ZipFile(str(pkgpath), 'r')
|
||||
except Exception as err:
|
||||
pipe_to_blender.send(InstallError("Failed to read zip file: %s" % err))
|
||||
raise InstallException from err
|
||||
|
||||
conflicts = [f for f in 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
|
||||
|
||||
try:
|
||||
file_to_extract.extractall(str(dest))
|
||||
except Exception as err:
|
||||
pipe_to_blender.send(InstallError("Failed to extract zip file to '%s': %s" % (dest, err)))
|
||||
raise InstallException from err
|
||||
|
||||
else:
|
||||
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
|
||||
|
||||
try:
|
||||
shutil.copyfile(str(pkgpath), str(dest_file))
|
||||
except Exception as err:
|
||||
pipe_to_blender.send(InstallError("Failed to copy file to '%s': %s" % (dest, err)))
|
||||
raise InstallException from err
|
||||
|
||||
try:
|
||||
pkgpath.unlink()
|
||||
except Exception as err:
|
||||
pipe_to_blender.send(SubprocWarning("Failed to remove package from cache: %s" % err))
|
||||
raise InstallException from err
|
||||
|
||||
pipe_to_blender.send(Progress(1.0))
|
||||
return
|
||||
|
||||
|
||||
def download_and_install(pipe_to_blender, package_url: str, install_dir: pathlib.Path):
|
||||
"""Downloads and installs the given package."""
|
||||
|
||||
from . import cache
|
||||
@@ -123,8 +212,13 @@ def download_and_install(pipe_to_blender, package_url: str):
|
||||
log.debug('Download failed/aborted, not going to install anything.')
|
||||
return
|
||||
|
||||
# TODO: actually install the package
|
||||
log.warning('Installing is not actually implemented, install %s yourself.', downloaded)
|
||||
# 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)
|
||||
pipe_to_blender.send(Success())
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
def debug_hang():
|
||||
|
Reference in New Issue
Block a user