Skip to content

Commit

Permalink
Add plugin download system in backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Wrench56 committed Jul 8, 2024
1 parent cd08357 commit 4a07a02
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 21 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 18 additions & 12 deletions src/backend/plugins/downloader.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
33 changes: 33 additions & 0 deletions src/backend/plugins/handler.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions src/backend/plugins/priority.py
Original file line number Diff line number Diff line change
@@ -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])
51 changes: 42 additions & 9 deletions src/backend/plugins/unpack.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -23,30 +34,52 @@ 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)

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}')
3 changes: 3 additions & 0 deletions src/backend/plugins/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# TODO: Validate plugins TOML
def validate_toml(toml: dict) -> bool:
return True
4 changes: 4 additions & 0 deletions src/backend/utils/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit 4a07a02

Please sign in to comment.