diff --git a/aqt/addons.py b/aqt/addons.py index 5a3868245..2f8bf51ef 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -10,7 +10,7 @@ from send2trash import send2trash from aqt.qt import * from aqt.utils import showInfo, openFolder, isWin, openLink, \ - askUser, restoreGeom, saveGeom, showWarning, tooltip + askUser, restoreGeom, saveGeom, showWarning, tooltip, getFile from zipfile import ZipFile import aqt.forms import aqt @@ -100,27 +100,52 @@ When loading '%(name)s': # Installing and deleting add-ons ###################################################################### - def install(self, sid, data, fname): + def installDownload(self, sid, data, name): try: - z = ZipFile(io.BytesIO(data)) + zfile = ZipFile(io.BytesIO(data)) except zipfile.BadZipfile: showWarning(_("The download was corrupt. Please try again.")) - return + return False + + mod = intTime() + + self.install(sid, zfile, name, mod) - name = os.path.splitext(fname)[0] + def installLocal(self, path): + try: + zfile = ZipFile(path) + except zipfile.BadZipfile: + return False, _("Corrupt add-on archive.") + + 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 + except (KeyError, json.decoder.JSONDecodeError, AssertionError): + # raised for missing manifest, invalid json, missing/invalid keys + return False, _("Invalid add-on manifest.") + + self.install(package, zfile, name, mod) + return name, None + + def install(self, dir, zfile, name, mod): # previously installed? - meta = self.addonMeta(sid) - base = self.addonsFolder(sid) + meta = self.addonMeta(dir) + base = self.addonsFolder(dir) if os.path.exists(base): - self.backupUserFiles(sid) - self.deleteAddon(sid) + self.backupUserFiles(dir) + self.deleteAddon(dir) os.mkdir(base) - self.restoreUserFiles(sid) + self.restoreUserFiles(dir) # extract - for n in z.namelist(): + for n in zfile.namelist(): if n.endswith("/"): # folder; ignore continue @@ -129,16 +154,35 @@ When loading '%(name)s': # skip existing user files if os.path.exists(path) and n.startswith("user_files/"): continue - z.extract(n, base) + zfile.extract(n, base) # update metadata meta['name'] = name - meta['mod'] = intTime() - self.writeAddonMeta(sid, meta) + 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)) + # Processing local add-on files + ###################################################################### + + def processAPKX(self, paths): + log = [] + errs = [] + self.mw.progress.start(immediate=True) + for path in paths: + base = os.path.basename(path) + name, error = self.installLocal(path) + if error: + errs.append(_("Error installing %(base)s: %(error)s" + % dict(base=base, error=error))) + else: + log.append(_("Installed %(name)s" % dict(name=name))) + self.mw.progress.finish() + return log, errs + # Downloading ###################################################################### @@ -153,8 +197,8 @@ When loading '%(name)s': continue data, fname = ret fname = fname.replace("_", " ") - self.install(str(n), data, fname) name = os.path.splitext(fname)[0] + self.installDownload(str(n), data, name) log.append(_("Downloaded %(fname)s" % dict(fname=name))) self.mw.progress.finish() return log, errs @@ -296,6 +340,7 @@ class AddonsDialog(QDialog): f = self.form = aqt.forms.addons.Ui_Dialog() f.setupUi(self) f.getAddons.clicked.connect(self.onGetAddons) + f.installFromFile.clicked.connect(self.onInstallFiles) f.checkForUpdates.clicked.connect(self.onCheckForUpdates) f.toggleEnabled.clicked.connect(self.onToggleEnabled) f.viewPage.clicked.connect(self.onViewPage) @@ -385,6 +430,24 @@ class AddonsDialog(QDialog): def onGetAddons(self): GetAddons(self) + def onInstallFiles(self, paths=None): + if not paths: + key = (_("Packaged Anki Add-on") + " (*.apkx)") + paths = getFile(self, _("Install Add-on(s)"), None, key, + key="addons", multi=True) + if not paths: + return False + + log, errs = self.mgr.processAPKX(paths) + + if log: + tooltip("
".join(log), parent=self) + if errs: + msg = _("Please report this to the respective add-on author(s).") + showWarning("

".join(errs + [msg]), parent=self) + + self.redrawAddons() + def onCheckForUpdates(self): updated = self.mgr.checkForUpdates() if not updated: diff --git a/designer/addons.ui b/designer/addons.ui index eb7144608..0c620349c 100644 --- a/designer/addons.ui +++ b/designer/addons.ui @@ -47,6 +47,13 @@ + + + + Install from file... + + +