Refactor add-on installation error handling

Allows extending the installation pathways more easily.
In preparation of .ankiaddon file type handling.
This commit is contained in:
Glutanimate 2020-01-03 16:32:20 +01:00
parent 5edf901c16
commit ce1853167b

View file

@ -3,10 +3,11 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import io import io
import json import json
import os
import re import re
import zipfile import zipfile
from collections import defaultdict from collections import defaultdict
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
from zipfile import ZipFile from zipfile import ZipFile
import jsonschema import jsonschema
@ -37,6 +38,13 @@ from aqt.utils import (
) )
class AddonInstallationResult(NamedTuple):
success: bool
errmsg: Optional[str] = None
name: Optional[str] = None
conflicts: Optional[List[str]] = None
class AddonManager: class AddonManager:
ext = ".ankiaddon" ext = ".ankiaddon"
@ -201,14 +209,14 @@ and have been disabled: %(found)s"
return {} return {}
return manifest return manifest
def install(self, file, manifest=None): def install(self, file, manifest=None) -> AddonInstallationResult:
"""Install add-on from path or file-like object. Metadata is read """Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest' from the manifest file, with keys overriden by supplying a 'manifest'
dictionary""" dictionary"""
try: try:
zfile = ZipFile(file) zfile = ZipFile(file)
except zipfile.BadZipfile: except zipfile.BadZipfile:
return False, "zip" return AddonInstallationResult(success=False, errmsg="zip")
with zfile: with zfile:
file_manifest = self.readManifestFile(zfile) file_manifest = self.readManifestFile(zfile)
@ -216,7 +224,7 @@ and have been disabled: %(found)s"
file_manifest.update(manifest) file_manifest.update(manifest)
manifest = file_manifest manifest = file_manifest
if not manifest: if not manifest:
return False, "manifest" return AddonInstallationResult(success=False, errmsg="manifest")
package = manifest["package"] package = manifest["package"]
conflicts = manifest.get("conflicts", []) conflicts = manifest.get("conflicts", [])
found_conflicts = self._disableConflicting(package, conflicts) found_conflicts = self._disableConflicting(package, conflicts)
@ -230,7 +238,9 @@ and have been disabled: %(found)s"
meta.update(manifest_meta) meta.update(manifest_meta)
self.writeAddonMeta(package, meta) self.writeAddonMeta(package, meta)
return True, meta["name"], found_conflicts return AddonInstallationResult(
success=True, name=meta["name"], conflicts=found_conflicts
)
def _install(self, dir, zfile): def _install(self, dir, zfile):
# previously installed? # previously installed?
@ -274,40 +284,30 @@ and have been disabled: %(found)s"
# Processing local add-on files # Processing local add-on files
###################################################################### ######################################################################
def processPackages(self, paths): def processPackages(self, paths) -> Tuple[List[str], List[str]]:
log = [] log = []
errs = [] errs = []
self.mw.progress.start(immediate=True) self.mw.progress.start(immediate=True)
try: try:
for path in paths: for path in paths:
base = os.path.basename(path) base = os.path.basename(path)
ret = self.install(path) result = self.install(path)
if ret[0] is False:
if ret[1] == "zip": if not result.success:
msg = _("Corrupt add-on file.") errs.extend(
elif ret[1] == "manifest": self._installationErrorReport(result, base, mode="local")
msg = _("Invalid add-on manifest.")
else:
msg = "Unknown error: {}".format(ret[1])
errs.append(
_(
"Error installing <i>%(base)s</i>: %(error)s"
% dict(base=base, error=msg)
)
) )
else: else:
log.append(_("Installed %(name)s" % dict(name=ret[1]))) log.extend(
if ret[2]: self._installationSuccessReport(result, base, mode="local")
log.append(
_("The following conflicting add-ons were disabled:")
+ " "
+ " ".join(ret[2])
) )
finally: finally:
self.mw.progress.finish() self.mw.progress.finish()
return log, errs return log, errs
# Downloading # Downloading add-ons from AnkiWeb
###################################################################### ######################################################################
def downloadIds(self, ids): def downloadIds(self, ids):
@ -324,32 +324,67 @@ and have been disabled: %(found)s"
data, fname = ret data, fname = ret
fname = fname.replace("_", " ") fname = fname.replace("_", " ")
name = os.path.splitext(fname)[0] name = os.path.splitext(fname)[0]
ret = self.install( result = self.install(
io.BytesIO(data), io.BytesIO(data),
manifest={"package": str(n), "name": name, "mod": intTime()}, manifest={"package": str(n), "name": name, "mod": intTime()},
) )
if ret[0] is False: if not result.success:
if ret[1] == "zip": errs.extend(self._installationErrorReport(result, n))
msg = _("Corrupt add-on file.")
elif ret[1] == "manifest":
msg = _("Invalid add-on manifest.")
else: else:
msg = "Unknown error: {}".format(ret[1]) log.extend(self._installationSuccessReport(result, n))
errs.append(
_("Error downloading %(id)s: %(error)s") % dict(id=n, error=msg)
)
else:
log.append(_("Downloaded %(fname)s" % dict(fname=name)))
if ret[2]:
log.append(
_("The following conflicting add-ons were disabled:")
+ " "
+ " ".join(ret[2])
)
self.mw.progress.finish() self.mw.progress.finish()
return log, errs return log, errs
# Installation messaging
######################################################################
def _installationErrorReport(
self, result: AddonInstallationResult, base: str, mode="download"
) -> List[str]:
messages = {
"zip": _("Corrupt add-on file."),
"manifest": _("Invalid add-on manifest."),
}
if result.errmsg:
msg = messages.get(
result.errmsg, _("Unknown error: {}".format(result.errmsg))
)
else:
msg = _("Unknown error")
if mode == "download": # preserve old format strings for i18n
template = _("Error downloading <i>%(id)s</i>: %(error)s")
else:
template = _("Error installing <i>%(base)s</i>: %(error)s")
name = result.name or base
return [template % dict(base=name, id=name, error=msg)]
def _installationSuccessReport(
self, result: AddonInstallationResult, base: str, mode="download"
) -> List[str]:
if mode == "download": # preserve old format strings for i18n
template = _("Downloaded %(fname)s")
else:
template = _("Installed %(name)s")
name = result.name or base
strings = [template % dict(name=name, fname=name)]
if result.conflicts:
strings.append(
_("The following conflicting add-ons were disabled:")
+ " "
+ " ".join(result.conflicts)
)
return strings
# Updating # Updating
###################################################################### ######################################################################