mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00
Refactor: Add manifest schema, unify install paths, use context manager
Sets the foundation for more elaborate additions to the manifest. Manifest files are still only being read for local imports, but with this commit that could be easily changed in the future.
This commit is contained in:
parent
2ed61c9c99
commit
8fceccf4b7
1 changed files with 57 additions and 39 deletions
|
@ -22,6 +22,12 @@ from anki.sync import AnkiRequestsClient
|
|||
class AddonManager:
|
||||
|
||||
ext = ".ankiaddon"
|
||||
# todo?: use jsonschema package
|
||||
_manifest_schema = {
|
||||
"package": {"type": str, "req": True, "meta": False},
|
||||
"name": {"type": str, "req": True, "meta": True},
|
||||
"mod": {"type": int, "req": False, "meta": True}
|
||||
}
|
||||
|
||||
def __init__(self, mw):
|
||||
self.mw = mw
|
||||
|
@ -102,42 +108,49 @@ When loading '%(name)s':
|
|||
# Installing and deleting add-ons
|
||||
######################################################################
|
||||
|
||||
def installDownload(self, sid, data, name):
|
||||
try:
|
||||
zfile = ZipFile(io.BytesIO(data))
|
||||
except zipfile.BadZipfile:
|
||||
showWarning(_("The download was corrupt. Please try again."))
|
||||
return False
|
||||
|
||||
mod = intTime()
|
||||
|
||||
self.install(sid, zfile, name, mod)
|
||||
|
||||
def installLocal(self, path):
|
||||
try:
|
||||
zfile = ZipFile(path)
|
||||
except zipfile.BadZipfile:
|
||||
return False, _("Corrupt add-on archive.")
|
||||
|
||||
def _readManifestFile(self, zfile):
|
||||
try:
|
||||
with zfile.open("manifest.json") as f:
|
||||
info = json.loads(f.read())
|
||||
package = info["package"]
|
||||
name = info["name"]
|
||||
mod = info.get("mod", None)
|
||||
assert isinstance(package, str) and isinstance(name, str)
|
||||
assert isinstance(mod, int) or mod is None
|
||||
data = json.loads(f.read())
|
||||
manifest = {} # build new manifest from recognized keys
|
||||
for key, attrs in self._manifest_schema.items():
|
||||
if not attrs["req"] and key not in data:
|
||||
continue
|
||||
val = data[key]
|
||||
assert isinstance(val, attrs["type"])
|
||||
manifest[key] = val
|
||||
except (KeyError, json.decoder.JSONDecodeError, AssertionError):
|
||||
# raised for missing manifest, invalid json, missing/invalid keys
|
||||
return False, _("Invalid add-on manifest.")
|
||||
return {}
|
||||
return manifest
|
||||
|
||||
self.install(package, zfile, name, mod)
|
||||
def install(self, file, manifest=None):
|
||||
"""Install add-on from path or file-like object. Metadata is read
|
||||
from the manifest file by default, but this may me bypassed
|
||||
by supplying a 'manifest' dictionary"""
|
||||
try:
|
||||
zfile = ZipFile(file)
|
||||
except zipfile.BadZipfile:
|
||||
return False, "zip"
|
||||
|
||||
return name, None
|
||||
with zfile:
|
||||
manifest = manifest or self._readManifestFile(zfile)
|
||||
if not manifest:
|
||||
return False, "manifest"
|
||||
package = manifest["package"]
|
||||
meta = self.addonMeta(package)
|
||||
self._install(package, zfile)
|
||||
|
||||
def install(self, dir, zfile, name, mod):
|
||||
schema = self._manifest_schema
|
||||
manifest_meta = {k: v for k, v in manifest.items()
|
||||
if k in schema and schema[k]["meta"]}
|
||||
meta.update(manifest_meta)
|
||||
self.writeAddonMeta(package, meta)
|
||||
|
||||
return True, meta["name"]
|
||||
|
||||
def _install(self, dir, zfile):
|
||||
# previously installed?
|
||||
meta = self.addonMeta(dir)
|
||||
base = self.addonsFolder(dir)
|
||||
if os.path.exists(base):
|
||||
self.backupUserFiles(dir)
|
||||
|
@ -158,12 +171,6 @@ When loading '%(name)s':
|
|||
continue
|
||||
zfile.extract(n, base)
|
||||
|
||||
# update metadata
|
||||
meta['name'] = name
|
||||
if mod is not None: # allow packages to skip updating mod
|
||||
meta['mod'] = mod
|
||||
self.writeAddonMeta(dir, meta)
|
||||
|
||||
def deleteAddon(self, dir):
|
||||
send2trash(self.addonsFolder(dir))
|
||||
|
||||
|
@ -176,12 +183,16 @@ When loading '%(name)s':
|
|||
self.mw.progress.start(immediate=True)
|
||||
for path in paths:
|
||||
base = os.path.basename(path)
|
||||
name, error = self.installLocal(path)
|
||||
if error:
|
||||
ret = self.install(path)
|
||||
if ret[0] is False:
|
||||
if ret[1] == "zip":
|
||||
msg = _("Corrupt add-on file.")
|
||||
elif ret[1] == "manifest":
|
||||
msg = _("Invalid add-on manifest.")
|
||||
errs.append(_("Error installing <i>%(base)s</i>: %(error)s"
|
||||
% dict(base=base, error=error)))
|
||||
% dict(base=base, error=msg)))
|
||||
else:
|
||||
log.append(_("Installed %(name)s" % dict(name=name)))
|
||||
log.append(_("Installed %(name)s" % dict(name=ret[1])))
|
||||
self.mw.progress.finish()
|
||||
return log, errs
|
||||
|
||||
|
@ -200,7 +211,14 @@ When loading '%(name)s':
|
|||
data, fname = ret
|
||||
fname = fname.replace("_", " ")
|
||||
name = os.path.splitext(fname)[0]
|
||||
self.installDownload(str(n), data, name)
|
||||
ret = self.install(io.BytesIO(data),
|
||||
manifest={"package": str(n), "name": name,
|
||||
"mod": intTime()})
|
||||
if ret[0] is False:
|
||||
if ret[1] == "zip":
|
||||
showWarning(_("The download was corrupt. Please try again."))
|
||||
elif ret[1] == "manifest":
|
||||
showWarning(_("Invalid add-on manifest."))
|
||||
log.append(_("Downloaded %(fname)s" % dict(fname=name)))
|
||||
self.mw.progress.finish()
|
||||
return log, errs
|
||||
|
|
Loading…
Reference in a new issue