diff --git a/.gitignore b/.gitignore index bff7f5f..b3e84e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .idea .vscode .pytest_cache +.jupyter +*.ipynb +*.ipynb_checkpoints +*.tmp __pycache__ .kiauh-env *.code-workspace diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index ca9727f..1c17aac 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -353,10 +353,16 @@ def read_ports_from_nginx_configs() -> List[int]: lines = cfg.readlines() for line in lines: - line = line.replace("default_server", "") - line = re.sub(r"[;:\[\]]", "", line.strip()) - if line.startswith("listen") and line.split()[-1] not in port_list: - port_list.append(line.split()[-1]) + line = re.sub( + r"default_server|http://|https://|[;\[\]]", + "", + line.strip(), + ) + if line.startswith("listen"): + if ":" not in line: + port_list.append(line.split()[-1]) + else: + port_list.append(line.split(":")[-1]) ports_to_ints_list = [int(port) for port in port_list] return sorted(ports_to_ints_list, key=lambda x: int(x)) diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index d613ccf..ea75928 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -33,7 +33,7 @@ CURRENT_USER = pwd.getpwuid(os.getuid())[0] # dirs SYSTEMD = Path("/etc/systemd/system") -PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups") +PRINTER_DATA_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-data-backups") NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") NGINX_CONFD = Path("/etc/nginx/conf.d") diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index 60b1167..c44f555 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -58,12 +58,16 @@ class ExtensionsMenu(BaseMenu): module_path = f"kiauh.extensions.{ext.name}.{module_name}" # get the class name of the extension - ext_class: Type[BaseExtension] = inspect.getmembers( - importlib.import_module(module_path), - predicate=lambda o: inspect.isclass(o) - and issubclass(o, BaseExtension) - and o != BaseExtension, - )[0][1] + module = importlib.import_module(module_path) + + def predicate(o): + return ( + inspect.isclass(o) + and issubclass(o, BaseExtension) + and o != BaseExtension + ) + + ext_class: type = inspect.getmembers(module, predicate)[0][1] # instantiate the extension with its metadata and add to dict ext_instance: BaseExtension = ext_class(metadata) @@ -72,7 +76,7 @@ class ExtensionsMenu(BaseMenu): except (IOError, json.JSONDecodeError, ImportError) as e: print(f"Failed loading extension {ext}: {e}") - return dict(sorted(ext_dict.items())) + return dict(sorted(ext_dict.items(), key=lambda x: int(x[0]))) def extension_submenu(self, **kwargs): ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py index eb76fe4..d7ea5d4 100644 --- a/kiauh/extensions/octoapp/__init__.py +++ b/kiauh/extensions/octoapp/__init__.py @@ -14,7 +14,6 @@ OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git" # directories OA_DIR = Path.home().joinpath("octoapp") OA_ENV_DIR = Path.home().joinpath("octoapp-env") -OA_STORE_DIR = OA_DIR.joinpath("octoapp-store") # files OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") diff --git a/kiauh/extensions/octoapp/octoapp_extension.py b/kiauh/extensions/octoapp/octoapp_extension.py index c5d293d..3789145 100644 --- a/kiauh/extensions/octoapp/octoapp_extension.py +++ b/kiauh/extensions/octoapp/octoapp_extension.py @@ -10,6 +10,7 @@ import json from typing import List from components.moonraker.moonraker import Moonraker +from components.klipper.klipper import Klipper from core.instance_manager.instance_manager import InstanceManager from core.logger import DialogType, Logger from extensions.base_extension import BaseExtension @@ -131,6 +132,7 @@ class OctoappExtension(BaseExtension): try: self._remove_OA_instances(ob_instances) + self._remove_OA_store_dirs() self._remove_OA_dir() self._remove_OA_env() remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances) @@ -181,6 +183,21 @@ class OctoappExtension(BaseExtension): run_remove_routines(OA_DIR) + + def _remove_OA_store_dirs(self) -> None: + Logger.print_status("Removing OctoApp for Klipper store directory ...") + + klipper_instances: List[Moonraker] = get_instances(Klipper) + + for instance in klipper_instances: + store_dir = instance.data_dir.joinpath("octoapp-store") + if not store_dir.exists(): + Logger.print_info(f"'{store_dir}' does not exist. Skipped ...") + return + + run_remove_routines(store_dir) + + def _remove_OA_env(self) -> None: Logger.print_status("Removing OctoApp for Klipper environment ...") diff --git a/kiauh/extensions/simply_print/__init__.py b/kiauh/extensions/simply_print/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/extensions/simply_print/metadata.json b/kiauh/extensions/simply_print/metadata.json new file mode 100644 index 0000000..74213f1 --- /dev/null +++ b/kiauh/extensions/simply_print/metadata.json @@ -0,0 +1,13 @@ +{ + "metadata": { + "index": 10, + "module": "simply_print_extension", + "maintained_by": "dw-0", + "display_name": "SimplyPrint", + "description": [ + "3D Printer Cloud Management Software.", + "\n\n", + "3D printing doesn't have to be a complicated, analog, SD card-filled experience; step into the future of modern 3D printing" + ] + } +} diff --git a/kiauh/extensions/simply_print/simply_print_extension.py b/kiauh/extensions/simply_print/simply_print_extension.py new file mode 100644 index 0000000..e62c0d3 --- /dev/null +++ b/kiauh/extensions/simply_print/simply_print_extension.py @@ -0,0 +1,131 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # +from typing import List + +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from core.logger import DialogType, Logger +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) +from extensions.base_extension import BaseExtension +from utils.common import backup_printer_config_dir, moonraker_exists +from utils.input_utils import get_confirm + + +# noinspection PyMethodMayBeStatic +class SimplyPrintExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + Logger.print_status("Installing SimplyPrint ...") + + if not (mr_instances := moonraker_exists("SimplyPrint Installer")): + return + + Logger.print_dialog( + DialogType.INFO, + self._construct_dialog(mr_instances, True), + ) + + if not get_confirm( + "Continue SimplyPrint installation?", + default_choice=True, + allow_go_back=True, + ): + Logger.print_info("Exiting SimplyPrint installation ...") + return + + try: + self._patch_moonraker_confs(mr_instances, True) + + except Exception as e: + Logger.print_error(f"Error during SimplyPrint installation:\n{e}") + + def remove_extension(self, **kwargs) -> None: + Logger.print_status("Removing SimplyPrint ...") + + if not (mr_instances := moonraker_exists("SimplyPrint Uninstaller")): + return + + Logger.print_dialog( + DialogType.INFO, + self._construct_dialog(mr_instances, False), + ) + + if not get_confirm( + "Do you really want to uninstall SimplyPrint?", + default_choice=True, + allow_go_back=True, + ): + Logger.print_info("Exiting SimplyPrint uninstallation ...") + return + + try: + self._patch_moonraker_confs(mr_instances, False) + + except Exception as e: + Logger.print_error(f"Error during SimplyPrint installation:\n{e}") + + def _construct_dialog( + self, mr_instances: List[Moonraker], is_install: bool + ) -> List[str]: + mr_names = [f"● {m.service_file_path.name}" for m in mr_instances] + _type = "install" if is_install else "uninstall" + + return [ + "The following Moonraker instances were found:", + *mr_names, + "\n\n", + f"The setup will {_type} SimplyPrint for all Moonraker instances. " + f"After {_type}ation, all Moonraker services will be restarted!", + ] + + def _patch_moonraker_confs( + self, mr_instances: List[Moonraker], is_install: bool + ) -> None: + section = "simplyprint" + _type, _ft = ("Adding", "to") if is_install else ("Removing", "from") + + patched_files = [] + for moonraker in mr_instances: + Logger.print_status( + f"{_type} section 'simplyprint' {_ft} {moonraker.cfg_file} ..." + ) + scp = SimpleConfigParser() + scp.read_file(moonraker.cfg_file) + + install_and_has_section = is_install and scp.has_section(section) + uninstall_and_has_no_section = not is_install and not scp.has_section( + section + ) + + if install_and_has_section or uninstall_and_has_no_section: + status = "already" if is_install else "does not" + Logger.print_info( + f"Section 'simplyprint' {status} exists! Skipping ..." + ) + continue + + if is_install and not scp.has_section("simplyprint"): + backup_printer_config_dir() + scp.add_section(section) + elif not is_install and scp.has_section("simplyprint"): + backup_printer_config_dir() + scp.remove_section(section) + scp.write_file(moonraker.cfg_file) + patched_files.append(moonraker.cfg_file) + + if patched_files: + InstanceManager.restart_all(mr_instances) + + install_state = "successfully" if patched_files else "was already" + Logger.print_dialog( + DialogType.SUCCESS, + [f"SimplyPrint {install_state} {'' if is_install else 'un'}installed!"], + center_content=True, + ) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 4b89e9c..796839d 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -14,10 +14,11 @@ from pathlib import Path from typing import Dict, List, Literal, Optional, Set from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker from core.constants import ( COLOR_CYAN, GLOBAL_DEPS, - PRINTER_CFG_BACKUP_DIR, + PRINTER_DATA_BACKUP_DIR, RESET_FORMAT, ) from core.logger import DialogType, Logger @@ -142,23 +143,25 @@ def backup_printer_config_dir() -> None: instances: List[Klipper] = get_instances(Klipper) bm = BackupManager() + if not instances: + Logger.print_info("Unable to find directory to backup!") + Logger.print_info("Are there no Klipper instances installed?") + return + for instance in instances: - name = f"config-{instance.data_dir.name}" bm.backup_directory( - name, + instance.data_dir.name, source=instance.base.cfg_dir, - target=PRINTER_CFG_BACKUP_DIR, + target=PRINTER_DATA_BACKUP_DIR, ) -def moonraker_exists(name: str = "") -> bool: +def moonraker_exists(name: str = "") -> List[Moonraker]: """ Helper method to check if a Moonraker instance exists :param name: Optional name of an installer where the check is performed :return: True if at least one Moonraker instance exists, False otherwise """ - from components.moonraker.moonraker import Moonraker - mr_instances: List[Moonraker] = get_instances(Moonraker) info = ( @@ -175,8 +178,8 @@ def moonraker_exists(name: str = "") -> bool: f"{info}. Please install Moonraker first!", ], ) - return False - return True + return [] + return mr_instances def trunc_string(input_str: str, length: int) -> str: diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index fd39e0c..3991dcc 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -7,7 +7,7 @@ from http.client import HTTPResponse from json import JSONDecodeError from pathlib import Path from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run -from typing import List, Type +from typing import List, Tuple, Type from core.instance_manager.instance_manager import InstanceManager from core.logger import Logger @@ -70,7 +70,7 @@ def git_pull_wrapper(repo: str, target_dir: Path) -> None: return -def get_repo_name(repo: Path) -> tuple[str, str] | None: +def get_repo_name(repo: Path) -> Tuple[str, str]: """ Helper method to extract the organisation and name of a repository | :param repo: repository to extract the values from @@ -83,11 +83,14 @@ def get_repo_name(repo: Path) -> tuple[str, str] | None: cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"] result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8") substrings: List[str] = result.strip().split("/")[-2:] - return substrings[0], substrings[1] - # return "/".join(substrings).replace(".git", "") + orga: str = substrings[0] if substrings[0] else "-" + name: str = substrings[1] if substrings[1] else "-" + + return orga, name + except CalledProcessError: - return None + return "-", "-" def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]: @@ -184,7 +187,7 @@ def compare_semver_tags(tag1: str, tag2: str) -> bool: if tag1 == tag2: return False - def parse_version(v): + def parse_version(v) -> List[int]: return list(map(int, v[1:].split("."))) tag1_parts = parse_version(tag1)