From f1ea375465c51a904d3ae40a3154ff517379e0eb Mon Sep 17 00:00:00 2001 From: bkp <41797199+itdir@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:32:04 -0500 Subject: [PATCH 01/11] Refactor backup root initialization in BackupService Update backup root initialization to use the path of the Kiauh root directory instead of only using home directory. --- kiauh/core/services/backup_service.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index f88a8c30..00dabda3 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -21,9 +21,18 @@ class BackupService: def __init__(self): - self._backup_root = Path.home().joinpath("kiauh_backups") + kiauh_root = self._get_kiauh_root() + self._backup_root = kiauh_root.parent / "kiauh_backups" self._timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - + + @staticmethod + def _get_kiauh_root() -> Path: + this_file = Path(__file__).resolve() + for parent in this_file.parents: + if (parent / "kiauh.sh").exists() and (parent / "default.kiauh.cfg").exists(): + return parent + return Path.home() + @property def backup_root(self) -> Path: return self._backup_root From b9d030f1720bf621324710313dda8adc08cebc68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:30:39 +0000 Subject: [PATCH 02/11] Initial plan From 8336ecd9a38c98559f4f8364c26b2e2d3f42540b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:38:47 +0000 Subject: [PATCH 03/11] Replace hardcoded Path.home() with configurable BASE_DIR from KIAUH_BASE_DIR env var Introduces a centralized BASE_DIR constant in core/constants.py that reads from the KIAUH_BASE_DIR environment variable, defaulting to Path.home(). All component and extension directory constants now use BASE_DIR instead of hardcoded Path.home() calls, enabling system-wide installs to /opt/*, /srv/*, or any custom location. Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- kiauh/components/crowsnest/__init__.py | 4 ++-- kiauh/components/klipper/__init__.py | 8 +++++--- kiauh/components/klipperscreen/__init__.py | 8 +++----- kiauh/components/moonraker/__init__.py | 6 ++++-- kiauh/components/moonraker/utils/utils.py | 9 +++++---- kiauh/components/webui_client/client_utils.py | 3 ++- kiauh/components/webui_client/fluidd_data.py | 6 +++--- kiauh/components/webui_client/mainsail_data.py | 6 +++--- kiauh/core/constants.py | 6 ++++++ kiauh/core/services/backup_service.py | 14 +++++++++----- kiauh/extensions/gcode_shell_cmd/__init__.py | 4 +++- kiauh/extensions/klipper_backup/__init__.py | 8 +++++--- kiauh/extensions/mobileraker/__init__.py | 8 +++----- kiauh/extensions/obico/__init__.py | 6 ++++-- kiauh/extensions/octoapp/__init__.py | 8 ++++---- kiauh/extensions/octoeverywhere/__init__.py | 8 ++++---- kiauh/extensions/octoprint/octoprint.py | 10 +++++----- .../pretty_gcode/pretty_gcode_extension.py | 4 ++-- kiauh/extensions/spoolman/__init__.py | 4 +++- kiauh/extensions/telegram_bot/__init__.py | 6 ++++-- kiauh/extensions/tmc_autotune/__init__.py | 6 ++++-- kiauh/utils/fs_utils.py | 5 +++-- kiauh/utils/sys_utils.py | 8 +++++--- 23 files changed, 91 insertions(+), 64 deletions(-) diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py index f74ba17f..5e12c39f 100644 --- a/kiauh/components/crowsnest/__init__.py +++ b/kiauh/components/crowsnest/__init__.py @@ -9,7 +9,7 @@ from pathlib import Path -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" @@ -18,7 +18,7 @@ CROWSNEST_SERVICE_NAME = "crowsnest.service" # directories -CROWSNEST_DIR = Path.home().joinpath("crowsnest") +CROWSNEST_DIR = BASE_DIR.joinpath("crowsnest") # files CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 9b93b72c..9e7f72c1 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -9,6 +9,8 @@ from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" @@ -22,9 +24,9 @@ KLIPPER_SERVICE_NAME = "klipper.service" # directories -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") -KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") +KLIPPER_KCONFIGS_DIR = BASE_DIR.joinpath("klipper-kconfigs") +KLIPPER_ENV_DIR = BASE_DIR.joinpath("klippy-env") # files KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py index 5adbed3e..6e907137 100644 --- a/kiauh/components/klipperscreen/__init__.py +++ b/kiauh/components/klipperscreen/__init__.py @@ -6,9 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path - -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" @@ -19,8 +17,8 @@ KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" # directories -KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") -KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") +KLIPPERSCREEN_DIR = BASE_DIR.joinpath("KlipperScreen") +KLIPPERSCREEN_ENV_DIR = BASE_DIR.joinpath(".KlipperScreen-env") # files KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index c2b87a0d..69e21f0c 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -9,6 +9,8 @@ from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" @@ -21,8 +23,8 @@ MOONRAKER_ENV_FILE_NAME = "moonraker.env" # directories -MOONRAKER_DIR = Path.home().joinpath("moonraker") -MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") +MOONRAKER_DIR = BASE_DIR.joinpath("moonraker") +MOONRAKER_ENV_DIR = BASE_DIR.joinpath("moonraker-env") # files MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index 519baa9b..6a9b63c3 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -23,6 +23,7 @@ from components.moonraker.moonraker import Moonraker from components.moonraker.utils.sysdeps_parser import SysDepsParser from components.webui_client.base_data import BaseWebClient +from core.constants import BASE_DIR from core.logger import Logger from core.services.backup_service import BackupService from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( @@ -182,13 +183,13 @@ def backup_moonraker_db_dir() -> None: svc = BackupService() if not instances: - # fallback: search for printer data directories in the user's home directory + # fallback: search for printer data directories in the base directory Logger.print_info("No Moonraker instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in home directory..." + "Attempting to find printer data directories in base directory..." ) - home_dir = Path.home() + home_dir = BASE_DIR printer_data_dirs = [] for pattern in ["printer_data", "printer_*_data"]: @@ -198,7 +199,7 @@ def backup_moonraker_db_dir() -> None: if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") - Logger.print_info("No printer data directories found in home directory.") + Logger.print_info("No printer data directories found in base directory.") return for data_dir in printer_data_dirs: diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index e313e25d..8db53c88 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -11,6 +11,7 @@ import json import re import shutil +import tempfile from json import JSONDecodeError from pathlib import Path from subprocess import PIPE, CalledProcessError, run @@ -321,7 +322,7 @@ def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> :param template_src: the path to the template file :return: None """ - tmp = Path.home().joinpath(f"{name}.tmp") + tmp = Path(tempfile.mkstemp(suffix=f".{name}.tmp")[1]) shutil.copy(template_src, tmp) with open(tmp, "r+") as f: content = f.read() diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 5ed24d90..4ea5f9b7 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import NGINX_SITES_AVAILABLE +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class FluiddConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.FLUIDD name: str = client_config.value display_name: str = name.title() - config_dir: Path = Path.home().joinpath("fluidd-config") + config_dir: Path = BASE_DIR.joinpath("fluidd-config") config_filename: str = "fluidd.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" @@ -39,7 +39,7 @@ class FluiddData(BaseWebClient): client: WebClientType = WebClientType.FLUIDD name: str = client.value display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("fluidd") + client_dir: Path = BASE_DIR.joinpath("fluidd") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "fluidd-core/fluidd" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 025c90a4..551ef383 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import NGINX_SITES_AVAILABLE +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class MainsailConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.MAINSAIL name: str = client_config.value display_name: str = name.title() - config_dir: Path = Path.home().joinpath("mainsail-config") + config_dir: Path = BASE_DIR.joinpath("mainsail-config") config_filename: str = "mainsail.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" @@ -39,7 +39,7 @@ class MainsailData(BaseWebClient): client: WebClientType = WebClientType.MAINSAIL name: str = WebClientType.MAINSAIL.value display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("mainsail") + client_dir: Path = BASE_DIR.joinpath("mainsail") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "mainsail-crew/mainsail" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 1589147e..82e5aa07 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -20,6 +20,12 @@ # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] +# base directory for all component and extension installations +# Defaults to the current user's home directory. Override with the +# KIAUH_BASE_DIR environment variable to support system-wide installs +# (e.g. /opt/kiauh, /srv/kiauh). +BASE_DIR = Path(os.environ.get("KIAUH_BASE_DIR", str(Path.home()))) + # dirs SYSTEMD = Path("/etc/systemd/system") NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index 00dabda3..752b5d25 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -27,11 +27,13 @@ def __init__(self): @staticmethod def _get_kiauh_root() -> Path: + from core.constants import BASE_DIR + this_file = Path(__file__).resolve() for parent in this_file.parents: if (parent / "kiauh.sh").exists() and (parent / "default.kiauh.cfg").exists(): return parent - return Path.home() + return BASE_DIR @property def backup_root(self) -> Path: @@ -191,21 +193,23 @@ def backup_printer_config_dir(self) -> None: # fallback: search for printer data directories in the user's home directory Logger.print_info("No Klipper instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in home directory..." + "Attempting to find printer data directories in base directory..." ) - home_dir = Path.home() + from core.constants import BASE_DIR + + base_dir = BASE_DIR printer_data_dirs = [] for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): + for data_dir in base_dir.glob(pattern): if data_dir.is_dir(): printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") Logger.print_info( - "No printer data directories found in home directory." + "No printer data directories found in base directory." ) return diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py index 58a097bf..4561ec96 100644 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -9,10 +9,12 @@ from pathlib import Path +from core.constants import BASE_DIR + EXT_MODULE_NAME = "gcode_shell_command.py" MODULE_PATH = Path(__file__).resolve().parent MODULE_ASSETS = MODULE_PATH.joinpath("assets") -KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py index d5a66f88..949ed3ce 100644 --- a/kiauh/extensions/klipper_backup/__init__.py +++ b/kiauh/extensions/klipper_backup/__init__.py @@ -11,9 +11,11 @@ from pathlib import Path +from core.constants import BASE_DIR + EXT_MODULE_NAME = "klipper_backup_extension.py" MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") -KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") -KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") +MOONRAKER_CONF = BASE_DIR.joinpath("printer_data", "config", "moonraker.conf") +KLIPPERBACKUP_DIR = BASE_DIR.joinpath("klipper-backup") +KLIPPERBACKUP_CONFIG_DIR = BASE_DIR.joinpath("config_backup") KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/mobileraker/__init__.py b/kiauh/extensions/mobileraker/__init__.py index 02fa523c..146ee861 100644 --- a/kiauh/extensions/mobileraker/__init__.py +++ b/kiauh/extensions/mobileraker/__init__.py @@ -7,9 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path - -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" @@ -20,8 +18,8 @@ MOBILERAKER_LOG_NAME = "mobileraker.log" # directories -MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") -MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") +MOBILERAKER_DIR = BASE_DIR.joinpath("mobileraker_companion") +MOBILERAKER_ENV_DIR = BASE_DIR.joinpath("mobileraker-env") # files MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py index a6ac1b4f..5ff5ba6e 100644 --- a/kiauh/extensions/obico/__init__.py +++ b/kiauh/extensions/obico/__init__.py @@ -8,6 +8,8 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent # repo @@ -24,8 +26,8 @@ OBICO_MACROS_CFG_NAME = "moonraker_obico_macros.cfg" # directories -OBICO_DIR = Path.home().joinpath("moonraker-obico") -OBICO_ENV_DIR = Path.home().joinpath("moonraker-obico-env") +OBICO_DIR = BASE_DIR.joinpath("moonraker-obico") +OBICO_ENV_DIR = BASE_DIR.joinpath("moonraker-obico-env") # files OBICO_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_SERVICE_NAME}") diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py index a4767bc4..284146df 100644 --- a/kiauh/extensions/octoapp/__init__.py +++ b/kiauh/extensions/octoapp/__init__.py @@ -6,21 +6,21 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path +from core.constants import BASE_DIR # repo 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_DIR = BASE_DIR.joinpath("octoapp") +OA_ENV_DIR = BASE_DIR.joinpath("octoapp-env") # files OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json") OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh") OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh") -OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log") +OA_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoapp-installer.log") # filenames OA_CFG_NAME = "octoapp.conf" diff --git a/kiauh/extensions/octoeverywhere/__init__.py b/kiauh/extensions/octoeverywhere/__init__.py index 1870e625..57adbbe2 100644 --- a/kiauh/extensions/octoeverywhere/__init__.py +++ b/kiauh/extensions/octoeverywhere/__init__.py @@ -6,14 +6,14 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path +from core.constants import BASE_DIR # repo OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" # directories -OE_DIR = Path.home().joinpath("octoeverywhere") -OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") +OE_DIR = BASE_DIR.joinpath("octoeverywhere") +OE_ENV_DIR = BASE_DIR.joinpath("octoeverywhere-env") OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") # files @@ -21,7 +21,7 @@ OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") -OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") +OE_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoeverywhere-installer.log") # filenames OE_CFG_NAME = "octoeverywhere.conf" diff --git a/kiauh/extensions/octoprint/octoprint.py b/kiauh/extensions/octoprint/octoprint.py index c003efe5..925f99ca 100644 --- a/kiauh/extensions/octoprint/octoprint.py +++ b/kiauh/extensions/octoprint/octoprint.py @@ -13,7 +13,7 @@ from textwrap import dedent from components.klipper.klipper import Klipper -from core.constants import CURRENT_USER +from core.constants import BASE_DIR, CURRENT_USER from core.instance_manager.base_instance import BaseInstance from core.logger import Logger from extensions.octoprint import ( @@ -43,17 +43,17 @@ def __post_init__(self): # OctoPrint stores its data under ~/.octoprint[_SUFFIX] self.basedir = ( - Path.home().joinpath(OP_BASEDIR_PREFIX) + BASE_DIR.joinpath(OP_BASEDIR_PREFIX) if self.suffix == "" - else Path.home().joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") + else BASE_DIR.joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") ) self.cfg_file = self.basedir.joinpath("config.yaml") # OctoPrint virtualenv lives under ~/OctoPrint[_SUFFIX] self.env_dir = ( - Path.home().joinpath(OP_ENV_PREFIX) + BASE_DIR.joinpath(OP_ENV_PREFIX) if self.suffix == "" - else Path.home().joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") + else BASE_DIR.joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") ) def create(self, port: int) -> None: diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py index 0a4b6927..768ea4c1 100644 --- a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py +++ b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py @@ -10,7 +10,7 @@ from pathlib import Path from components.webui_client.client_utils import create_nginx_cfg -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.logger import DialogType, Logger from extensions.base_extension import BaseExtension from utils.common import check_install_dependencies @@ -22,7 +22,7 @@ from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr MODULE_PATH = Path(__file__).resolve().parent -PGC_DIR = Path.home().joinpath("pgcode") +PGC_DIR = BASE_DIR.joinpath("pgcode") PGC_REPO = "https://github.com/Kragrathea/pgcode" PGC_CONF = "pgcode.local.conf" diff --git a/kiauh/extensions/spoolman/__init__.py b/kiauh/extensions/spoolman/__init__.py index 4f7d628d..8c049105 100644 --- a/kiauh/extensions/spoolman/__init__.py +++ b/kiauh/extensions/spoolman/__init__.py @@ -8,9 +8,11 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest" -SPOOLMAN_DIR = Path.home().joinpath("spoolman") +SPOOLMAN_DIR = BASE_DIR.joinpath("spoolman") SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data") SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml") SPOOLMAN_DEFAULT_PORT = 7912 diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py index 773f725e..819b8e65 100644 --- a/kiauh/extensions/telegram_bot/__init__.py +++ b/kiauh/extensions/telegram_bot/__init__.py @@ -8,6 +8,8 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent # repo @@ -20,8 +22,8 @@ TG_BOT_ENV_FILE_NAME = "moonraker-telegram-bot.env" # directories -TG_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") -TG_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") +TG_BOT_DIR = BASE_DIR.joinpath("moonraker-telegram-bot") +TG_BOT_ENV = BASE_DIR.joinpath("moonraker-telegram-bot-env") # files TG_BOT_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_SERVICE_NAME}") diff --git a/kiauh/extensions/tmc_autotune/__init__.py b/kiauh/extensions/tmc_autotune/__init__.py index 9438829b..07187137 100644 --- a/kiauh/extensions/tmc_autotune/__init__.py +++ b/kiauh/extensions/tmc_autotune/__init__.py @@ -8,13 +8,15 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + # repo TMCA_REPO = "https://github.com/andrewmcgr/klipper_tmc_autotune" # directories -TMCA_DIR = Path.home().joinpath("klipper_tmc_autotune") +TMCA_DIR = BASE_DIR.joinpath("klipper_tmc_autotune") MODULE_PATH = Path(__file__).resolve().parent -KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") KLIPPER_PLUGINS = KLIPPER_DIR.joinpath("klippy/plugins") KLIPPER_EXTENSIONS_PATH = ( diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 0d141715..035946e8 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -18,6 +18,7 @@ from typing import List from zipfile import ZipFile +from core.constants import BASE_DIR from core.decorators import deprecated from core.logger import Logger @@ -169,6 +170,6 @@ def get_data_dir(instance_type: type, suffix: str) -> Path: if suffix != "": # this is the new data dir naming scheme introduced in v6.0.0 - return Path.home().joinpath(f"printer_{suffix}_data") + return BASE_DIR.joinpath(f"printer_{suffix}_data") - return Path.home().joinpath("printer_data") + return BASE_DIR.joinpath("printer_data") diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 02354aa6..3b856d7e 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -417,19 +417,21 @@ def download_progress(block_num, block_size, total_size) -> None: def set_nginx_permissions() -> None: """ - Check if permissions of the users home directory + Check if permissions of the base directory grant execution rights to group and other and set them if not set. Required permissions for NGINX to be able to serve Mainsail/Fluidd. This seems to have become necessary with Ubuntu 21+. | :return: None """ - cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" + from core.constants import BASE_DIR + + cmd = f"ls -ld {BASE_DIR} | cut -d' ' -f1" homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) permissions = homedir_perm.stdout if permissions.count("x") < 3: Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", Path.home()]) + run(["chmod", "og+x", BASE_DIR]) Logger.print_ok("Permissions granted.") From 7e3efd5a9459082821d34c52262ee140477311d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:40:05 +0000 Subject: [PATCH 04/11] Address code review: fix fd leak in tempfile, rename variable for clarity Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- kiauh/components/moonraker/utils/utils.py | 4 ++-- kiauh/components/webui_client/client_utils.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index 6a9b63c3..3a682b21 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -189,11 +189,11 @@ def backup_moonraker_db_dir() -> None: "Attempting to find printer data directories in base directory..." ) - home_dir = BASE_DIR + base_dir = BASE_DIR printer_data_dirs = [] for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): + for data_dir in base_dir.glob(pattern): if data_dir.is_dir(): printer_data_dirs.append(data_dir) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 8db53c88..b4065026 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import os import re import shutil import tempfile @@ -322,7 +323,9 @@ def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> :param template_src: the path to the template file :return: None """ - tmp = Path(tempfile.mkstemp(suffix=f".{name}.tmp")[1]) + fd, tmp_path = tempfile.mkstemp(suffix=f".{name}.tmp") + os.close(fd) + tmp = Path(tmp_path) shutil.copy(template_src, tmp) with open(tmp, "r+") as f: content = f.read() From c2aac006ddbbb9c2efc9b535060fdf915df2a6e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:04:16 +0000 Subject: [PATCH 05/11] Ensure backward compatibility: validate BASE_DIR, preserve home directory fallbacks - Validate KIAUH_BASE_DIR: empty, whitespace-only, or relative paths safely fall back to Path.home() instead of producing unexpected behavior - set_nginx_permissions(): always check home directory permissions (upstream behavior) plus BASE_DIR when it differs, since NGINX needs traversal access - Backup fallback search: search both Path.home() and BASE_DIR when they differ, ensuring existing home-directory installs are always found Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- kiauh/components/moonraker/utils/utils.py | 20 ++++++++++++-------- kiauh/core/constants.py | 3 ++- kiauh/core/services/backup_service.py | 18 +++++++++++------- kiauh/utils/sys_utils.py | 21 +++++++++++++-------- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index 3a682b21..23dab044 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -183,23 +183,27 @@ def backup_moonraker_db_dir() -> None: svc = BackupService() if not instances: - # fallback: search for printer data directories in the base directory + # fallback: search for printer data directories Logger.print_info("No Moonraker instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in base directory..." + "Attempting to find printer data directories ..." ) - base_dir = BASE_DIR + search_dirs = [Path.home()] + if BASE_DIR != Path.home(): + search_dirs.append(BASE_DIR) + printer_data_dirs = [] - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in base_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) + for search_dir in search_dirs: + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in search_dir.glob(pattern): + if data_dir.is_dir() and data_dir not in printer_data_dirs: + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") - Logger.print_info("No printer data directories found in base directory.") + Logger.print_info("No printer data directories found.") return for data_dir in printer_data_dirs: diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 82e5aa07..2826f62b 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -24,7 +24,8 @@ # Defaults to the current user's home directory. Override with the # KIAUH_BASE_DIR environment variable to support system-wide installs # (e.g. /opt/kiauh, /srv/kiauh). -BASE_DIR = Path(os.environ.get("KIAUH_BASE_DIR", str(Path.home()))) +_base_dir_env = os.environ.get("KIAUH_BASE_DIR", "").strip() +BASE_DIR = Path(_base_dir_env) if _base_dir_env and Path(_base_dir_env).is_absolute() else Path.home() # dirs SYSTEMD = Path("/etc/systemd/system") diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index 752b5d25..cafa25c4 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -193,23 +193,27 @@ def backup_printer_config_dir(self) -> None: # fallback: search for printer data directories in the user's home directory Logger.print_info("No Klipper instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in base directory..." + "Attempting to find printer data directories ..." ) from core.constants import BASE_DIR - base_dir = BASE_DIR + search_dirs = [Path.home()] + if BASE_DIR != Path.home(): + search_dirs.append(BASE_DIR) + printer_data_dirs = [] - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in base_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) + for search_dir in search_dirs: + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in search_dir.glob(pattern): + if data_dir.is_dir() and data_dir not in printer_data_dirs: + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") Logger.print_info( - "No printer data directories found in base directory." + "No printer data directories found." ) return diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 3b856d7e..163033d0 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -417,7 +417,7 @@ def download_progress(block_num, block_size, total_size) -> None: def set_nginx_permissions() -> None: """ - Check if permissions of the base directory + Check if permissions of the users home directory and base directory grant execution rights to group and other and set them if not set. Required permissions for NGINX to be able to serve Mainsail/Fluidd. This seems to have become necessary with Ubuntu 21+. | @@ -425,14 +425,19 @@ def set_nginx_permissions() -> None: """ from core.constants import BASE_DIR - cmd = f"ls -ld {BASE_DIR} | cut -d' ' -f1" - homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) - permissions = homedir_perm.stdout + dirs_to_check = [Path.home()] + if BASE_DIR != Path.home(): + dirs_to_check.append(BASE_DIR) - if permissions.count("x") < 3: - Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", BASE_DIR]) - Logger.print_ok("Permissions granted.") + for check_dir in dirs_to_check: + cmd = f"ls -ld {check_dir} | cut -d' ' -f1" + dir_perm = run(cmd, shell=True, stdout=PIPE, text=True) + permissions = dir_perm.stdout + + if permissions.count("x") < 3: + Logger.print_status("Granting NGINX the required permissions ...") + run(["chmod", "og+x", check_dir]) + Logger.print_ok("Permissions granted.") def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: From 817406a179b2cc3535278fe7ca9f142b4642cdf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:06:01 +0000 Subject: [PATCH 06/11] Address code review: use shlex.quote for shell safety, use set for dedup Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- kiauh/components/moonraker/utils/utils.py | 7 +++++-- kiauh/core/services/backup_service.py | 7 +++++-- kiauh/utils/sys_utils.py | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index 23dab044..cf6e8cf4 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -193,12 +193,15 @@ def backup_moonraker_db_dir() -> None: if BASE_DIR != Path.home(): search_dirs.append(BASE_DIR) - printer_data_dirs = [] + printer_data_dirs: List[Path] = [] + seen: set = set() for search_dir in search_dirs: for pattern in ["printer_data", "printer_*_data"]: for data_dir in search_dir.glob(pattern): - if data_dir.is_dir() and data_dir not in printer_data_dirs: + resolved = data_dir.resolve() + if data_dir.is_dir() and resolved not in seen: + seen.add(resolved) printer_data_dirs.append(data_dir) if not printer_data_dirs: diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index cafa25c4..9fd8300a 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -202,12 +202,15 @@ def backup_printer_config_dir(self) -> None: if BASE_DIR != Path.home(): search_dirs.append(BASE_DIR) - printer_data_dirs = [] + printer_data_dirs: List[Path] = [] + seen: set = set() for search_dir in search_dirs: for pattern in ["printer_data", "printer_*_data"]: for data_dir in search_dir.glob(pattern): - if data_dir.is_dir() and data_dir not in printer_data_dirs: + resolved = data_dir.resolve() + if data_dir.is_dir() and resolved not in seen: + seen.add(resolved) printer_data_dirs.append(data_dir) if not printer_data_dirs: diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 163033d0..058d5feb 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -423,6 +423,8 @@ def set_nginx_permissions() -> None: This seems to have become necessary with Ubuntu 21+. | :return: None """ + import shlex + from core.constants import BASE_DIR dirs_to_check = [Path.home()] @@ -430,13 +432,13 @@ def set_nginx_permissions() -> None: dirs_to_check.append(BASE_DIR) for check_dir in dirs_to_check: - cmd = f"ls -ld {check_dir} | cut -d' ' -f1" + cmd = f"ls -ld {shlex.quote(str(check_dir))} | cut -d' ' -f1" dir_perm = run(cmd, shell=True, stdout=PIPE, text=True) permissions = dir_perm.stdout if permissions.count("x") < 3: Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", check_dir]) + run(["chmod", "og+x", str(check_dir)]) Logger.print_ok("Permissions granted.") From a5bb03fe78fba40207f4a380fc178efe0d51829c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:29:06 +0000 Subject: [PATCH 07/11] Comprehensive alternative: settings-integrated configurable base directory with full proposal - Enhance BASE_DIR resolution: env var > kiauh.cfg > Path.home() - Add base_dir to AppSettings and KiauhSettings for persistence - Add base_dir option to default.kiauh.cfg (commented, self-documenting) - Add startup logging when non-default base dir is active - Create docs/proposal-configurable-base-dir.md with full analysis, SWOT, pros/cons, migration guide, and justification for upstream maintainers Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- default.kiauh.cfg | 6 + docs/proposal-configurable-base-dir.md | 308 +++++++++++++++++++++++++ kiauh/core/constants.py | 54 ++++- kiauh/core/settings/kiauh_settings.py | 15 ++ kiauh/main.py | 8 + 5 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 docs/proposal-configurable-base-dir.md diff --git a/default.kiauh.cfg b/default.kiauh.cfg index d3ac1496..b8eedc14 100644 --- a/default.kiauh.cfg +++ b/default.kiauh.cfg @@ -1,6 +1,12 @@ [kiauh] backup_before_update: False +# base directory for component and extension installations +# defaults to the current user's home directory if left empty +# use an absolute path to override, e.g. /opt/klipper-farm +# can also be set via the KIAUH_BASE_DIR environment variable (env var takes priority) +#base_dir: + [klipper] # add custom repositories here, if at least one is given, the first in the list will be used by default # otherwise the official repository is used diff --git a/docs/proposal-configurable-base-dir.md b/docs/proposal-configurable-base-dir.md new file mode 100644 index 00000000..36b4d7cd --- /dev/null +++ b/docs/proposal-configurable-base-dir.md @@ -0,0 +1,308 @@ +# Proposal: Configurable Base Directory for KIAUH + +> **Status:** Proposed +> **Author:** itdir +> **Target:** dw-0/kiauh upstream +> **Backward Compatible:** Yes — zero behaviour change when no override is set + +--- + +## Executive Summary + +This proposal introduces a configurable base directory (`BASE_DIR`) for all +component and extension installations in KIAUH. Today, every install path is +hard-wired to `Path.home()` (the running user's home directory). This change +replaces those references with a single, centrally-defined `BASE_DIR` that +**defaults to `Path.home()`** and can optionally be overridden via: + +1. The `KIAUH_BASE_DIR` environment variable (highest priority), or +2. The `base_dir` option in `kiauh.cfg` (persisted configuration) + +When neither is set, KIAUH behaves **identically to upstream** — every path +resolves to `~/component_name` exactly as before. + +--- + +## Problem Statement + +### Current State (Upstream) + +KIAUH hard-codes `Path.home()` in **~25 module-level constants** across +components and extensions: + +```python +# kiauh/components/klipper/__init__.py (upstream) +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") + +# kiauh/components/moonraker/__init__.py (upstream) +MOONRAKER_DIR = Path.home().joinpath("moonraker") + +# kiauh/extensions/octoeverywhere/__init__.py (upstream) +OE_DIR = Path.home().joinpath("octoeverywhere") +``` + +This design assumes a single user running a single Klipper stack in their +home directory. It prevents several legitimate deployment scenarios. + +### Use Cases Blocked by Hard-Coded Paths + +| Scenario | Description | +|----------|-------------| +| **Multi-printer farms** | A print farm operator wants separate Klipper stacks under `/srv/printer1/`, `/srv/printer2/` | +| **System-wide installs** | A shared workstation where Klipper is installed under `/opt/klipper-stack/` | +| **Container/chroot deployments** | Docker or systemd-nspawn environments where `$HOME` doesn't exist or is ephemeral | +| **CI/CD testing** | Automated test pipelines that need deterministic, non-home install paths | +| **Backup portability** | Backups that reference a configurable root instead of a user-specific home directory | + +--- + +## Proposed Solution + +### Architecture + +``` +Resolution Order (first non-empty absolute path wins): + + ┌─────────────────────────────────┐ + │ 1. KIAUH_BASE_DIR env variable │ ← Highest priority (ops/CI) + ├─────────────────────────────────┤ + │ 2. base_dir in kiauh.cfg │ ← Persisted user preference + ├─────────────────────────────────┤ + │ 3. Path.home() │ ← Default (upstream behaviour) + └─────────────────────────────────┘ +``` + +### Code Changes Summary + +| File | Change | Impact | +|------|--------|--------| +| `core/constants.py` | Add `_resolve_base_dir()` → `BASE_DIR` | Single source of truth | +| `default.kiauh.cfg` | Add commented `#base_dir:` option | Self-documenting | +| `core/settings/kiauh_settings.py` | Add `base_dir` to `AppSettings` | Persistence support | +| `main.py` | Log non-default `BASE_DIR` at startup | User feedback | +| 15× `__init__.py` (components) | `Path.home()` → `BASE_DIR` | Path consistency | +| 10× `__init__.py` (extensions) | `Path.home()` → `BASE_DIR` | Path consistency | +| `utils/fs_utils.py` | `Path.home()` → `BASE_DIR` in `get_data_dir()` | Data dir resolution | +| `utils/sys_utils.py` | Check both home + `BASE_DIR` for NGINX perms | Backward compat | +| `core/services/backup_service.py` | Search both home + `BASE_DIR` for fallback | Backward compat | +| `components/moonraker/utils/utils.py` | Search both home + `BASE_DIR` for fallback | Backward compat | +| `components/webui_client/client_utils.py` | Use `tempfile.mkstemp()` for nginx cfg tmp | Safety fix | + +### Key Design Decisions + +1. **Module-level constant, not a function call.** `BASE_DIR` is evaluated once + at import time. This matches the existing pattern used by `SYSTEMD`, + `NGINX_SITES_AVAILABLE`, etc. in `core/constants.py`. + +2. **Config file is read with a minimal parser** (not `KiauhSettings`) to avoid + circular imports. Component `__init__` modules import `BASE_DIR` before + `KiauhSettings` is constructed. + +3. **Env var takes priority over config file.** This follows the standard + twelve-factor app convention and allows ops teams to override the setting + without modifying files. + +4. **Only absolute paths are accepted.** Empty strings, whitespace, and relative + paths silently fall back to `Path.home()`, preventing misconfiguration. + +5. **Backward-compatible fallback search.** Backup and NGINX permission checks + search *both* `Path.home()` and `BASE_DIR` when they differ, so existing + installations in `~` are always found. + +--- + +## Analysis + +### What Changes for Existing Users? + +**Nothing.** When `KIAUH_BASE_DIR` is unset and `base_dir` is absent from +`kiauh.cfg`, `BASE_DIR` resolves to `Path.home()` — the exact same value +upstream uses today. Every path remains `~/klipper`, `~/moonraker`, +`~/printer_data`, etc. + +### What Changes for the Codebase? + +The diff touches ~25 files, but the pattern in each is identical: + +```diff +- from pathlib import Path ++ from core.constants import BASE_DIR + +- COMPONENT_DIR = Path.home().joinpath("component") ++ COMPONENT_DIR = BASE_DIR.joinpath("component") +``` + +No logic changes. No new dependencies. No new classes. The only *behavioural* +additions are in backup fallback search (search both directories) and NGINX +permission checks (ensure both directories have execute rights). + +--- + +## SWOT Analysis + +### Strengths +- **Zero-impact default.** No behaviour change for the >99% of users who run + KIAUH from their home directory. +- **Single constant.** One `BASE_DIR` in one file, imported everywhere. Easy to + audit, easy to grep. +- **Settings integration.** `kiauh.cfg` already governs ports, repos, and + update preferences — `base_dir` fits naturally. +- **Env var support.** Standard mechanism for container/CI overrides without + file changes. +- **Backward-compatible fallbacks.** Backup search and NGINX permissions handle + the case where `BASE_DIR ≠ Path.home()` gracefully. + +### Weaknesses +- **Module-level constant.** `BASE_DIR` is evaluated at import time, so + changing it requires restarting KIAUH. (This is the same constraint as all + other constants in `core/constants.py`.) +- **Config file parsed twice.** `_resolve_base_dir()` does a lightweight read + of `kiauh.cfg` before `KiauhSettings` parses it fully. This is intentional + (to avoid circular imports) but adds a small amount of duplication. +- **Touches many files.** The diff spans ~25 files, which can be intimidating + to review. However, each change is mechanical and identical. + +### Opportunities +- **Multi-printer-per-host support.** With a configurable base dir, KIAUH can + be invoked multiple times with different `KIAUH_BASE_DIR` values to manage + separate printer stacks on one machine. +- **Container-native deployments.** Makes KIAUH usable in Docker, Podman, and + systemd-nspawn without special `HOME` manipulation. +- **Easier automated testing.** CI pipelines can set `KIAUH_BASE_DIR` to a + temporary directory, avoiding pollution of the test user's home. +- **Foundation for future features.** A configurable root paves the way for + profile management, per-printer settings, and fleet administration. + +### Threats +- **Upstream merge conflicts.** If upstream adds new `Path.home()` references + in new components or extensions, they would need to use `BASE_DIR` instead. + This is mitigable with a lint rule or CI check. +- **User misconfiguration.** An incorrect `base_dir` path could cause KIAUH to + install into an unexpected location. Mitigated by: (a) only accepting + absolute paths, (b) logging the active `BASE_DIR` at startup, and (c) + leaving the option commented out by default. + +--- + +## Pros & Cons + +### Pros + +1. ✅ **100% backward compatible** — default behaviour is unchanged +2. ✅ **Minimal API surface** — one constant (`BASE_DIR`), one env var, one config option +3. ✅ **Follows existing patterns** — uses the same module-level constant style as `SYSTEMD`, `NGINX_SITES_AVAILABLE` +4. ✅ **Self-documenting** — `default.kiauh.cfg` includes commented documentation +5. ✅ **Startup feedback** — non-default base dir is logged at launch +6. ✅ **Security improvement** — nginx config temp files use `tempfile.mkstemp()` instead of predictable `~/name.tmp` +7. ✅ **Dual-search fallback** — backups find data in both `~` and `BASE_DIR` + +### Cons + +1. ⚠️ **Large diff** — touches ~25 files (all mechanical, same pattern) +2. ⚠️ **Ongoing maintenance** — new components must use `BASE_DIR` instead of `Path.home()` +3. ⚠️ **Not runtime-changeable** — requires restart to take effect (same as all other constants) +4. ⚠️ **Dual config parse** — `kiauh.cfg` read once by `_resolve_base_dir()` and once by `KiauhSettings` + +--- + +## Migration Guide + +### For Users + +No migration needed. KIAUH works exactly as before. + +To use a custom base directory: + +```bash +# Option A: environment variable (temporary / per-session) +export KIAUH_BASE_DIR=/opt/klipper-farm +./kiauh.sh + +# Option B: persistent configuration +# Edit kiauh.cfg and add under [kiauh]: +# base_dir: /opt/klipper-farm +``` + +### For Developers / Downstream Forks + +When adding a new component or extension, use `BASE_DIR` instead of +`Path.home()`: + +```python +from core.constants import BASE_DIR + +MY_COMPONENT_DIR = BASE_DIR.joinpath("my-component") +``` + +--- + +## Comparison with Alternative Approaches + +| Approach | Pros | Cons | +|----------|------|------| +| **This proposal (BASE_DIR constant)** | Simple, one constant, follows existing patterns | Touches many files | +| **Monkey-patch `Path.home()`** | Zero file changes needed | Fragile, affects all Python code, terrible practice | +| **Runtime path resolver function** | Lazy evaluation possible | Every path access becomes a function call; breaks existing APIs | +| **Centralized path registry** | All paths in one file | Massive refactor; breaks all existing imports | +| **Symlink-based approach** | No code changes | Fragile, OS-dependent, hard to debug | + +The proposed approach (module-level `BASE_DIR` constant) offers the best +trade-off between simplicity, compatibility, and maintainability. + +--- + +## Testing + +### Automated + +```bash +# Default behaviour (no override) +PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: default') +" + +# Environment variable override +KIAUH_BASE_DIR=/opt/test PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +assert str(BASE_DIR) == '/opt/test' +print('PASS: env var') +" + +# Invalid values fall back to home +KIAUH_BASE_DIR='' PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: empty env') +" + +KIAUH_BASE_DIR='relative' PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: relative env') +" +``` + +### Manual + +1. Run KIAUH without any override → verify all paths use `~` +2. Set `KIAUH_BASE_DIR=/tmp/kiauh-test` → verify startup log message +3. Add `base_dir: /tmp/kiauh-test` to `kiauh.cfg` → verify paths update +4. Set both env var and config → verify env var wins + +--- + +## Conclusion + +This proposal provides a clean, backward-compatible mechanism for configuring +KIAUH's installation base directory. It follows existing codebase conventions, +integrates with the settings system, and opens the door for multi-printer and +container deployments — all without changing the experience for existing users. + +We respectfully request the upstream maintainers consider this change for +inclusion in a future KIAUH release. diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 2826f62b..68d815a4 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -20,12 +20,58 @@ # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] + +def _resolve_base_dir() -> Path: + """Resolve the base directory for all component and extension installs. + + Resolution order (first non-empty absolute path wins): + 1. KIAUH_BASE_DIR environment variable + 2. ``base_dir`` option in ``[kiauh]`` section of ``kiauh.cfg`` + 3. ``Path.home()`` (default — preserves upstream behaviour) + + The settings file is read directly here (not via KiauhSettings) to + avoid circular imports — component ``__init__`` modules depend on + ``BASE_DIR`` and are imported before KiauhSettings is constructed. + """ + + # 1. env var — highest priority + env = os.environ.get("KIAUH_BASE_DIR", "").strip() + if env and Path(env).is_absolute(): + return Path(env) + + # 2. settings file — read only the raw value to stay import-safe + try: + # PROJECT_ROOT is two levels up from this file (kiauh/core/constants.py) + project_root = Path(__file__).resolve().parent.parent.parent + cfg_path = project_root / "kiauh.cfg" + if not cfg_path.exists(): + cfg_path = project_root / "default.kiauh.cfg" + if cfg_path.exists(): + with open(cfg_path, "r") as fh: + in_kiauh_section = False + for line in fh: + stripped = line.strip() + if stripped.startswith("["): + in_kiauh_section = stripped == "[kiauh]" + continue + if in_kiauh_section and stripped.startswith("base_dir"): + parts = stripped.split(":", 1) + if len(parts) == 2: + val = parts[1].strip() + if val and Path(val).is_absolute(): + return Path(val) + except Exception: + pass # any I/O or parse error → fall through to default + + # 3. default — identical to upstream behaviour + return Path.home() + + # base directory for all component and extension installations # Defaults to the current user's home directory. Override with the -# KIAUH_BASE_DIR environment variable to support system-wide installs -# (e.g. /opt/kiauh, /srv/kiauh). -_base_dir_env = os.environ.get("KIAUH_BASE_DIR", "").strip() -BASE_DIR = Path(_base_dir_env) if _base_dir_env and Path(_base_dir_env).is_absolute() else Path.home() +# KIAUH_BASE_DIR environment variable **or** the ``base_dir`` option in +# kiauh.cfg to support system-wide installs (e.g. /opt/kiauh, /srv/kiauh). +BASE_DIR: Path = _resolve_base_dir() # dirs SYSTEMD = Path("/etc/systemd/system") diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 4f27a90c..2f41ac4a 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -41,6 +41,7 @@ def __init__(self, section: str, option: str, value: str): @dataclass class AppSettings: backup_before_update: bool | None = field(default=None) + base_dir: str | None = field(default=None) @dataclass @@ -157,6 +158,13 @@ def __set_internal_state(self) -> None: self.config.getboolean, False, ) + self.kiauh.base_dir = self.__read_from_cfg( + "kiauh", + "base_dir", + self.config.getval, + None, + True, + ) # parse Klipper options self.klipper.use_python_binary = self.__read_from_cfg( @@ -300,6 +308,13 @@ def __write_internal_state_to_cfg(self) -> None: str(self.kiauh.backup_before_update), ) + if self.kiauh.base_dir is not None: + self.config.set_option( + "kiauh", + "base_dir", + self.kiauh.base_dir, + ) + # Handle repositories if self.klipper.repositories is not None: repos = [f"{repo.url}, {repo.branch}" for repo in self.klipper.repositories] diff --git a/kiauh/main.py b/kiauh/main.py index 3832ba69..96a5f21d 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -24,6 +24,14 @@ def main() -> None: try: KiauhSettings() ensure_encoding() + + from pathlib import Path + + from core.constants import BASE_DIR + + if BASE_DIR != Path.home(): + Logger.print_info(f"Using custom base directory: {BASE_DIR}") + MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) From 2eabc257ca0f9e71160035a2c49b84eb25c6e2f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:31:15 +0000 Subject: [PATCH 08/11] Address code review: module-level imports, dual separator support, project_root validation, config example Co-authored-by: itdir <41797199+itdir@users.noreply.github.com> --- default.kiauh.cfg | 2 +- kiauh/core/constants.py | 17 ++++++++++++----- kiauh/main.py | 6 ++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/default.kiauh.cfg b/default.kiauh.cfg index b8eedc14..23a8331a 100644 --- a/default.kiauh.cfg +++ b/default.kiauh.cfg @@ -5,7 +5,7 @@ backup_before_update: False # defaults to the current user's home directory if left empty # use an absolute path to override, e.g. /opt/klipper-farm # can also be set via the KIAUH_BASE_DIR environment variable (env var takes priority) -#base_dir: +#base_dir: /opt/klipper-farm [klipper] # add custom repositories here, if at least one is given, the first in the list will be used by default diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 68d815a4..79363b1f 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -43,6 +43,9 @@ def _resolve_base_dir() -> Path: try: # PROJECT_ROOT is two levels up from this file (kiauh/core/constants.py) project_root = Path(__file__).resolve().parent.parent.parent + # sanity check that we found the right directory + if not (project_root / "kiauh.sh").exists(): + raise FileNotFoundError cfg_path = project_root / "kiauh.cfg" if not cfg_path.exists(): cfg_path = project_root / "default.kiauh.cfg" @@ -55,11 +58,15 @@ def _resolve_base_dir() -> Path: in_kiauh_section = stripped == "[kiauh]" continue if in_kiauh_section and stripped.startswith("base_dir"): - parts = stripped.split(":", 1) - if len(parts) == 2: - val = parts[1].strip() - if val and Path(val).is_absolute(): - return Path(val) + # handle both "key: value" and "key = value" formats + for sep in (":", "="): + if sep in stripped: + parts = stripped.split(sep, 1) + if len(parts) == 2: + val = parts[1].strip() + if val and Path(val).is_absolute(): + return Path(val) + break except Exception: pass # any I/O or parse error → fall through to default diff --git a/kiauh/main.py b/kiauh/main.py index 96a5f21d..2ec6c0f5 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -8,7 +8,9 @@ # ======================================================================= # import io import sys +from pathlib import Path +from core.constants import BASE_DIR from core.logger import Logger from core.menus.main_menu import MainMenu from core.settings.kiauh_settings import KiauhSettings @@ -25,10 +27,6 @@ def main() -> None: KiauhSettings() ensure_encoding() - from pathlib import Path - - from core.constants import BASE_DIR - if BASE_DIR != Path.home(): Logger.print_info(f"Using custom base directory: {BASE_DIR}") From b0d233808fb0ab991e5317f13124be4cfb7a9fa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:25:11 +0000 Subject: [PATCH 09/11] Initial plan From 6dbc938e481d3446b61b7e92af98889115120982 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 27 Feb 2026 19:31:18 +0000 Subject: [PATCH 10/11] Revert "Merge pull request #1 from itdir/copilot/analyze-hardcoded-paths" This reverts commit 2c0044fd9f518f87bc1e148c9cb30212f382b7cb, reversing changes made to f1ea375465c51a904d3ae40a3154ff517379e0eb. --- default.kiauh.cfg | 6 - docs/proposal-configurable-base-dir.md | 308 ------------------ kiauh/components/crowsnest/__init__.py | 4 +- kiauh/components/klipper/__init__.py | 8 +- kiauh/components/klipperscreen/__init__.py | 8 +- kiauh/components/moonraker/__init__.py | 6 +- kiauh/components/moonraker/utils/utils.py | 26 +- kiauh/components/webui_client/client_utils.py | 6 +- kiauh/components/webui_client/fluidd_data.py | 6 +- .../components/webui_client/mainsail_data.py | 6 +- kiauh/core/constants.py | 60 ---- kiauh/core/services/backup_service.py | 29 +- kiauh/core/settings/kiauh_settings.py | 15 - kiauh/extensions/gcode_shell_cmd/__init__.py | 4 +- kiauh/extensions/klipper_backup/__init__.py | 8 +- kiauh/extensions/mobileraker/__init__.py | 8 +- kiauh/extensions/obico/__init__.py | 6 +- kiauh/extensions/octoapp/__init__.py | 8 +- kiauh/extensions/octoeverywhere/__init__.py | 8 +- kiauh/extensions/octoprint/octoprint.py | 10 +- .../pretty_gcode/pretty_gcode_extension.py | 4 +- kiauh/extensions/spoolman/__init__.py | 4 +- kiauh/extensions/telegram_bot/__init__.py | 6 +- kiauh/extensions/tmc_autotune/__init__.py | 6 +- kiauh/main.py | 6 - kiauh/utils/fs_utils.py | 5 +- kiauh/utils/sys_utils.py | 25 +- 27 files changed, 78 insertions(+), 518 deletions(-) delete mode 100644 docs/proposal-configurable-base-dir.md diff --git a/default.kiauh.cfg b/default.kiauh.cfg index 23a8331a..d3ac1496 100644 --- a/default.kiauh.cfg +++ b/default.kiauh.cfg @@ -1,12 +1,6 @@ [kiauh] backup_before_update: False -# base directory for component and extension installations -# defaults to the current user's home directory if left empty -# use an absolute path to override, e.g. /opt/klipper-farm -# can also be set via the KIAUH_BASE_DIR environment variable (env var takes priority) -#base_dir: /opt/klipper-farm - [klipper] # add custom repositories here, if at least one is given, the first in the list will be used by default # otherwise the official repository is used diff --git a/docs/proposal-configurable-base-dir.md b/docs/proposal-configurable-base-dir.md deleted file mode 100644 index 36b4d7cd..00000000 --- a/docs/proposal-configurable-base-dir.md +++ /dev/null @@ -1,308 +0,0 @@ -# Proposal: Configurable Base Directory for KIAUH - -> **Status:** Proposed -> **Author:** itdir -> **Target:** dw-0/kiauh upstream -> **Backward Compatible:** Yes — zero behaviour change when no override is set - ---- - -## Executive Summary - -This proposal introduces a configurable base directory (`BASE_DIR`) for all -component and extension installations in KIAUH. Today, every install path is -hard-wired to `Path.home()` (the running user's home directory). This change -replaces those references with a single, centrally-defined `BASE_DIR` that -**defaults to `Path.home()`** and can optionally be overridden via: - -1. The `KIAUH_BASE_DIR` environment variable (highest priority), or -2. The `base_dir` option in `kiauh.cfg` (persisted configuration) - -When neither is set, KIAUH behaves **identically to upstream** — every path -resolves to `~/component_name` exactly as before. - ---- - -## Problem Statement - -### Current State (Upstream) - -KIAUH hard-codes `Path.home()` in **~25 module-level constants** across -components and extensions: - -```python -# kiauh/components/klipper/__init__.py (upstream) -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") - -# kiauh/components/moonraker/__init__.py (upstream) -MOONRAKER_DIR = Path.home().joinpath("moonraker") - -# kiauh/extensions/octoeverywhere/__init__.py (upstream) -OE_DIR = Path.home().joinpath("octoeverywhere") -``` - -This design assumes a single user running a single Klipper stack in their -home directory. It prevents several legitimate deployment scenarios. - -### Use Cases Blocked by Hard-Coded Paths - -| Scenario | Description | -|----------|-------------| -| **Multi-printer farms** | A print farm operator wants separate Klipper stacks under `/srv/printer1/`, `/srv/printer2/` | -| **System-wide installs** | A shared workstation where Klipper is installed under `/opt/klipper-stack/` | -| **Container/chroot deployments** | Docker or systemd-nspawn environments where `$HOME` doesn't exist or is ephemeral | -| **CI/CD testing** | Automated test pipelines that need deterministic, non-home install paths | -| **Backup portability** | Backups that reference a configurable root instead of a user-specific home directory | - ---- - -## Proposed Solution - -### Architecture - -``` -Resolution Order (first non-empty absolute path wins): - - ┌─────────────────────────────────┐ - │ 1. KIAUH_BASE_DIR env variable │ ← Highest priority (ops/CI) - ├─────────────────────────────────┤ - │ 2. base_dir in kiauh.cfg │ ← Persisted user preference - ├─────────────────────────────────┤ - │ 3. Path.home() │ ← Default (upstream behaviour) - └─────────────────────────────────┘ -``` - -### Code Changes Summary - -| File | Change | Impact | -|------|--------|--------| -| `core/constants.py` | Add `_resolve_base_dir()` → `BASE_DIR` | Single source of truth | -| `default.kiauh.cfg` | Add commented `#base_dir:` option | Self-documenting | -| `core/settings/kiauh_settings.py` | Add `base_dir` to `AppSettings` | Persistence support | -| `main.py` | Log non-default `BASE_DIR` at startup | User feedback | -| 15× `__init__.py` (components) | `Path.home()` → `BASE_DIR` | Path consistency | -| 10× `__init__.py` (extensions) | `Path.home()` → `BASE_DIR` | Path consistency | -| `utils/fs_utils.py` | `Path.home()` → `BASE_DIR` in `get_data_dir()` | Data dir resolution | -| `utils/sys_utils.py` | Check both home + `BASE_DIR` for NGINX perms | Backward compat | -| `core/services/backup_service.py` | Search both home + `BASE_DIR` for fallback | Backward compat | -| `components/moonraker/utils/utils.py` | Search both home + `BASE_DIR` for fallback | Backward compat | -| `components/webui_client/client_utils.py` | Use `tempfile.mkstemp()` for nginx cfg tmp | Safety fix | - -### Key Design Decisions - -1. **Module-level constant, not a function call.** `BASE_DIR` is evaluated once - at import time. This matches the existing pattern used by `SYSTEMD`, - `NGINX_SITES_AVAILABLE`, etc. in `core/constants.py`. - -2. **Config file is read with a minimal parser** (not `KiauhSettings`) to avoid - circular imports. Component `__init__` modules import `BASE_DIR` before - `KiauhSettings` is constructed. - -3. **Env var takes priority over config file.** This follows the standard - twelve-factor app convention and allows ops teams to override the setting - without modifying files. - -4. **Only absolute paths are accepted.** Empty strings, whitespace, and relative - paths silently fall back to `Path.home()`, preventing misconfiguration. - -5. **Backward-compatible fallback search.** Backup and NGINX permission checks - search *both* `Path.home()` and `BASE_DIR` when they differ, so existing - installations in `~` are always found. - ---- - -## Analysis - -### What Changes for Existing Users? - -**Nothing.** When `KIAUH_BASE_DIR` is unset and `base_dir` is absent from -`kiauh.cfg`, `BASE_DIR` resolves to `Path.home()` — the exact same value -upstream uses today. Every path remains `~/klipper`, `~/moonraker`, -`~/printer_data`, etc. - -### What Changes for the Codebase? - -The diff touches ~25 files, but the pattern in each is identical: - -```diff -- from pathlib import Path -+ from core.constants import BASE_DIR - -- COMPONENT_DIR = Path.home().joinpath("component") -+ COMPONENT_DIR = BASE_DIR.joinpath("component") -``` - -No logic changes. No new dependencies. No new classes. The only *behavioural* -additions are in backup fallback search (search both directories) and NGINX -permission checks (ensure both directories have execute rights). - ---- - -## SWOT Analysis - -### Strengths -- **Zero-impact default.** No behaviour change for the >99% of users who run - KIAUH from their home directory. -- **Single constant.** One `BASE_DIR` in one file, imported everywhere. Easy to - audit, easy to grep. -- **Settings integration.** `kiauh.cfg` already governs ports, repos, and - update preferences — `base_dir` fits naturally. -- **Env var support.** Standard mechanism for container/CI overrides without - file changes. -- **Backward-compatible fallbacks.** Backup search and NGINX permissions handle - the case where `BASE_DIR ≠ Path.home()` gracefully. - -### Weaknesses -- **Module-level constant.** `BASE_DIR` is evaluated at import time, so - changing it requires restarting KIAUH. (This is the same constraint as all - other constants in `core/constants.py`.) -- **Config file parsed twice.** `_resolve_base_dir()` does a lightweight read - of `kiauh.cfg` before `KiauhSettings` parses it fully. This is intentional - (to avoid circular imports) but adds a small amount of duplication. -- **Touches many files.** The diff spans ~25 files, which can be intimidating - to review. However, each change is mechanical and identical. - -### Opportunities -- **Multi-printer-per-host support.** With a configurable base dir, KIAUH can - be invoked multiple times with different `KIAUH_BASE_DIR` values to manage - separate printer stacks on one machine. -- **Container-native deployments.** Makes KIAUH usable in Docker, Podman, and - systemd-nspawn without special `HOME` manipulation. -- **Easier automated testing.** CI pipelines can set `KIAUH_BASE_DIR` to a - temporary directory, avoiding pollution of the test user's home. -- **Foundation for future features.** A configurable root paves the way for - profile management, per-printer settings, and fleet administration. - -### Threats -- **Upstream merge conflicts.** If upstream adds new `Path.home()` references - in new components or extensions, they would need to use `BASE_DIR` instead. - This is mitigable with a lint rule or CI check. -- **User misconfiguration.** An incorrect `base_dir` path could cause KIAUH to - install into an unexpected location. Mitigated by: (a) only accepting - absolute paths, (b) logging the active `BASE_DIR` at startup, and (c) - leaving the option commented out by default. - ---- - -## Pros & Cons - -### Pros - -1. ✅ **100% backward compatible** — default behaviour is unchanged -2. ✅ **Minimal API surface** — one constant (`BASE_DIR`), one env var, one config option -3. ✅ **Follows existing patterns** — uses the same module-level constant style as `SYSTEMD`, `NGINX_SITES_AVAILABLE` -4. ✅ **Self-documenting** — `default.kiauh.cfg` includes commented documentation -5. ✅ **Startup feedback** — non-default base dir is logged at launch -6. ✅ **Security improvement** — nginx config temp files use `tempfile.mkstemp()` instead of predictable `~/name.tmp` -7. ✅ **Dual-search fallback** — backups find data in both `~` and `BASE_DIR` - -### Cons - -1. ⚠️ **Large diff** — touches ~25 files (all mechanical, same pattern) -2. ⚠️ **Ongoing maintenance** — new components must use `BASE_DIR` instead of `Path.home()` -3. ⚠️ **Not runtime-changeable** — requires restart to take effect (same as all other constants) -4. ⚠️ **Dual config parse** — `kiauh.cfg` read once by `_resolve_base_dir()` and once by `KiauhSettings` - ---- - -## Migration Guide - -### For Users - -No migration needed. KIAUH works exactly as before. - -To use a custom base directory: - -```bash -# Option A: environment variable (temporary / per-session) -export KIAUH_BASE_DIR=/opt/klipper-farm -./kiauh.sh - -# Option B: persistent configuration -# Edit kiauh.cfg and add under [kiauh]: -# base_dir: /opt/klipper-farm -``` - -### For Developers / Downstream Forks - -When adding a new component or extension, use `BASE_DIR` instead of -`Path.home()`: - -```python -from core.constants import BASE_DIR - -MY_COMPONENT_DIR = BASE_DIR.joinpath("my-component") -``` - ---- - -## Comparison with Alternative Approaches - -| Approach | Pros | Cons | -|----------|------|------| -| **This proposal (BASE_DIR constant)** | Simple, one constant, follows existing patterns | Touches many files | -| **Monkey-patch `Path.home()`** | Zero file changes needed | Fragile, affects all Python code, terrible practice | -| **Runtime path resolver function** | Lazy evaluation possible | Every path access becomes a function call; breaks existing APIs | -| **Centralized path registry** | All paths in one file | Massive refactor; breaks all existing imports | -| **Symlink-based approach** | No code changes | Fragile, OS-dependent, hard to debug | - -The proposed approach (module-level `BASE_DIR` constant) offers the best -trade-off between simplicity, compatibility, and maintainability. - ---- - -## Testing - -### Automated - -```bash -# Default behaviour (no override) -PYTHONPATH=kiauh python3 -c " -from core.constants import BASE_DIR -from pathlib import Path -assert BASE_DIR == Path.home() -print('PASS: default') -" - -# Environment variable override -KIAUH_BASE_DIR=/opt/test PYTHONPATH=kiauh python3 -c " -from core.constants import BASE_DIR -assert str(BASE_DIR) == '/opt/test' -print('PASS: env var') -" - -# Invalid values fall back to home -KIAUH_BASE_DIR='' PYTHONPATH=kiauh python3 -c " -from core.constants import BASE_DIR -from pathlib import Path -assert BASE_DIR == Path.home() -print('PASS: empty env') -" - -KIAUH_BASE_DIR='relative' PYTHONPATH=kiauh python3 -c " -from core.constants import BASE_DIR -from pathlib import Path -assert BASE_DIR == Path.home() -print('PASS: relative env') -" -``` - -### Manual - -1. Run KIAUH without any override → verify all paths use `~` -2. Set `KIAUH_BASE_DIR=/tmp/kiauh-test` → verify startup log message -3. Add `base_dir: /tmp/kiauh-test` to `kiauh.cfg` → verify paths update -4. Set both env var and config → verify env var wins - ---- - -## Conclusion - -This proposal provides a clean, backward-compatible mechanism for configuring -KIAUH's installation base directory. It follows existing codebase conventions, -integrates with the settings system, and opens the door for multi-printer and -container deployments — all without changing the experience for existing users. - -We respectfully request the upstream maintainers consider this change for -inclusion in a future KIAUH release. diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py index 5e12c39f..f74ba17f 100644 --- a/kiauh/components/crowsnest/__init__.py +++ b/kiauh/components/crowsnest/__init__.py @@ -9,7 +9,7 @@ from pathlib import Path -from core.constants import BASE_DIR, SYSTEMD +from core.constants import SYSTEMD # repo CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" @@ -18,7 +18,7 @@ CROWSNEST_SERVICE_NAME = "crowsnest.service" # directories -CROWSNEST_DIR = BASE_DIR.joinpath("crowsnest") +CROWSNEST_DIR = Path.home().joinpath("crowsnest") # files CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 9e7f72c1..9b93b72c 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -9,8 +9,6 @@ from pathlib import Path -from core.constants import BASE_DIR - MODULE_PATH = Path(__file__).resolve().parent KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" @@ -24,9 +22,9 @@ KLIPPER_SERVICE_NAME = "klipper.service" # directories -KLIPPER_DIR = BASE_DIR.joinpath("klipper") -KLIPPER_KCONFIGS_DIR = BASE_DIR.joinpath("klipper-kconfigs") -KLIPPER_ENV_DIR = BASE_DIR.joinpath("klippy-env") +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") +KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") # files KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py index 6e907137..5adbed3e 100644 --- a/kiauh/components/klipperscreen/__init__.py +++ b/kiauh/components/klipperscreen/__init__.py @@ -6,7 +6,9 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from core.constants import BASE_DIR, SYSTEMD +from pathlib import Path + +from core.constants import SYSTEMD # repo KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" @@ -17,8 +19,8 @@ KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" # directories -KLIPPERSCREEN_DIR = BASE_DIR.joinpath("KlipperScreen") -KLIPPERSCREEN_ENV_DIR = BASE_DIR.joinpath(".KlipperScreen-env") +KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") +KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") # files KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 69e21f0c..c2b87a0d 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -9,8 +9,6 @@ from pathlib import Path -from core.constants import BASE_DIR - MODULE_PATH = Path(__file__).resolve().parent MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" @@ -23,8 +21,8 @@ MOONRAKER_ENV_FILE_NAME = "moonraker.env" # directories -MOONRAKER_DIR = BASE_DIR.joinpath("moonraker") -MOONRAKER_ENV_DIR = BASE_DIR.joinpath("moonraker-env") +MOONRAKER_DIR = Path.home().joinpath("moonraker") +MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") # files MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index cf6e8cf4..519baa9b 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -23,7 +23,6 @@ from components.moonraker.moonraker import Moonraker from components.moonraker.utils.sysdeps_parser import SysDepsParser from components.webui_client.base_data import BaseWebClient -from core.constants import BASE_DIR from core.logger import Logger from core.services.backup_service import BackupService from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( @@ -183,30 +182,23 @@ def backup_moonraker_db_dir() -> None: svc = BackupService() if not instances: - # fallback: search for printer data directories + # fallback: search for printer data directories in the user's home directory Logger.print_info("No Moonraker instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories ..." + "Attempting to find printer data directories in home directory..." ) - search_dirs = [Path.home()] - if BASE_DIR != Path.home(): - search_dirs.append(BASE_DIR) + home_dir = Path.home() + printer_data_dirs = [] - printer_data_dirs: List[Path] = [] - seen: set = set() - - for search_dir in search_dirs: - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in search_dir.glob(pattern): - resolved = data_dir.resolve() - if data_dir.is_dir() and resolved not in seen: - seen.add(resolved) - printer_data_dirs.append(data_dir) + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in home_dir.glob(pattern): + if data_dir.is_dir(): + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") - Logger.print_info("No printer data directories found.") + Logger.print_info("No printer data directories found in home directory.") return for data_dir in printer_data_dirs: diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index b4065026..e313e25d 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -9,10 +9,8 @@ from __future__ import annotations import json -import os import re import shutil -import tempfile from json import JSONDecodeError from pathlib import Path from subprocess import PIPE, CalledProcessError, run @@ -323,9 +321,7 @@ def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> :param template_src: the path to the template file :return: None """ - fd, tmp_path = tempfile.mkstemp(suffix=f".{name}.tmp") - os.close(fd) - tmp = Path(tmp_path) + tmp = Path.home().joinpath(f"{name}.tmp") shutil.copy(template_src, tmp) with open(tmp, "r+") as f: content = f.read() diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 4ea5f9b7..5ed24d90 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE +from core.constants import NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class FluiddConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.FLUIDD name: str = client_config.value display_name: str = name.title() - config_dir: Path = BASE_DIR.joinpath("fluidd-config") + config_dir: Path = Path.home().joinpath("fluidd-config") config_filename: str = "fluidd.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" @@ -39,7 +39,7 @@ class FluiddData(BaseWebClient): client: WebClientType = WebClientType.FLUIDD name: str = client.value display_name: str = name.capitalize() - client_dir: Path = BASE_DIR.joinpath("fluidd") + client_dir: Path = Path.home().joinpath("fluidd") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "fluidd-core/fluidd" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 551ef383..025c90a4 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE +from core.constants import NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class MainsailConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.MAINSAIL name: str = client_config.value display_name: str = name.title() - config_dir: Path = BASE_DIR.joinpath("mainsail-config") + config_dir: Path = Path.home().joinpath("mainsail-config") config_filename: str = "mainsail.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" @@ -39,7 +39,7 @@ class MainsailData(BaseWebClient): client: WebClientType = WebClientType.MAINSAIL name: str = WebClientType.MAINSAIL.value display_name: str = name.capitalize() - client_dir: Path = BASE_DIR.joinpath("mainsail") + client_dir: Path = Path.home().joinpath("mainsail") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "mainsail-crew/mainsail" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 79363b1f..1589147e 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -20,66 +20,6 @@ # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] - -def _resolve_base_dir() -> Path: - """Resolve the base directory for all component and extension installs. - - Resolution order (first non-empty absolute path wins): - 1. KIAUH_BASE_DIR environment variable - 2. ``base_dir`` option in ``[kiauh]`` section of ``kiauh.cfg`` - 3. ``Path.home()`` (default — preserves upstream behaviour) - - The settings file is read directly here (not via KiauhSettings) to - avoid circular imports — component ``__init__`` modules depend on - ``BASE_DIR`` and are imported before KiauhSettings is constructed. - """ - - # 1. env var — highest priority - env = os.environ.get("KIAUH_BASE_DIR", "").strip() - if env and Path(env).is_absolute(): - return Path(env) - - # 2. settings file — read only the raw value to stay import-safe - try: - # PROJECT_ROOT is two levels up from this file (kiauh/core/constants.py) - project_root = Path(__file__).resolve().parent.parent.parent - # sanity check that we found the right directory - if not (project_root / "kiauh.sh").exists(): - raise FileNotFoundError - cfg_path = project_root / "kiauh.cfg" - if not cfg_path.exists(): - cfg_path = project_root / "default.kiauh.cfg" - if cfg_path.exists(): - with open(cfg_path, "r") as fh: - in_kiauh_section = False - for line in fh: - stripped = line.strip() - if stripped.startswith("["): - in_kiauh_section = stripped == "[kiauh]" - continue - if in_kiauh_section and stripped.startswith("base_dir"): - # handle both "key: value" and "key = value" formats - for sep in (":", "="): - if sep in stripped: - parts = stripped.split(sep, 1) - if len(parts) == 2: - val = parts[1].strip() - if val and Path(val).is_absolute(): - return Path(val) - break - except Exception: - pass # any I/O or parse error → fall through to default - - # 3. default — identical to upstream behaviour - return Path.home() - - -# base directory for all component and extension installations -# Defaults to the current user's home directory. Override with the -# KIAUH_BASE_DIR environment variable **or** the ``base_dir`` option in -# kiauh.cfg to support system-wide installs (e.g. /opt/kiauh, /srv/kiauh). -BASE_DIR: Path = _resolve_base_dir() - # dirs SYSTEMD = Path("/etc/systemd/system") NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index 9fd8300a..00dabda3 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -27,13 +27,11 @@ def __init__(self): @staticmethod def _get_kiauh_root() -> Path: - from core.constants import BASE_DIR - this_file = Path(__file__).resolve() for parent in this_file.parents: if (parent / "kiauh.sh").exists() and (parent / "default.kiauh.cfg").exists(): return parent - return BASE_DIR + return Path.home() @property def backup_root(self) -> Path: @@ -193,30 +191,21 @@ def backup_printer_config_dir(self) -> None: # fallback: search for printer data directories in the user's home directory Logger.print_info("No Klipper instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories ..." + "Attempting to find printer data directories in home directory..." ) - from core.constants import BASE_DIR - - search_dirs = [Path.home()] - if BASE_DIR != Path.home(): - search_dirs.append(BASE_DIR) - - printer_data_dirs: List[Path] = [] - seen: set = set() + home_dir = Path.home() + printer_data_dirs = [] - for search_dir in search_dirs: - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in search_dir.glob(pattern): - resolved = data_dir.resolve() - if data_dir.is_dir() and resolved not in seen: - seen.add(resolved) - printer_data_dirs.append(data_dir) + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in home_dir.glob(pattern): + if data_dir.is_dir(): + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") Logger.print_info( - "No printer data directories found." + "No printer data directories found in home directory." ) return diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 2f41ac4a..4f27a90c 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -41,7 +41,6 @@ def __init__(self, section: str, option: str, value: str): @dataclass class AppSettings: backup_before_update: bool | None = field(default=None) - base_dir: str | None = field(default=None) @dataclass @@ -158,13 +157,6 @@ def __set_internal_state(self) -> None: self.config.getboolean, False, ) - self.kiauh.base_dir = self.__read_from_cfg( - "kiauh", - "base_dir", - self.config.getval, - None, - True, - ) # parse Klipper options self.klipper.use_python_binary = self.__read_from_cfg( @@ -308,13 +300,6 @@ def __write_internal_state_to_cfg(self) -> None: str(self.kiauh.backup_before_update), ) - if self.kiauh.base_dir is not None: - self.config.set_option( - "kiauh", - "base_dir", - self.kiauh.base_dir, - ) - # Handle repositories if self.klipper.repositories is not None: repos = [f"{repo.url}, {repo.branch}" for repo in self.klipper.repositories] diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py index 4561ec96..58a097bf 100644 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -9,12 +9,10 @@ from pathlib import Path -from core.constants import BASE_DIR - EXT_MODULE_NAME = "gcode_shell_command.py" MODULE_PATH = Path(__file__).resolve().parent MODULE_ASSETS = MODULE_PATH.joinpath("assets") -KLIPPER_DIR = BASE_DIR.joinpath("klipper") +KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py index 949ed3ce..d5a66f88 100644 --- a/kiauh/extensions/klipper_backup/__init__.py +++ b/kiauh/extensions/klipper_backup/__init__.py @@ -11,11 +11,9 @@ from pathlib import Path -from core.constants import BASE_DIR - EXT_MODULE_NAME = "klipper_backup_extension.py" MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_CONF = BASE_DIR.joinpath("printer_data", "config", "moonraker.conf") -KLIPPERBACKUP_DIR = BASE_DIR.joinpath("klipper-backup") -KLIPPERBACKUP_CONFIG_DIR = BASE_DIR.joinpath("config_backup") +MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") +KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") +KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/mobileraker/__init__.py b/kiauh/extensions/mobileraker/__init__.py index 146ee861..02fa523c 100644 --- a/kiauh/extensions/mobileraker/__init__.py +++ b/kiauh/extensions/mobileraker/__init__.py @@ -7,7 +7,9 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from core.constants import BASE_DIR, SYSTEMD +from pathlib import Path + +from core.constants import SYSTEMD # repo MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" @@ -18,8 +20,8 @@ MOBILERAKER_LOG_NAME = "mobileraker.log" # directories -MOBILERAKER_DIR = BASE_DIR.joinpath("mobileraker_companion") -MOBILERAKER_ENV_DIR = BASE_DIR.joinpath("mobileraker-env") +MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") +MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") # files MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py index 5ff5ba6e..a6ac1b4f 100644 --- a/kiauh/extensions/obico/__init__.py +++ b/kiauh/extensions/obico/__init__.py @@ -8,8 +8,6 @@ # ======================================================================= # from pathlib import Path -from core.constants import BASE_DIR - MODULE_PATH = Path(__file__).resolve().parent # repo @@ -26,8 +24,8 @@ OBICO_MACROS_CFG_NAME = "moonraker_obico_macros.cfg" # directories -OBICO_DIR = BASE_DIR.joinpath("moonraker-obico") -OBICO_ENV_DIR = BASE_DIR.joinpath("moonraker-obico-env") +OBICO_DIR = Path.home().joinpath("moonraker-obico") +OBICO_ENV_DIR = Path.home().joinpath("moonraker-obico-env") # files OBICO_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_SERVICE_NAME}") diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py index 284146df..a4767bc4 100644 --- a/kiauh/extensions/octoapp/__init__.py +++ b/kiauh/extensions/octoapp/__init__.py @@ -6,21 +6,21 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from core.constants import BASE_DIR +from pathlib import Path # repo OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git" # directories -OA_DIR = BASE_DIR.joinpath("octoapp") -OA_ENV_DIR = BASE_DIR.joinpath("octoapp-env") +OA_DIR = Path.home().joinpath("octoapp") +OA_ENV_DIR = Path.home().joinpath("octoapp-env") # files OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json") OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh") OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh") -OA_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoapp-installer.log") +OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log") # filenames OA_CFG_NAME = "octoapp.conf" diff --git a/kiauh/extensions/octoeverywhere/__init__.py b/kiauh/extensions/octoeverywhere/__init__.py index 57adbbe2..1870e625 100644 --- a/kiauh/extensions/octoeverywhere/__init__.py +++ b/kiauh/extensions/octoeverywhere/__init__.py @@ -6,14 +6,14 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from core.constants import BASE_DIR +from pathlib import Path # repo OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" # directories -OE_DIR = BASE_DIR.joinpath("octoeverywhere") -OE_ENV_DIR = BASE_DIR.joinpath("octoeverywhere-env") +OE_DIR = Path.home().joinpath("octoeverywhere") +OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") # files @@ -21,7 +21,7 @@ OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") -OE_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoeverywhere-installer.log") +OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") # filenames OE_CFG_NAME = "octoeverywhere.conf" diff --git a/kiauh/extensions/octoprint/octoprint.py b/kiauh/extensions/octoprint/octoprint.py index 925f99ca..c003efe5 100644 --- a/kiauh/extensions/octoprint/octoprint.py +++ b/kiauh/extensions/octoprint/octoprint.py @@ -13,7 +13,7 @@ from textwrap import dedent from components.klipper.klipper import Klipper -from core.constants import BASE_DIR, CURRENT_USER +from core.constants import CURRENT_USER from core.instance_manager.base_instance import BaseInstance from core.logger import Logger from extensions.octoprint import ( @@ -43,17 +43,17 @@ def __post_init__(self): # OctoPrint stores its data under ~/.octoprint[_SUFFIX] self.basedir = ( - BASE_DIR.joinpath(OP_BASEDIR_PREFIX) + Path.home().joinpath(OP_BASEDIR_PREFIX) if self.suffix == "" - else BASE_DIR.joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") + else Path.home().joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") ) self.cfg_file = self.basedir.joinpath("config.yaml") # OctoPrint virtualenv lives under ~/OctoPrint[_SUFFIX] self.env_dir = ( - BASE_DIR.joinpath(OP_ENV_PREFIX) + Path.home().joinpath(OP_ENV_PREFIX) if self.suffix == "" - else BASE_DIR.joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") + else Path.home().joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") ) def create(self, port: int) -> None: diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py index 768ea4c1..0a4b6927 100644 --- a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py +++ b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py @@ -10,7 +10,7 @@ from pathlib import Path from components.webui_client.client_utils import create_nginx_cfg -from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.logger import DialogType, Logger from extensions.base_extension import BaseExtension from utils.common import check_install_dependencies @@ -22,7 +22,7 @@ from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr MODULE_PATH = Path(__file__).resolve().parent -PGC_DIR = BASE_DIR.joinpath("pgcode") +PGC_DIR = Path.home().joinpath("pgcode") PGC_REPO = "https://github.com/Kragrathea/pgcode" PGC_CONF = "pgcode.local.conf" diff --git a/kiauh/extensions/spoolman/__init__.py b/kiauh/extensions/spoolman/__init__.py index 8c049105..4f7d628d 100644 --- a/kiauh/extensions/spoolman/__init__.py +++ b/kiauh/extensions/spoolman/__init__.py @@ -8,11 +8,9 @@ # ======================================================================= # from pathlib import Path -from core.constants import BASE_DIR - MODULE_PATH = Path(__file__).resolve().parent SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest" -SPOOLMAN_DIR = BASE_DIR.joinpath("spoolman") +SPOOLMAN_DIR = Path.home().joinpath("spoolman") SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data") SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml") SPOOLMAN_DEFAULT_PORT = 7912 diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py index 819b8e65..773f725e 100644 --- a/kiauh/extensions/telegram_bot/__init__.py +++ b/kiauh/extensions/telegram_bot/__init__.py @@ -8,8 +8,6 @@ # ======================================================================= # from pathlib import Path -from core.constants import BASE_DIR - MODULE_PATH = Path(__file__).resolve().parent # repo @@ -22,8 +20,8 @@ TG_BOT_ENV_FILE_NAME = "moonraker-telegram-bot.env" # directories -TG_BOT_DIR = BASE_DIR.joinpath("moonraker-telegram-bot") -TG_BOT_ENV = BASE_DIR.joinpath("moonraker-telegram-bot-env") +TG_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") +TG_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") # files TG_BOT_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_SERVICE_NAME}") diff --git a/kiauh/extensions/tmc_autotune/__init__.py b/kiauh/extensions/tmc_autotune/__init__.py index 07187137..9438829b 100644 --- a/kiauh/extensions/tmc_autotune/__init__.py +++ b/kiauh/extensions/tmc_autotune/__init__.py @@ -8,15 +8,13 @@ # ======================================================================= # from pathlib import Path -from core.constants import BASE_DIR - # repo TMCA_REPO = "https://github.com/andrewmcgr/klipper_tmc_autotune" # directories -TMCA_DIR = BASE_DIR.joinpath("klipper_tmc_autotune") +TMCA_DIR = Path.home().joinpath("klipper_tmc_autotune") MODULE_PATH = Path(__file__).resolve().parent -KLIPPER_DIR = BASE_DIR.joinpath("klipper") +KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") KLIPPER_PLUGINS = KLIPPER_DIR.joinpath("klippy/plugins") KLIPPER_EXTENSIONS_PATH = ( diff --git a/kiauh/main.py b/kiauh/main.py index 2ec6c0f5..3832ba69 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -8,9 +8,7 @@ # ======================================================================= # import io import sys -from pathlib import Path -from core.constants import BASE_DIR from core.logger import Logger from core.menus.main_menu import MainMenu from core.settings.kiauh_settings import KiauhSettings @@ -26,10 +24,6 @@ def main() -> None: try: KiauhSettings() ensure_encoding() - - if BASE_DIR != Path.home(): - Logger.print_info(f"Using custom base directory: {BASE_DIR}") - MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 035946e8..0d141715 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -18,7 +18,6 @@ from typing import List from zipfile import ZipFile -from core.constants import BASE_DIR from core.decorators import deprecated from core.logger import Logger @@ -170,6 +169,6 @@ def get_data_dir(instance_type: type, suffix: str) -> Path: if suffix != "": # this is the new data dir naming scheme introduced in v6.0.0 - return BASE_DIR.joinpath(f"printer_{suffix}_data") + return Path.home().joinpath(f"printer_{suffix}_data") - return BASE_DIR.joinpath("printer_data") + return Path.home().joinpath("printer_data") diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 058d5feb..02354aa6 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -417,29 +417,20 @@ def download_progress(block_num, block_size, total_size) -> None: def set_nginx_permissions() -> None: """ - Check if permissions of the users home directory and base directory + Check if permissions of the users home directory grant execution rights to group and other and set them if not set. Required permissions for NGINX to be able to serve Mainsail/Fluidd. This seems to have become necessary with Ubuntu 21+. | :return: None """ - import shlex + cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" + homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) + permissions = homedir_perm.stdout - from core.constants import BASE_DIR - - dirs_to_check = [Path.home()] - if BASE_DIR != Path.home(): - dirs_to_check.append(BASE_DIR) - - for check_dir in dirs_to_check: - cmd = f"ls -ld {shlex.quote(str(check_dir))} | cut -d' ' -f1" - dir_perm = run(cmd, shell=True, stdout=PIPE, text=True) - permissions = dir_perm.stdout - - if permissions.count("x") < 3: - Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", str(check_dir)]) - Logger.print_ok("Permissions granted.") + if permissions.count("x") < 3: + Logger.print_status("Granting NGINX the required permissions ...") + run(["chmod", "og+x", Path.home()]) + Logger.print_ok("Permissions granted.") def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: From a54936817273ee61cda719a0a59be0524c010e40 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 27 Feb 2026 19:32:17 +0000 Subject: [PATCH 11/11] Squash all commits after b9d030f: configurable BASE_DIR via KIAUH_BASE_DIR env var Squashed commits: - b9d030f Initial plan - 8336ecd Replace hardcoded Path.home() with configurable BASE_DIR from KIAUH_BASE_DIR env var - 7e3efd5 Address code review: fix fd leak in tempfile, rename variable for clarity - c2aac00 Ensure backward compatibility: validate BASE_DIR, preserve home directory fallbacks - 817406a Address code review: use shlex.quote for shell safety, use set for dedup - a5bb03f Comprehensive alternative: settings-integrated configurable base directory with full proposal - 2eabc25 Address code review: module-level imports, dual separator support, project_root validation, config example --- default.kiauh.cfg | 6 + docs/proposal-configurable-base-dir.md | 308 ++++++++++++++++++ kiauh/components/crowsnest/__init__.py | 4 +- kiauh/components/klipper/__init__.py | 8 +- kiauh/components/klipperscreen/__init__.py | 8 +- kiauh/components/moonraker/__init__.py | 6 +- kiauh/components/moonraker/utils/utils.py | 26 +- kiauh/components/webui_client/client_utils.py | 6 +- kiauh/components/webui_client/fluidd_data.py | 6 +- .../components/webui_client/mainsail_data.py | 6 +- kiauh/core/constants.py | 60 ++++ kiauh/core/services/backup_service.py | 29 +- kiauh/core/settings/kiauh_settings.py | 15 + kiauh/extensions/gcode_shell_cmd/__init__.py | 4 +- kiauh/extensions/klipper_backup/__init__.py | 8 +- kiauh/extensions/mobileraker/__init__.py | 8 +- kiauh/extensions/obico/__init__.py | 6 +- kiauh/extensions/octoapp/__init__.py | 8 +- kiauh/extensions/octoeverywhere/__init__.py | 8 +- kiauh/extensions/octoprint/octoprint.py | 10 +- .../pretty_gcode/pretty_gcode_extension.py | 4 +- kiauh/extensions/spoolman/__init__.py | 4 +- kiauh/extensions/telegram_bot/__init__.py | 6 +- kiauh/extensions/tmc_autotune/__init__.py | 6 +- kiauh/main.py | 6 + kiauh/utils/fs_utils.py | 5 +- kiauh/utils/sys_utils.py | 25 +- 27 files changed, 518 insertions(+), 78 deletions(-) create mode 100644 docs/proposal-configurable-base-dir.md diff --git a/default.kiauh.cfg b/default.kiauh.cfg index d3ac1496..23a8331a 100644 --- a/default.kiauh.cfg +++ b/default.kiauh.cfg @@ -1,6 +1,12 @@ [kiauh] backup_before_update: False +# base directory for component and extension installations +# defaults to the current user's home directory if left empty +# use an absolute path to override, e.g. /opt/klipper-farm +# can also be set via the KIAUH_BASE_DIR environment variable (env var takes priority) +#base_dir: /opt/klipper-farm + [klipper] # add custom repositories here, if at least one is given, the first in the list will be used by default # otherwise the official repository is used diff --git a/docs/proposal-configurable-base-dir.md b/docs/proposal-configurable-base-dir.md new file mode 100644 index 00000000..36b4d7cd --- /dev/null +++ b/docs/proposal-configurable-base-dir.md @@ -0,0 +1,308 @@ +# Proposal: Configurable Base Directory for KIAUH + +> **Status:** Proposed +> **Author:** itdir +> **Target:** dw-0/kiauh upstream +> **Backward Compatible:** Yes — zero behaviour change when no override is set + +--- + +## Executive Summary + +This proposal introduces a configurable base directory (`BASE_DIR`) for all +component and extension installations in KIAUH. Today, every install path is +hard-wired to `Path.home()` (the running user's home directory). This change +replaces those references with a single, centrally-defined `BASE_DIR` that +**defaults to `Path.home()`** and can optionally be overridden via: + +1. The `KIAUH_BASE_DIR` environment variable (highest priority), or +2. The `base_dir` option in `kiauh.cfg` (persisted configuration) + +When neither is set, KIAUH behaves **identically to upstream** — every path +resolves to `~/component_name` exactly as before. + +--- + +## Problem Statement + +### Current State (Upstream) + +KIAUH hard-codes `Path.home()` in **~25 module-level constants** across +components and extensions: + +```python +# kiauh/components/klipper/__init__.py (upstream) +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") + +# kiauh/components/moonraker/__init__.py (upstream) +MOONRAKER_DIR = Path.home().joinpath("moonraker") + +# kiauh/extensions/octoeverywhere/__init__.py (upstream) +OE_DIR = Path.home().joinpath("octoeverywhere") +``` + +This design assumes a single user running a single Klipper stack in their +home directory. It prevents several legitimate deployment scenarios. + +### Use Cases Blocked by Hard-Coded Paths + +| Scenario | Description | +|----------|-------------| +| **Multi-printer farms** | A print farm operator wants separate Klipper stacks under `/srv/printer1/`, `/srv/printer2/` | +| **System-wide installs** | A shared workstation where Klipper is installed under `/opt/klipper-stack/` | +| **Container/chroot deployments** | Docker or systemd-nspawn environments where `$HOME` doesn't exist or is ephemeral | +| **CI/CD testing** | Automated test pipelines that need deterministic, non-home install paths | +| **Backup portability** | Backups that reference a configurable root instead of a user-specific home directory | + +--- + +## Proposed Solution + +### Architecture + +``` +Resolution Order (first non-empty absolute path wins): + + ┌─────────────────────────────────┐ + │ 1. KIAUH_BASE_DIR env variable │ ← Highest priority (ops/CI) + ├─────────────────────────────────┤ + │ 2. base_dir in kiauh.cfg │ ← Persisted user preference + ├─────────────────────────────────┤ + │ 3. Path.home() │ ← Default (upstream behaviour) + └─────────────────────────────────┘ +``` + +### Code Changes Summary + +| File | Change | Impact | +|------|--------|--------| +| `core/constants.py` | Add `_resolve_base_dir()` → `BASE_DIR` | Single source of truth | +| `default.kiauh.cfg` | Add commented `#base_dir:` option | Self-documenting | +| `core/settings/kiauh_settings.py` | Add `base_dir` to `AppSettings` | Persistence support | +| `main.py` | Log non-default `BASE_DIR` at startup | User feedback | +| 15× `__init__.py` (components) | `Path.home()` → `BASE_DIR` | Path consistency | +| 10× `__init__.py` (extensions) | `Path.home()` → `BASE_DIR` | Path consistency | +| `utils/fs_utils.py` | `Path.home()` → `BASE_DIR` in `get_data_dir()` | Data dir resolution | +| `utils/sys_utils.py` | Check both home + `BASE_DIR` for NGINX perms | Backward compat | +| `core/services/backup_service.py` | Search both home + `BASE_DIR` for fallback | Backward compat | +| `components/moonraker/utils/utils.py` | Search both home + `BASE_DIR` for fallback | Backward compat | +| `components/webui_client/client_utils.py` | Use `tempfile.mkstemp()` for nginx cfg tmp | Safety fix | + +### Key Design Decisions + +1. **Module-level constant, not a function call.** `BASE_DIR` is evaluated once + at import time. This matches the existing pattern used by `SYSTEMD`, + `NGINX_SITES_AVAILABLE`, etc. in `core/constants.py`. + +2. **Config file is read with a minimal parser** (not `KiauhSettings`) to avoid + circular imports. Component `__init__` modules import `BASE_DIR` before + `KiauhSettings` is constructed. + +3. **Env var takes priority over config file.** This follows the standard + twelve-factor app convention and allows ops teams to override the setting + without modifying files. + +4. **Only absolute paths are accepted.** Empty strings, whitespace, and relative + paths silently fall back to `Path.home()`, preventing misconfiguration. + +5. **Backward-compatible fallback search.** Backup and NGINX permission checks + search *both* `Path.home()` and `BASE_DIR` when they differ, so existing + installations in `~` are always found. + +--- + +## Analysis + +### What Changes for Existing Users? + +**Nothing.** When `KIAUH_BASE_DIR` is unset and `base_dir` is absent from +`kiauh.cfg`, `BASE_DIR` resolves to `Path.home()` — the exact same value +upstream uses today. Every path remains `~/klipper`, `~/moonraker`, +`~/printer_data`, etc. + +### What Changes for the Codebase? + +The diff touches ~25 files, but the pattern in each is identical: + +```diff +- from pathlib import Path ++ from core.constants import BASE_DIR + +- COMPONENT_DIR = Path.home().joinpath("component") ++ COMPONENT_DIR = BASE_DIR.joinpath("component") +``` + +No logic changes. No new dependencies. No new classes. The only *behavioural* +additions are in backup fallback search (search both directories) and NGINX +permission checks (ensure both directories have execute rights). + +--- + +## SWOT Analysis + +### Strengths +- **Zero-impact default.** No behaviour change for the >99% of users who run + KIAUH from their home directory. +- **Single constant.** One `BASE_DIR` in one file, imported everywhere. Easy to + audit, easy to grep. +- **Settings integration.** `kiauh.cfg` already governs ports, repos, and + update preferences — `base_dir` fits naturally. +- **Env var support.** Standard mechanism for container/CI overrides without + file changes. +- **Backward-compatible fallbacks.** Backup search and NGINX permissions handle + the case where `BASE_DIR ≠ Path.home()` gracefully. + +### Weaknesses +- **Module-level constant.** `BASE_DIR` is evaluated at import time, so + changing it requires restarting KIAUH. (This is the same constraint as all + other constants in `core/constants.py`.) +- **Config file parsed twice.** `_resolve_base_dir()` does a lightweight read + of `kiauh.cfg` before `KiauhSettings` parses it fully. This is intentional + (to avoid circular imports) but adds a small amount of duplication. +- **Touches many files.** The diff spans ~25 files, which can be intimidating + to review. However, each change is mechanical and identical. + +### Opportunities +- **Multi-printer-per-host support.** With a configurable base dir, KIAUH can + be invoked multiple times with different `KIAUH_BASE_DIR` values to manage + separate printer stacks on one machine. +- **Container-native deployments.** Makes KIAUH usable in Docker, Podman, and + systemd-nspawn without special `HOME` manipulation. +- **Easier automated testing.** CI pipelines can set `KIAUH_BASE_DIR` to a + temporary directory, avoiding pollution of the test user's home. +- **Foundation for future features.** A configurable root paves the way for + profile management, per-printer settings, and fleet administration. + +### Threats +- **Upstream merge conflicts.** If upstream adds new `Path.home()` references + in new components or extensions, they would need to use `BASE_DIR` instead. + This is mitigable with a lint rule or CI check. +- **User misconfiguration.** An incorrect `base_dir` path could cause KIAUH to + install into an unexpected location. Mitigated by: (a) only accepting + absolute paths, (b) logging the active `BASE_DIR` at startup, and (c) + leaving the option commented out by default. + +--- + +## Pros & Cons + +### Pros + +1. ✅ **100% backward compatible** — default behaviour is unchanged +2. ✅ **Minimal API surface** — one constant (`BASE_DIR`), one env var, one config option +3. ✅ **Follows existing patterns** — uses the same module-level constant style as `SYSTEMD`, `NGINX_SITES_AVAILABLE` +4. ✅ **Self-documenting** — `default.kiauh.cfg` includes commented documentation +5. ✅ **Startup feedback** — non-default base dir is logged at launch +6. ✅ **Security improvement** — nginx config temp files use `tempfile.mkstemp()` instead of predictable `~/name.tmp` +7. ✅ **Dual-search fallback** — backups find data in both `~` and `BASE_DIR` + +### Cons + +1. ⚠️ **Large diff** — touches ~25 files (all mechanical, same pattern) +2. ⚠️ **Ongoing maintenance** — new components must use `BASE_DIR` instead of `Path.home()` +3. ⚠️ **Not runtime-changeable** — requires restart to take effect (same as all other constants) +4. ⚠️ **Dual config parse** — `kiauh.cfg` read once by `_resolve_base_dir()` and once by `KiauhSettings` + +--- + +## Migration Guide + +### For Users + +No migration needed. KIAUH works exactly as before. + +To use a custom base directory: + +```bash +# Option A: environment variable (temporary / per-session) +export KIAUH_BASE_DIR=/opt/klipper-farm +./kiauh.sh + +# Option B: persistent configuration +# Edit kiauh.cfg and add under [kiauh]: +# base_dir: /opt/klipper-farm +``` + +### For Developers / Downstream Forks + +When adding a new component or extension, use `BASE_DIR` instead of +`Path.home()`: + +```python +from core.constants import BASE_DIR + +MY_COMPONENT_DIR = BASE_DIR.joinpath("my-component") +``` + +--- + +## Comparison with Alternative Approaches + +| Approach | Pros | Cons | +|----------|------|------| +| **This proposal (BASE_DIR constant)** | Simple, one constant, follows existing patterns | Touches many files | +| **Monkey-patch `Path.home()`** | Zero file changes needed | Fragile, affects all Python code, terrible practice | +| **Runtime path resolver function** | Lazy evaluation possible | Every path access becomes a function call; breaks existing APIs | +| **Centralized path registry** | All paths in one file | Massive refactor; breaks all existing imports | +| **Symlink-based approach** | No code changes | Fragile, OS-dependent, hard to debug | + +The proposed approach (module-level `BASE_DIR` constant) offers the best +trade-off between simplicity, compatibility, and maintainability. + +--- + +## Testing + +### Automated + +```bash +# Default behaviour (no override) +PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: default') +" + +# Environment variable override +KIAUH_BASE_DIR=/opt/test PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +assert str(BASE_DIR) == '/opt/test' +print('PASS: env var') +" + +# Invalid values fall back to home +KIAUH_BASE_DIR='' PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: empty env') +" + +KIAUH_BASE_DIR='relative' PYTHONPATH=kiauh python3 -c " +from core.constants import BASE_DIR +from pathlib import Path +assert BASE_DIR == Path.home() +print('PASS: relative env') +" +``` + +### Manual + +1. Run KIAUH without any override → verify all paths use `~` +2. Set `KIAUH_BASE_DIR=/tmp/kiauh-test` → verify startup log message +3. Add `base_dir: /tmp/kiauh-test` to `kiauh.cfg` → verify paths update +4. Set both env var and config → verify env var wins + +--- + +## Conclusion + +This proposal provides a clean, backward-compatible mechanism for configuring +KIAUH's installation base directory. It follows existing codebase conventions, +integrates with the settings system, and opens the door for multi-printer and +container deployments — all without changing the experience for existing users. + +We respectfully request the upstream maintainers consider this change for +inclusion in a future KIAUH release. diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py index f74ba17f..5e12c39f 100644 --- a/kiauh/components/crowsnest/__init__.py +++ b/kiauh/components/crowsnest/__init__.py @@ -9,7 +9,7 @@ from pathlib import Path -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" @@ -18,7 +18,7 @@ CROWSNEST_SERVICE_NAME = "crowsnest.service" # directories -CROWSNEST_DIR = Path.home().joinpath("crowsnest") +CROWSNEST_DIR = BASE_DIR.joinpath("crowsnest") # files CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 9b93b72c..9e7f72c1 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -9,6 +9,8 @@ from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" @@ -22,9 +24,9 @@ KLIPPER_SERVICE_NAME = "klipper.service" # directories -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") -KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") +KLIPPER_KCONFIGS_DIR = BASE_DIR.joinpath("klipper-kconfigs") +KLIPPER_ENV_DIR = BASE_DIR.joinpath("klippy-env") # files KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py index 5adbed3e..6e907137 100644 --- a/kiauh/components/klipperscreen/__init__.py +++ b/kiauh/components/klipperscreen/__init__.py @@ -6,9 +6,7 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path - -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" @@ -19,8 +17,8 @@ KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" # directories -KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") -KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") +KLIPPERSCREEN_DIR = BASE_DIR.joinpath("KlipperScreen") +KLIPPERSCREEN_ENV_DIR = BASE_DIR.joinpath(".KlipperScreen-env") # files KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index c2b87a0d..69e21f0c 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -9,6 +9,8 @@ from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" @@ -21,8 +23,8 @@ MOONRAKER_ENV_FILE_NAME = "moonraker.env" # directories -MOONRAKER_DIR = Path.home().joinpath("moonraker") -MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") +MOONRAKER_DIR = BASE_DIR.joinpath("moonraker") +MOONRAKER_ENV_DIR = BASE_DIR.joinpath("moonraker-env") # files MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py index 519baa9b..cf6e8cf4 100644 --- a/kiauh/components/moonraker/utils/utils.py +++ b/kiauh/components/moonraker/utils/utils.py @@ -23,6 +23,7 @@ from components.moonraker.moonraker import Moonraker from components.moonraker.utils.sysdeps_parser import SysDepsParser from components.webui_client.base_data import BaseWebClient +from core.constants import BASE_DIR from core.logger import Logger from core.services.backup_service import BackupService from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( @@ -182,23 +183,30 @@ def backup_moonraker_db_dir() -> None: svc = BackupService() if not instances: - # fallback: search for printer data directories in the user's home directory + # fallback: search for printer data directories Logger.print_info("No Moonraker instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in home directory..." + "Attempting to find printer data directories ..." ) - home_dir = Path.home() - printer_data_dirs = [] + search_dirs = [Path.home()] + if BASE_DIR != Path.home(): + search_dirs.append(BASE_DIR) - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) + printer_data_dirs: List[Path] = [] + seen: set = set() + + for search_dir in search_dirs: + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in search_dir.glob(pattern): + resolved = data_dir.resolve() + if data_dir.is_dir() and resolved not in seen: + seen.add(resolved) + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") - Logger.print_info("No printer data directories found in home directory.") + Logger.print_info("No printer data directories found.") return for data_dir in printer_data_dirs: diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index e313e25d..b4065026 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -9,8 +9,10 @@ from __future__ import annotations import json +import os import re import shutil +import tempfile from json import JSONDecodeError from pathlib import Path from subprocess import PIPE, CalledProcessError, run @@ -321,7 +323,9 @@ def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> :param template_src: the path to the template file :return: None """ - tmp = Path.home().joinpath(f"{name}.tmp") + fd, tmp_path = tempfile.mkstemp(suffix=f".{name}.tmp") + os.close(fd) + tmp = Path(tmp_path) shutil.copy(template_src, tmp) with open(tmp, "r+") as f: content = f.read() diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index 5ed24d90..4ea5f9b7 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import NGINX_SITES_AVAILABLE +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class FluiddConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.FLUIDD name: str = client_config.value display_name: str = name.title() - config_dir: Path = Path.home().joinpath("fluidd-config") + config_dir: Path = BASE_DIR.joinpath("fluidd-config") config_filename: str = "fluidd.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" @@ -39,7 +39,7 @@ class FluiddData(BaseWebClient): client: WebClientType = WebClientType.FLUIDD name: str = client.value display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("fluidd") + client_dir: Path = BASE_DIR.joinpath("fluidd") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "fluidd-core/fluidd" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 025c90a4..551ef383 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -18,7 +18,7 @@ WebClientConfigType, WebClientType, ) -from core.constants import NGINX_SITES_AVAILABLE +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE @dataclass() @@ -26,7 +26,7 @@ class MainsailConfigWeb(BaseWebClientConfig): client_config: WebClientConfigType = WebClientConfigType.MAINSAIL name: str = client_config.value display_name: str = name.title() - config_dir: Path = Path.home().joinpath("mainsail-config") + config_dir: Path = BASE_DIR.joinpath("mainsail-config") config_filename: str = "mainsail.cfg" config_section: str = f"include {config_filename}" repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" @@ -39,7 +39,7 @@ class MainsailData(BaseWebClient): client: WebClientType = WebClientType.MAINSAIL name: str = WebClientType.MAINSAIL.value display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("mainsail") + client_dir: Path = BASE_DIR.joinpath("mainsail") config_file: Path = client_dir.joinpath("config.json") repo_path: str = "mainsail-crew/mainsail" nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index 1589147e..79363b1f 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -20,6 +20,66 @@ # current user CURRENT_USER = pwd.getpwuid(os.getuid())[0] + +def _resolve_base_dir() -> Path: + """Resolve the base directory for all component and extension installs. + + Resolution order (first non-empty absolute path wins): + 1. KIAUH_BASE_DIR environment variable + 2. ``base_dir`` option in ``[kiauh]`` section of ``kiauh.cfg`` + 3. ``Path.home()`` (default — preserves upstream behaviour) + + The settings file is read directly here (not via KiauhSettings) to + avoid circular imports — component ``__init__`` modules depend on + ``BASE_DIR`` and are imported before KiauhSettings is constructed. + """ + + # 1. env var — highest priority + env = os.environ.get("KIAUH_BASE_DIR", "").strip() + if env and Path(env).is_absolute(): + return Path(env) + + # 2. settings file — read only the raw value to stay import-safe + try: + # PROJECT_ROOT is two levels up from this file (kiauh/core/constants.py) + project_root = Path(__file__).resolve().parent.parent.parent + # sanity check that we found the right directory + if not (project_root / "kiauh.sh").exists(): + raise FileNotFoundError + cfg_path = project_root / "kiauh.cfg" + if not cfg_path.exists(): + cfg_path = project_root / "default.kiauh.cfg" + if cfg_path.exists(): + with open(cfg_path, "r") as fh: + in_kiauh_section = False + for line in fh: + stripped = line.strip() + if stripped.startswith("["): + in_kiauh_section = stripped == "[kiauh]" + continue + if in_kiauh_section and stripped.startswith("base_dir"): + # handle both "key: value" and "key = value" formats + for sep in (":", "="): + if sep in stripped: + parts = stripped.split(sep, 1) + if len(parts) == 2: + val = parts[1].strip() + if val and Path(val).is_absolute(): + return Path(val) + break + except Exception: + pass # any I/O or parse error → fall through to default + + # 3. default — identical to upstream behaviour + return Path.home() + + +# base directory for all component and extension installations +# Defaults to the current user's home directory. Override with the +# KIAUH_BASE_DIR environment variable **or** the ``base_dir`` option in +# kiauh.cfg to support system-wide installs (e.g. /opt/kiauh, /srv/kiauh). +BASE_DIR: Path = _resolve_base_dir() + # dirs SYSTEMD = Path("/etc/systemd/system") NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py index 00dabda3..9fd8300a 100644 --- a/kiauh/core/services/backup_service.py +++ b/kiauh/core/services/backup_service.py @@ -27,11 +27,13 @@ def __init__(self): @staticmethod def _get_kiauh_root() -> Path: + from core.constants import BASE_DIR + this_file = Path(__file__).resolve() for parent in this_file.parents: if (parent / "kiauh.sh").exists() and (parent / "default.kiauh.cfg").exists(): return parent - return Path.home() + return BASE_DIR @property def backup_root(self) -> Path: @@ -191,21 +193,30 @@ def backup_printer_config_dir(self) -> None: # fallback: search for printer data directories in the user's home directory Logger.print_info("No Klipper instances found via systemd services.") Logger.print_info( - "Attempting to find printer data directories in home directory..." + "Attempting to find printer data directories ..." ) - home_dir = Path.home() - printer_data_dirs = [] + from core.constants import BASE_DIR + + search_dirs = [Path.home()] + if BASE_DIR != Path.home(): + search_dirs.append(BASE_DIR) + + printer_data_dirs: List[Path] = [] + seen: set = set() - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) + for search_dir in search_dirs: + for pattern in ["printer_data", "printer_*_data"]: + for data_dir in search_dir.glob(pattern): + resolved = data_dir.resolve() + if data_dir.is_dir() and resolved not in seen: + seen.add(resolved) + printer_data_dirs.append(data_dir) if not printer_data_dirs: Logger.print_info("Unable to find directory to backup!") Logger.print_info( - "No printer data directories found in home directory." + "No printer data directories found." ) return diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 4f27a90c..2f41ac4a 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -41,6 +41,7 @@ def __init__(self, section: str, option: str, value: str): @dataclass class AppSettings: backup_before_update: bool | None = field(default=None) + base_dir: str | None = field(default=None) @dataclass @@ -157,6 +158,13 @@ def __set_internal_state(self) -> None: self.config.getboolean, False, ) + self.kiauh.base_dir = self.__read_from_cfg( + "kiauh", + "base_dir", + self.config.getval, + None, + True, + ) # parse Klipper options self.klipper.use_python_binary = self.__read_from_cfg( @@ -300,6 +308,13 @@ def __write_internal_state_to_cfg(self) -> None: str(self.kiauh.backup_before_update), ) + if self.kiauh.base_dir is not None: + self.config.set_option( + "kiauh", + "base_dir", + self.kiauh.base_dir, + ) + # Handle repositories if self.klipper.repositories is not None: repos = [f"{repo.url}, {repo.branch}" for repo in self.klipper.repositories] diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py index 58a097bf..4561ec96 100644 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ b/kiauh/extensions/gcode_shell_cmd/__init__.py @@ -9,10 +9,12 @@ from pathlib import Path +from core.constants import BASE_DIR + EXT_MODULE_NAME = "gcode_shell_command.py" MODULE_PATH = Path(__file__).resolve().parent MODULE_ASSETS = MODULE_PATH.joinpath("assets") -KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py index d5a66f88..949ed3ce 100644 --- a/kiauh/extensions/klipper_backup/__init__.py +++ b/kiauh/extensions/klipper_backup/__init__.py @@ -11,9 +11,11 @@ from pathlib import Path +from core.constants import BASE_DIR + EXT_MODULE_NAME = "klipper_backup_extension.py" MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") -KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") -KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") +MOONRAKER_CONF = BASE_DIR.joinpath("printer_data", "config", "moonraker.conf") +KLIPPERBACKUP_DIR = BASE_DIR.joinpath("klipper-backup") +KLIPPERBACKUP_CONFIG_DIR = BASE_DIR.joinpath("config_backup") KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/mobileraker/__init__.py b/kiauh/extensions/mobileraker/__init__.py index 02fa523c..146ee861 100644 --- a/kiauh/extensions/mobileraker/__init__.py +++ b/kiauh/extensions/mobileraker/__init__.py @@ -7,9 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path - -from core.constants import SYSTEMD +from core.constants import BASE_DIR, SYSTEMD # repo MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" @@ -20,8 +18,8 @@ MOBILERAKER_LOG_NAME = "mobileraker.log" # directories -MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") -MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") +MOBILERAKER_DIR = BASE_DIR.joinpath("mobileraker_companion") +MOBILERAKER_ENV_DIR = BASE_DIR.joinpath("mobileraker-env") # files MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py index a6ac1b4f..5ff5ba6e 100644 --- a/kiauh/extensions/obico/__init__.py +++ b/kiauh/extensions/obico/__init__.py @@ -8,6 +8,8 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent # repo @@ -24,8 +26,8 @@ OBICO_MACROS_CFG_NAME = "moonraker_obico_macros.cfg" # directories -OBICO_DIR = Path.home().joinpath("moonraker-obico") -OBICO_ENV_DIR = Path.home().joinpath("moonraker-obico-env") +OBICO_DIR = BASE_DIR.joinpath("moonraker-obico") +OBICO_ENV_DIR = BASE_DIR.joinpath("moonraker-obico-env") # files OBICO_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_SERVICE_NAME}") diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py index a4767bc4..284146df 100644 --- a/kiauh/extensions/octoapp/__init__.py +++ b/kiauh/extensions/octoapp/__init__.py @@ -6,21 +6,21 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path +from core.constants import BASE_DIR # repo 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_DIR = BASE_DIR.joinpath("octoapp") +OA_ENV_DIR = BASE_DIR.joinpath("octoapp-env") # files OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json") OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh") OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh") -OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log") +OA_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoapp-installer.log") # filenames OA_CFG_NAME = "octoapp.conf" diff --git a/kiauh/extensions/octoeverywhere/__init__.py b/kiauh/extensions/octoeverywhere/__init__.py index 1870e625..57adbbe2 100644 --- a/kiauh/extensions/octoeverywhere/__init__.py +++ b/kiauh/extensions/octoeverywhere/__init__.py @@ -6,14 +6,14 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from pathlib import Path +from core.constants import BASE_DIR # repo OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" # directories -OE_DIR = Path.home().joinpath("octoeverywhere") -OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") +OE_DIR = BASE_DIR.joinpath("octoeverywhere") +OE_ENV_DIR = BASE_DIR.joinpath("octoeverywhere-env") OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") # files @@ -21,7 +21,7 @@ OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") -OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") +OE_INSTALLER_LOG_FILE = BASE_DIR.joinpath("octoeverywhere-installer.log") # filenames OE_CFG_NAME = "octoeverywhere.conf" diff --git a/kiauh/extensions/octoprint/octoprint.py b/kiauh/extensions/octoprint/octoprint.py index c003efe5..925f99ca 100644 --- a/kiauh/extensions/octoprint/octoprint.py +++ b/kiauh/extensions/octoprint/octoprint.py @@ -13,7 +13,7 @@ from textwrap import dedent from components.klipper.klipper import Klipper -from core.constants import CURRENT_USER +from core.constants import BASE_DIR, CURRENT_USER from core.instance_manager.base_instance import BaseInstance from core.logger import Logger from extensions.octoprint import ( @@ -43,17 +43,17 @@ def __post_init__(self): # OctoPrint stores its data under ~/.octoprint[_SUFFIX] self.basedir = ( - Path.home().joinpath(OP_BASEDIR_PREFIX) + BASE_DIR.joinpath(OP_BASEDIR_PREFIX) if self.suffix == "" - else Path.home().joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") + else BASE_DIR.joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") ) self.cfg_file = self.basedir.joinpath("config.yaml") # OctoPrint virtualenv lives under ~/OctoPrint[_SUFFIX] self.env_dir = ( - Path.home().joinpath(OP_ENV_PREFIX) + BASE_DIR.joinpath(OP_ENV_PREFIX) if self.suffix == "" - else Path.home().joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") + else BASE_DIR.joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") ) def create(self, port: int) -> None: diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py index 0a4b6927..768ea4c1 100644 --- a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py +++ b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py @@ -10,7 +10,7 @@ from pathlib import Path from components.webui_client.client_utils import create_nginx_cfg -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED +from core.constants import BASE_DIR, NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.logger import DialogType, Logger from extensions.base_extension import BaseExtension from utils.common import check_install_dependencies @@ -22,7 +22,7 @@ from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr MODULE_PATH = Path(__file__).resolve().parent -PGC_DIR = Path.home().joinpath("pgcode") +PGC_DIR = BASE_DIR.joinpath("pgcode") PGC_REPO = "https://github.com/Kragrathea/pgcode" PGC_CONF = "pgcode.local.conf" diff --git a/kiauh/extensions/spoolman/__init__.py b/kiauh/extensions/spoolman/__init__.py index 4f7d628d..8c049105 100644 --- a/kiauh/extensions/spoolman/__init__.py +++ b/kiauh/extensions/spoolman/__init__.py @@ -8,9 +8,11 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest" -SPOOLMAN_DIR = Path.home().joinpath("spoolman") +SPOOLMAN_DIR = BASE_DIR.joinpath("spoolman") SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data") SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml") SPOOLMAN_DEFAULT_PORT = 7912 diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py index 773f725e..819b8e65 100644 --- a/kiauh/extensions/telegram_bot/__init__.py +++ b/kiauh/extensions/telegram_bot/__init__.py @@ -8,6 +8,8 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + MODULE_PATH = Path(__file__).resolve().parent # repo @@ -20,8 +22,8 @@ TG_BOT_ENV_FILE_NAME = "moonraker-telegram-bot.env" # directories -TG_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") -TG_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") +TG_BOT_DIR = BASE_DIR.joinpath("moonraker-telegram-bot") +TG_BOT_ENV = BASE_DIR.joinpath("moonraker-telegram-bot-env") # files TG_BOT_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_SERVICE_NAME}") diff --git a/kiauh/extensions/tmc_autotune/__init__.py b/kiauh/extensions/tmc_autotune/__init__.py index 9438829b..07187137 100644 --- a/kiauh/extensions/tmc_autotune/__init__.py +++ b/kiauh/extensions/tmc_autotune/__init__.py @@ -8,13 +8,15 @@ # ======================================================================= # from pathlib import Path +from core.constants import BASE_DIR + # repo TMCA_REPO = "https://github.com/andrewmcgr/klipper_tmc_autotune" # directories -TMCA_DIR = Path.home().joinpath("klipper_tmc_autotune") +TMCA_DIR = BASE_DIR.joinpath("klipper_tmc_autotune") MODULE_PATH = Path(__file__).resolve().parent -KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_DIR = BASE_DIR.joinpath("klipper") KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") KLIPPER_PLUGINS = KLIPPER_DIR.joinpath("klippy/plugins") KLIPPER_EXTENSIONS_PATH = ( diff --git a/kiauh/main.py b/kiauh/main.py index 3832ba69..2ec6c0f5 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -8,7 +8,9 @@ # ======================================================================= # import io import sys +from pathlib import Path +from core.constants import BASE_DIR from core.logger import Logger from core.menus.main_menu import MainMenu from core.settings.kiauh_settings import KiauhSettings @@ -24,6 +26,10 @@ def main() -> None: try: KiauhSettings() ensure_encoding() + + if BASE_DIR != Path.home(): + Logger.print_info(f"Using custom base directory: {BASE_DIR}") + MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 0d141715..035946e8 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -18,6 +18,7 @@ from typing import List from zipfile import ZipFile +from core.constants import BASE_DIR from core.decorators import deprecated from core.logger import Logger @@ -169,6 +170,6 @@ def get_data_dir(instance_type: type, suffix: str) -> Path: if suffix != "": # this is the new data dir naming scheme introduced in v6.0.0 - return Path.home().joinpath(f"printer_{suffix}_data") + return BASE_DIR.joinpath(f"printer_{suffix}_data") - return Path.home().joinpath("printer_data") + return BASE_DIR.joinpath("printer_data") diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py index 02354aa6..058d5feb 100644 --- a/kiauh/utils/sys_utils.py +++ b/kiauh/utils/sys_utils.py @@ -417,20 +417,29 @@ def download_progress(block_num, block_size, total_size) -> None: def set_nginx_permissions() -> None: """ - Check if permissions of the users home directory + Check if permissions of the users home directory and base directory grant execution rights to group and other and set them if not set. Required permissions for NGINX to be able to serve Mainsail/Fluidd. This seems to have become necessary with Ubuntu 21+. | :return: None """ - cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" - homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) - permissions = homedir_perm.stdout + import shlex - if permissions.count("x") < 3: - Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", Path.home()]) - Logger.print_ok("Permissions granted.") + from core.constants import BASE_DIR + + dirs_to_check = [Path.home()] + if BASE_DIR != Path.home(): + dirs_to_check.append(BASE_DIR) + + for check_dir in dirs_to_check: + cmd = f"ls -ld {shlex.quote(str(check_dir))} | cut -d' ' -f1" + dir_perm = run(cmd, shell=True, stdout=PIPE, text=True) + permissions = dir_perm.stdout + + if permissions.count("x") < 3: + Logger.print_status("Granting NGINX the required permissions ...") + run(["chmod", "og+x", str(check_dir)]) + Logger.print_ok("Permissions granted.") def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: