From 4a07a0224acf0acf184d4651089273b4982cb062 Mon Sep 17 00:00:00 2001 From: Wrench56 Date: Mon, 8 Jul 2024 02:48:59 -0400 Subject: [PATCH] Add plugin download system in `backend` --- .gitignore | 8 +++++ src/backend/plugins/downloader.py | 30 ++++++++++-------- src/backend/plugins/handler.py | 33 ++++++++++++++++++++ src/backend/plugins/priority.py | 39 +++++++++++++++++++++++ src/backend/plugins/unpack.py | 51 +++++++++++++++++++++++++------ src/backend/plugins/validate.py | 3 ++ src/backend/utils/const.py | 4 +++ 7 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 src/backend/plugins/handler.py create mode 100644 src/backend/plugins/priority.py create mode 100644 src/backend/plugins/validate.py diff --git a/.gitignore b/.gitignore index f123fc1..01ea83c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,11 @@ src/backend/plugins/plugins # Ignore built frontend public + +# Ignore plugin pages +src/frontend/src/routes/plugins/** +!src/frontend/src/routes/plugins/+layout.ts +!src/frontend/src/routes/plugins/+page.svelte + +# Ignore plugin components +src/frontend/src/lib/plugins diff --git a/src/backend/plugins/downloader.py b/src/backend/plugins/downloader.py index 7455f1c..76bffb9 100644 --- a/src/backend/plugins/downloader.py +++ b/src/backend/plugins/downloader.py @@ -1,38 +1,44 @@ from typing import Optional -from plugins import unpack -from utils.const import PLUGINS_DIR, PLUGINS_DOWNLOAD +from plugins import handler, priority, unpack, validate +from utils.const import PLUGINS_DOWNLOAD -from configparser import ConfigParser import logging +import tomllib import requests def from_url(url: str) -> bool: - plugin_ini = _download_plugin_ini(url) - if plugin_ini is None: + plugin_toml = _download_plugin_toml(url) + if plugin_toml is None: return False - config = ConfigParser() - config.read_string(plugin_ini) - name = config['package'].get('name') - zip_url = config['package'].get('zip_url') + config = tomllib.loads(plugin_toml) + if not validate.validate_toml(config): + return False + + name = config['plugin'].get('name').replace('-', '_') + zip_url = config['plugin'].get('zip_url') if not _download_plugin_zip(zip_url, name): return False if not unpack.unzip(name): return False - if not unpack.unpack(f'{PLUGINS_DIR}/{name}', 'plugin.ini'): + if not unpack.unpack(name, 'Plugin.toml'): + return False + if not unpack.distribute(name): return False + priority.add_new_plugin(name, 2) + handler.load(name) return True -def _download_plugin_ini(url: str) -> Optional[str]: +def _download_plugin_toml(url: str) -> Optional[str]: res = requests.get(url, timeout=10) if not res.ok: - logging.error(f'Can\'t download plugin.ini at "{url}"') + logging.error(f'Can\'t download Plugin.toml at "{url}"') return None return res.text diff --git a/src/backend/plugins/handler.py b/src/backend/plugins/handler.py new file mode 100644 index 0000000..516febc --- /dev/null +++ b/src/backend/plugins/handler.py @@ -0,0 +1,33 @@ +from typing import Optional + +import importlib +import logging + +from plugins.base_plugin import Plugin +from plugins import priority + + +def load_all() -> None: + for plugin_name, prio in priority.fetch_plugins(): + logging.info(f'Loading plugin "{plugin_name}" with priority {prio}') + plugin = load(plugin_name) + if plugin is not None: + plugin.load() + + +def load(name: str) -> Optional[Plugin]: + try: + source = f'plugins.plugins.{name}.backend.main' + plugin: Plugin = importlib.import_module(source).init() + return plugin + except TypeError: + # Abstract class (Plugin) does not implement methods like load & unload + logging.error(f'Plugin "{name}" does not implement abstract methods') + except AttributeError: + # No init() function + logging.error(f'Plugin "{name}" does not provide an init() function') + except ModuleNotFoundError: + # No such file + logging.error(f'Plugin "{name}" does not exist') + + return None diff --git a/src/backend/plugins/priority.py b/src/backend/plugins/priority.py new file mode 100644 index 0000000..3d6fa13 --- /dev/null +++ b/src/backend/plugins/priority.py @@ -0,0 +1,39 @@ +from typing import Generator, List, Tuple + +from utils.const import PLUGINS_DIR + +_PRIORITIES: List[Tuple[str, int]] = [] + + +def fetch_plugins() -> Generator[Tuple[str, int], None, None]: + with open(f'{PLUGINS_DIR}/priorities.csv', 'r', encoding='utf-8') as f: + name, priority = f.readline().strip().split(',') + _PRIORITIES.append((name, int(priority))) + yield (name, int(priority)) + f.close() + + +def add_new_plugin(name: str, priority: int) -> None: + for pp_pair in _PRIORITIES: + if pp_pair[0] == name: + return + _PRIORITIES.append((name, priority)) + _sort_priorities() + _save_priorities() + + +def change_plugin_priority(name: str, priority: int) -> None: + for i, pp_pair in enumerate(_PRIORITIES): + if pp_pair[0] == name: + _PRIORITIES[i] = (name, priority) + + +def _save_priorities() -> None: + with open(f'{PLUGINS_DIR}/priorities.csv', 'w', encoding='utf-8') as f: + for prio in _PRIORITIES: + f.write(f'{prio[0]},{prio[1]}') + f.close() + + +def _sort_priorities() -> None: + _PRIORITIES.sort(key=lambda o: o[1]) diff --git a/src/backend/plugins/unpack.py b/src/backend/plugins/unpack.py index db2c396..46ac604 100644 --- a/src/backend/plugins/unpack.py +++ b/src/backend/plugins/unpack.py @@ -1,6 +1,11 @@ from typing import Optional -from utils.const import PLUGINS_DIR, PLUGINS_DOWNLOAD +from utils.const import ( + FRONTEND_PAGES_DIR, + FRONTEND_PLUGINS_DIR, + PLUGINS_DIR, + PLUGINS_DOWNLOAD, +) import logging import os @@ -9,12 +14,18 @@ def unzip(name: str) -> bool: + _clear_prev_installation(name) with zipfile.ZipFile(f'{PLUGINS_DOWNLOAD}/{name}.zip', 'r') as zip_ref: zip_ref.extractall(f'{PLUGINS_DIR}/{name}') return True +def _clear_prev_installation(name: str) -> None: + if os.path.exists(f'{PLUGINS_DIR}/{name}'): + shutil.rmtree(f'{PLUGINS_DIR}/{name}') + + def _find_target_file(root_dir: str, target: str) -> Optional[str]: for dirpath, _, filenames in os.walk(root_dir): if target in filenames: @@ -23,15 +34,13 @@ def _find_target_file(root_dir: str, target: str) -> Optional[str]: def _move_contents(target_folder, root_dir) -> bool: - root_parent_folder = os.path.dirname(root_dir) for item in os.listdir(target_folder): source = os.path.join(target_folder, item) - destination = os.path.join(root_parent_folder, item) - + destination = os.path.join(root_dir, item) if os.path.isdir(source): shutil.move(source, destination) else: - shutil.move(source, root_parent_folder) + shutil.move(source, root_dir) # Clean up the now empty target_folder os.rmdir(target_folder) @@ -39,14 +48,38 @@ def _move_contents(target_folder, root_dir) -> bool: return True -def unpack(root_dir: str, target: str): +def unpack(plugin_name: str, target: str) -> bool: + root_dir = f'{PLUGINS_DIR}/{plugin_name}' target_folder = _find_target_file(root_dir, target) if target_folder: logging.info(f'Found {target} in {target_folder}') _move_contents(target_folder, root_dir) - # Remove the root folder after moving contents - shutil.rmtree(root_dir) logging.info('Plugin unpacked') + return True + + logging.error(f'{target} not found in the plugin folder') + return False + + +def distribute(plugin_name: str) -> bool: + # Make sure previous installations are deleted + _clear_frontend_installation(plugin_name) + root_dir = f'{PLUGINS_DIR}/{plugin_name}' + if os.path.exists(f'{root_dir}/frontend'): + shutil.move(f'{root_dir}/frontend', f'{FRONTEND_PLUGINS_DIR}/{plugin_name}') else: - logging.error(f'{target} not found in the plugin folder') + logging.warning(f'Frontend directorty of plugin "{plugin_name}" does not exist') + + if os.path.exists(f'{root_dir}/pages'): + shutil.move(f'{root_dir}/pages', f'{FRONTEND_PAGES_DIR}/{plugin_name}') + else: + logging.warning(f'Pages directory of plugin "{plugin_name}" does not exist') + return True + + +def _clear_frontend_installation(name: str) -> None: + if os.path.exists(f'{FRONTEND_PLUGINS_DIR}/{name}'): + shutil.rmtree(f'{FRONTEND_PLUGINS_DIR}/{name}') + if os.path.exists(f'{FRONTEND_PAGES_DIR}/{name}'): + shutil.rmtree(f'{FRONTEND_PAGES_DIR}/{name}') diff --git a/src/backend/plugins/validate.py b/src/backend/plugins/validate.py new file mode 100644 index 0000000..3b0d133 --- /dev/null +++ b/src/backend/plugins/validate.py @@ -0,0 +1,3 @@ +# TODO: Validate plugins TOML +def validate_toml(toml: dict) -> bool: + return True diff --git a/src/backend/utils/const.py b/src/backend/utils/const.py index 6611411..89335da 100644 --- a/src/backend/utils/const.py +++ b/src/backend/utils/const.py @@ -3,3 +3,7 @@ _plugins: str = './plugins' PLUGINS_DIR: str = f'{_plugins}/plugins' PLUGINS_DOWNLOAD: str = f'{_plugins}/downloads' + +_frontend: str = '../frontend' +FRONTEND_PLUGINS_DIR = f'{_frontend}/src/lib/plugins' +FRONTEND_PAGES_DIR = f'{_frontend}/src/routes/plugins'