diff --git a/githubplugin/__init__.py b/githubplugin/__init__.py index 94859c6f1..dc226016a 100644 --- a/githubplugin/__init__.py +++ b/githubplugin/__init__.py @@ -26,6 +26,7 @@ ######################################################################### import os +from shutil import rmtree from pathlib import Path from lib.model.smartplugin import SmartPlugin @@ -269,9 +270,6 @@ class GithubPlugin(SmartPlugin): shng plugins fork and branch, and then setting up a local repo containing that fork. Additionally, the specified plugin will be soft-linked into the "live" plugins repo worktree as a private plugin. - - At the moment, this is a standalone demonstrator, which will be transformed - into a SmartPlugin later. """ PLUGIN_VERSION = '1.0.0' @@ -297,7 +295,8 @@ def __init__(self, sh): # 'link': os.path.join('plugins', f'priv_{plugin}'), # relativer Pfad-/Dateiname des Plugin-Symlinks unterhalb von shng # 'rel_link_path': os.path.join(wt_path, plugin), # relativer Pfad des Pluginordners "unterhalb" von plugins/ # 'force': False, # vorhandene Dateien überschreiben - # 'repo': repo # git.Repo(path) + # 'repo': repo, # git.Repo(path) + # 'dirty': bool # repo is dirty? # }, # '': {...} # } @@ -379,7 +378,8 @@ def read_repos_from_dir(self): 'link': os.path.join('plugins', f'priv_{plugin}'), 'rel_link_path': os.path.join(wt_path, plugin), 'force': False, - 'repo': repo + 'repo': repo, + 'dirty': repo.is_dirty() } # add missing ids to repoitem @@ -559,6 +559,67 @@ def create_repo(self, name) -> bool: return True + def remove_plugin(self, name) -> bool: + """ remove plugin link, worktree and if not longer needed, local repo """ + if name not in self.repos: + self.logger.warning(f'plugin entry {name} not found.') + return False + + # get all data to remove + repo = self.repos[name] + link_path = repo['link'] + wt_path = repo['full_wt_path'] + repo_path = repo['full_repo_path'] + owner = repo['owner'] + # check if repo is used by other plugins + last = True + for r in self.repos: + if r == name: + continue + if self.repos[r]["owner"] == owner: + last = False + break + + err = [] + try: + self.logger.debug(f'removing link {link_path}') + os.remove(link_path) + except Exception as e: + err.append(e) + try: + self.logger.debug(f'removing worktree {wt_path}') + rmtree(wt_path) + except Exception as e: + err.append(e) + try: + self.logger.debug('pruning worktree') + repo['repo'].git.worktree('prune') + except Exception as e: + err.append(e) + if last: + try: + self.logger.debug(f'repo {repo_path} is no longer used, removing') + rmtree(repo_path) + except Exception as e: + err.append(e) + + # remove repo entry from plugin dict + del self.repos[name] + del repo + + # remove repo entry from repoitem + if self._repoitem is not None: + try: + self._repoitem.dict.delete(wt_path) + except Exception as e: + err.append(e) + + if err: + self.logger.warning(f'error(s) occurred while removing plugin: {", ".join(err)}') + return False + + return True + # # github API methods # diff --git a/githubplugin/requirements.txt b/githubplugin/requirements.txt new file mode 100644 index 000000000..b77965513 --- /dev/null +++ b/githubplugin/requirements.txt @@ -0,0 +1,2 @@ +GitPython +PyGithub \ No newline at end of file diff --git a/githubplugin/user_doc.rst b/githubplugin/user_doc.rst index 86882edc0..b0cd38500 100644 --- a/githubplugin/user_doc.rst +++ b/githubplugin/user_doc.rst @@ -1,91 +1,47 @@ -.. index:: Plugins; Pluginname (in Kleinbuchstaben) -.. index:: Pluginname (in Kleinbuchstaben) +.. index:: Plugins; githubplugin +.. index:: githubplugin -=============================== -Pluginname (in Kleinbuchstaben) -=============================== +============ +githubplugin +============ -.. comment set image name and extension according to the image file you use for the plugin-logo +Wenn man das Plugin eines anderen Autors ausprobieren oder testen möchte, muss es aus einem fremden Repository von GitHub in die eigene Installation eingebunden werden. -.. image:: webif/static/img/plugin_logo.png - :alt: plugin logo - :width: 300px - :height: 300px - :scale: 50 % - :align: left +Dieses Plugin ermöglicht es komfortabel, fremde Plugins von GitHub zu installieren und wieder zu deinstallieren. - +Auch wenn die Funktionen des Plugins grundsätzlich über Logiken genutzt werden können, erfolgt die Bedienung grundsätzlich über das pluginspezifische Webinterface, das über die Admin-UI von SmartHomeNG zugänglich ist. +Dort können Plugins angezeigt, installiert und entfernt werden. Das Löschen von installierten Plugins, deren git-Verzeichnisse nicht "sauber" sind (veränderte, gelöschte oder hinzugefügte Dateien im git-Index), können nicht über die Weboberfläche entfernt werden. Diese Änderungen müssen erst von Hand rückgängig gemacht oder per commit/push gesichert werden. + +Vorsicht: Wenn Änderungen an Fremd-Plugins per `git commit` in den Index übernommen wurden, aber nicht per push oder Pull-Request an GitHub gesendet wurden, können diese beim Löschen des Plugins ggf. unwiderruflich verloren gehen. Anforderungen ============= -... - Notwendige Software ------------------- - - -Unterstützte Geräte -------------------- - - +Das Plugin benötigt die Python-Pakete GitPython und PyGithub. Konfiguration ============= -.. comment Den Text **Pluginname (in Kleinbuchstaben)** durch :doc:`/plugins_doc/config/pluginname` ersetzen - -Die Plugin Parameter, die Informationen zur Item-spezifischen Konfiguration des Plugins und zur Logik-spezifischen -Konfiguration sind unter **Pluginname (in Kleinbuchstaben)** beschrieben. - -Dort findet sich auch die Dokumentation zu Funktionen, die das Plugin evtl. bereit stellt. - - -Funktionen ----------- - - - - - -| - -Beispiele -========= - -Hier können bei Bedarf Konfigurationsbeispiele dokumentiert werden. - -| - -Web Interface -============= - - - -Tab 1: ----------------------- +Das Plugin ist ohne Konfiguration lauffähig. - +Optional kann ein GitHub-API-Key hinterlegt werden, um die Anzahl der möglichen GitHub-Zugriffe zu erhöhen. -.. image:: assets/webif_tab1.jpg - :class: screenshot +Die installiereten Fremd-Plugins werden über den Besitzer des GitHub-Repositories, den jeweiligen branch und den Pluginnamen identifiziert. Dazu wird ein Bezeichner nach dem Format `/-` erstellt. Alternativ kann vom Benutzer ein eigener Bezeichner frei gewählt werden. Um diese Bezeichner dauerhaft zu sichern, kann ein Item vom Typ `dict` bestimmt werden. Dieses benötigt nur die Item-Attribute - +Die Plugin Parameter und die Informationen zur Item-spezifischen Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/githubplugin` beschrieben. +Dort findet sich auch die Dokumentation zu Funktionen, die das Plugin evtl. bereit stellt. diff --git a/githubplugin/webif/__init__.py b/githubplugin/webif/__init__.py index ff4679132..dbaf92ea1 100644 --- a/githubplugin/webif/__init__.py +++ b/githubplugin/webif/__init__.py @@ -56,7 +56,7 @@ def __init__(self, webif_dir, plugin): self.tplenv = self.init_template_environment() @cherrypy.expose - def index(self): + def index(self, action=None): """ Build index.html for cherrypy @@ -64,6 +64,10 @@ def index(self): :return: contents of the template after beeing rendered """ + if action == 'rescan': + self.plugin.read_repos_from_dir() + raise cherrypy.HTTPRedirect(cherrypy.url()) + # try to get the webif pagelength from the module.yaml configuration pagelength = self.plugin.get_parameter_value('webif_pagelength') @@ -119,6 +123,20 @@ def updatePlugins(self): if plugins != {}: return {"operation": "request", "result": "success", "data": plugins} + @cherrypy.expose + @cherrypy.tools.json_out() + @cherrypy.tools.json_in() + def removePlugin(self): + json = cherrypy.request.json + name = json.get("name") + if name is None or name == '' or name not in self.plugin.repos: + msg = f'Repo {name} nicht vorhanden.' + self.logger.error(msg) + return {"operation": "request", "result": "error", "data": msg} + + if self.plugin.remove_plugin(name): + return {"operation": "request", "result": "success"} + @cherrypy.expose @cherrypy.tools.json_out() @cherrypy.tools.json_in() diff --git a/githubplugin/webif/templates/index.html b/githubplugin/webif/templates/index.html index 8443e8eb9..dd024a585 100644 --- a/githubplugin/webif/templates/index.html +++ b/githubplugin/webif/templates/index.html @@ -13,6 +13,7 @@ {% block buttons %} + {% endblock buttons %} {% block headtable %} @@ -23,12 +24,30 @@ Installieren von Plugin aus dem Repo /plugins, Branch
- Soll das Plugin wirklich installiert werden?
+ Soll das Plugin wirklich installiert werden?
- Befehl wird ausgeführt. Bitte etwas Geduld... + Installation wird ausgeführt. Bitte Geduld, dies kann etwas dauern... +
+ + + +
+
+ × +
+ Entfernen von Plugin aus dem Repo /plugins, Branch + +
+
+ + Soll das Plugin wirklich gelöscht werden?
+ + +
+ Löschen wird ausgeführt. Bitte Geduld, dies kann etwas dauern...
@@ -116,7 +135,6 @@ var pulls = {{ pulls }}; - function clearSelect(sel) { var i, L = sel.options.length - 1; for (i = L; i > 0; i--) { @@ -133,27 +151,33 @@ sel.add(option); } - function showModal(owner, branch, plugin) { - document.getElementById('doplugin').textContent = plugin; - document.getElementById('doowner').textContent = owner; - document.getElementById('dobranch').textContent = branch; - document.getElementById('modalButtons').style.display = 'block'; - document.getElementById('modalSpinner').style.display = 'none'; - document.getElementById('pluginModal').style.display = 'block'; + function showModal(owner, branch, plugin, nr, name) { + document.getElementById('doplugin' + nr).textContent = plugin; + document.getElementById('doowner' + nr).textContent = owner; + document.getElementById('dobranch' + nr).textContent = branch; + document.getElementById('modalButtons' + nr).style.display = 'block'; + document.getElementById('modalSpinner' + nr).style.display = 'none'; + document.getElementById('pluginModal' + nr).style.display = 'block'; + if (nr == '2') { + document.getElementById('doname2').textContent = name; + } } - function hideModal() { - document.getElementById('pluginModal').style.display = 'none'; - document.getElementById('doplugin').textContent = ''; - document.getElementById('doowner').textContent = ''; - document.getElementById('dobranch').textContent = ''; - document.getElementById('modalButtons').style.display = 'block'; - document.getElementById('modalSpinner').style.display = 'none'; + function hideModal(nr) { + document.getElementById('pluginModal' + nr).style.display = 'none'; + document.getElementById('doplugin' + nr).textContent = ''; + document.getElementById('doowner' + nr).textContent = ''; + document.getElementById('dobranch' + nr).textContent = ''; + document.getElementById('modalButtons' + nr).style.display = 'block'; + document.getElementById('modalSpinner' + nr).style.display = 'none'; + if (nr == 2) { + document.getElementById('doname2').textContent = ''; + } } - function spinModal() { - document.getElementById('modalButtons').style.display = 'none'; - document.getElementById('modalSpinner').style.display = 'block'; + function spinModal(nr) { + document.getElementById('modalButtons' + nr).style.display = 'none'; + document.getElementById('modalSpinner' + nr).style.display = 'block'; } function selectedPR(selObj) { @@ -290,7 +314,7 @@ alert("Fehler beim Initialisieren: " + response['data']) }, success: function(response) { - showModal(owner, branch, plugin); + showModal(owner, branch, plugin, ''); } }) } @@ -301,9 +325,13 @@ var branch = document.getElementById('branch').value; var plugin = document.getElementById('plugin').value; var name = document.getElementById('name').value; + doInstallPlugin(owner, branch, plugin, name); + } + function doInstallPlugin(owner, branch, plugin, name) { + showModal(owner, branch, plugin, ''); if (owner != '' && branch != '' && plugin != '') { - spinModal(); + spinModal(''); $.ajax({ type: "POST", url: "selectPlugin", @@ -314,13 +342,34 @@ alert("Fehler beim Installieren: " + response['data']) }, success: function(response) { - hideModal(); + hideModal(''); setTimeout(window.location.reload(), 300); } }) } } + function removePlugin(selObj) { + var name = document.getElementById('doname2').textContent; + if (name != '') { + spinModal('2'); + $.ajax({ + type: "POST", + url: "removePlugin", + data: JSON.stringify({'name': name}), + contentType: 'application/json', + dataType: 'json', + error: function(response) { + alert("Fehler beim Entfernen: " + response['data']) + }, + success: function(response) { + hideModal('2'); + // setTimeout(window.location.reload(), 300); + } + }) + } + } + @@ -339,6 +388,7 @@ Autor Branch Pfad (Worktree) + Aktion @@ -350,6 +400,7 @@ {{ repos[plugin].owner }} {{ repos[plugin].branch }} {{ repos[plugin].full_wt_path }} + {% if repos[plugin].dirty %}Änderungen vorhanden{% else %}{% endif %} {% endfor %} @@ -371,6 +422,7 @@ Autor Branch Pfad (Worktree) + @@ -382,6 +434,8 @@ {{ init_repos[plugin].owner }} {{ init_repos[plugin].branch }} {{ init_repos[plugin].full_wt_path }} + + {% endfor %}