Basic install function
This commit is contained in:
@@ -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