Basic install function
This commit is contained in:
@@ -182,12 +182,21 @@ class BPKG_OT_install(SubprocMixin, bpy.types.Operator):
|
|||||||
self.msg_handlers = {
|
self.msg_handlers = {
|
||||||
subproc.Progress: self._subproc_progress,
|
subproc.Progress: self._subproc_progress,
|
||||||
subproc.DownloadError: self._subproc_download_error,
|
subproc.DownloadError: self._subproc_download_error,
|
||||||
|
subproc.InstallError: self._subproc_install_error,
|
||||||
|
subproc.FileConflictError: self._subproc_conflict_error,
|
||||||
subproc.Success: self._subproc_success,
|
subproc.Success: self._subproc_success,
|
||||||
subproc.Aborted: self._subproc_aborted,
|
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,
|
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
|
return proc
|
||||||
|
|
||||||
def _subproc_progress(self, progress: subproc.Progress):
|
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.report({'ERROR'}, 'Unable to download package: %s' % error.description)
|
||||||
self.quit()
|
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):
|
def _subproc_success(self, success: subproc.Success):
|
||||||
self.report({'INFO'}, 'Package downloaded successfully')
|
self.report({'INFO'}, 'Package installed successfully')
|
||||||
self.quit()
|
self.quit()
|
||||||
|
|
||||||
def _subproc_aborted(self, aborted: subproc.Aborted):
|
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()
|
self.quit()
|
||||||
|
|
||||||
def report_process_died(self):
|
def report_process_died(self):
|
||||||
|
@@ -31,6 +31,27 @@ class Progress(SubprocMessage):
|
|||||||
def __init__(self, progress: float):
|
def __init__(self, progress: float):
|
||||||
self.progress = progress
|
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):
|
class DownloadError(SubprocMessage):
|
||||||
"""Sent when there was an error downloading something."""
|
"""Sent when there was an error downloading something."""
|
||||||
@@ -48,6 +69,9 @@ class Aborted(SubprocMessage):
|
|||||||
"""Sent as response to Abort message."""
|
"""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:
|
def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> pathlib.Path:
|
||||||
"""Downloads the given package
|
"""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.')
|
log.warning('Server did not send content length, cannot report progress.')
|
||||||
content_length = float('inf')
|
content_length = float('inf')
|
||||||
|
|
||||||
|
# TODO: check if there's enough disk space.
|
||||||
|
|
||||||
# TODO: get filename from Content-Disposition header, if available.
|
# TODO: get filename from Content-Disposition header, if available.
|
||||||
# TODO: use urllib.parse to parse the URL.
|
# TODO: use urllib.parse to parse the URL.
|
||||||
local_filename = package_url.split('/')[-1] or 'download.tmp'
|
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.
|
# leave 30% "progress" for installation of the package.
|
||||||
pipe_to_blender.send(Progress(downloaded_length / content_length))
|
pipe_to_blender.send(Progress(downloaded_length / content_length))
|
||||||
|
|
||||||
pipe_to_blender.send(Success())
|
|
||||||
return local_fpath
|
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."""
|
"""Downloads and installs the given package."""
|
||||||
|
|
||||||
from . import cache
|
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.')
|
log.debug('Download failed/aborted, not going to install anything.')
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: actually install the package
|
# Only send success if _install doesn't throw an exception
|
||||||
log.warning('Installing is not actually implemented, install %s yourself.', downloaded)
|
# 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():
|
def debug_hang():
|
||||||
|
Reference in New Issue
Block a user