mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-15 03:24:29 +05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9459bd68e | ||
|
|
ee460663c9 | ||
|
|
6f0e0146ef | ||
|
|
229f317025 | ||
|
|
48c0ae7227 | ||
|
|
9c7b5fcb10 | ||
|
|
191bdd4874 |
@@ -123,7 +123,7 @@ def create_example_moonraker_conf(
|
|||||||
scp = SimpleConfigParser()
|
scp = SimpleConfigParser()
|
||||||
scp.read_file(target)
|
scp.read_file(target)
|
||||||
trusted_clients: List[str] = [
|
trusted_clients: List[str] = [
|
||||||
f" {'.'.join(ip)}\n",
|
f"{'.'.join(ip)}",
|
||||||
*scp.getvals("authorization", "trusted_clients"),
|
*scp.getvals("authorization", "trusted_clients"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
from json import JSONDecodeError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, CalledProcessError, run
|
from subprocess import PIPE, CalledProcessError, run
|
||||||
from typing import List, get_args
|
from typing import List, get_args
|
||||||
@@ -151,18 +152,36 @@ def symlink_webui_nginx_log(
|
|||||||
def get_local_client_version(client: BaseWebClient) -> str | None:
|
def get_local_client_version(client: BaseWebClient) -> str | None:
|
||||||
relinfo_file = client.client_dir.joinpath("release_info.json")
|
relinfo_file = client.client_dir.joinpath("release_info.json")
|
||||||
version_file = client.client_dir.joinpath(".version")
|
version_file = client.client_dir.joinpath(".version")
|
||||||
|
default = "n/a"
|
||||||
|
|
||||||
if not client.client_dir.exists():
|
if not client.client_dir.exists():
|
||||||
return None
|
return default
|
||||||
if not relinfo_file.is_file() and not version_file.is_file():
|
|
||||||
return "n/a"
|
|
||||||
|
|
||||||
|
# try to get version from release_info.json first
|
||||||
if relinfo_file.is_file():
|
if relinfo_file.is_file():
|
||||||
with open(relinfo_file, "r") as f:
|
try:
|
||||||
return str(json.load(f)["version"])
|
if relinfo_file.stat().st_size == 0:
|
||||||
else:
|
raise JSONDecodeError("Empty file", "", 0)
|
||||||
with open(version_file, "r") as f:
|
with open(relinfo_file, "r", encoding="utf-8") as f:
|
||||||
return f.readlines()[0]
|
data = json.load(f)
|
||||||
|
raw_version = data.get("version")
|
||||||
|
if raw_version is not None:
|
||||||
|
parsed = str(raw_version).strip()
|
||||||
|
if parsed:
|
||||||
|
return parsed
|
||||||
|
except (JSONDecodeError, OSError):
|
||||||
|
Logger.print_error("Invalid 'release_info.json'")
|
||||||
|
|
||||||
|
# fallback to .version file
|
||||||
|
if version_file.is_file():
|
||||||
|
try:
|
||||||
|
with open(version_file, "r") as f:
|
||||||
|
line = f.readline().strip()
|
||||||
|
return line or default
|
||||||
|
except OSError:
|
||||||
|
Logger.print_error("Unable to read '.version'")
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def get_remote_client_version(client: BaseWebClient) -> str | None:
|
def get_remote_client_version(client: BaseWebClient) -> str | None:
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class BackupMenu(BaseMenu):
|
|||||||
|
|
||||||
def print_menu(self) -> None:
|
def print_menu(self) -> None:
|
||||||
line1 = Color.apply(
|
line1 = Color.apply(
|
||||||
"INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW
|
"INFO: Backups are located in '~/kiauh_backups'", Color.YELLOW
|
||||||
)
|
)
|
||||||
menu = textwrap.dedent(
|
menu = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ class BackupService:
|
|||||||
target_name
|
target_name
|
||||||
or f"{source_path.stem}_{self.timestamp}{source_path.suffix}"
|
or f"{source_path.stem}_{self.timestamp}{source_path.suffix}"
|
||||||
)
|
)
|
||||||
if target_path is not None:
|
|
||||||
backup_path = self._backup_root.joinpath(target_path, filename)
|
|
||||||
else:
|
|
||||||
backup_path = self._backup_root.joinpath(filename)
|
|
||||||
|
|
||||||
backup_path.mkdir(parents=True, exist_ok=True)
|
backup_dir = self._backup_root
|
||||||
shutil.copy2(source_path, backup_path)
|
if target_path is not None:
|
||||||
|
backup_dir = self._backup_root.joinpath(target_path)
|
||||||
|
|
||||||
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(source_path, backup_dir.joinpath(filename))
|
||||||
|
|
||||||
Logger.print_ok(
|
Logger.print_ok(
|
||||||
f"Successfully backed up '{source_path}' to '{backup_path}'"
|
f"Successfully backed up '{source_path}' to '{backup_dir}'"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -109,7 +109,16 @@ class BackupService:
|
|||||||
else:
|
else:
|
||||||
backup_path = self._backup_root.joinpath(backup_dir_name)
|
backup_path = self._backup_root.joinpath(backup_dir_name)
|
||||||
|
|
||||||
shutil.copytree(source_path, backup_path)
|
if backup_path.exists():
|
||||||
|
Logger.print_info(f"Reusing existing backup directory '{backup_path}'")
|
||||||
|
|
||||||
|
shutil.copytree(
|
||||||
|
source_path,
|
||||||
|
backup_path,
|
||||||
|
dirs_exist_ok=True,
|
||||||
|
symlinks=True,
|
||||||
|
ignore_dangling_symlinks=True,
|
||||||
|
)
|
||||||
|
|
||||||
Logger.print_ok(
|
Logger.print_ok(
|
||||||
f"Successfully backed up '{source_path}' to '{backup_path}'"
|
f"Successfully backed up '{source_path}' to '{backup_path}'"
|
||||||
|
|||||||
@@ -254,32 +254,34 @@ class KiauhSettings:
|
|||||||
section: str,
|
section: str,
|
||||||
option: str,
|
option: str,
|
||||||
getter: Callable[[str, str, T | None], T],
|
getter: Callable[[str, str, T | None], T],
|
||||||
fallback: T = None,
|
fallback: T | None = None,
|
||||||
silent: bool = False,
|
silent: bool = False,
|
||||||
) -> T:
|
) -> T | None:
|
||||||
if not self.__check_option_exists(section, option, fallback, silent):
|
if not self.__check_option_exists(section, option, fallback, silent):
|
||||||
return fallback
|
return fallback
|
||||||
return getter(section, option, fallback)
|
return getter(section, option, fallback)
|
||||||
|
|
||||||
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
|
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
|
||||||
_repos: List[Repository] = []
|
_repos: List[Repository] = []
|
||||||
for repo in repos:
|
for raw in repos:
|
||||||
try:
|
line = raw.strip()
|
||||||
if repo.strip().startswith("#") or repo.strip().startswith(";"):
|
|
||||||
continue
|
|
||||||
if "," in repo:
|
|
||||||
url, branch = repo.strip().split(",")
|
|
||||||
|
|
||||||
if not branch:
|
if not line or line.startswith("#") or line.startswith(";"):
|
||||||
branch = "master"
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "," in line:
|
||||||
|
url_part, branch_part = line.split(",")
|
||||||
|
url = url_part.strip()
|
||||||
|
branch = branch_part.strip() or "master"
|
||||||
else:
|
else:
|
||||||
url = repo.strip()
|
url = line
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|
||||||
# url must not be empty otherwise it's considered
|
# url must not be empty otherwise it's considered
|
||||||
# as an unrecoverable, invalid configuration
|
# as an unrecoverable, invalid configuration
|
||||||
if not url:
|
if not url:
|
||||||
raise InvalidValueError(section, "repositories", repo)
|
raise InvalidValueError(section, "repositories", line)
|
||||||
|
|
||||||
_repos.append(Repository(url.strip(), branch.strip()))
|
_repos.append(Repository(url.strip(), branch.strip()))
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import re
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
from typing import Any, Callable, Dict, List, Set, Union
|
||||||
|
|
||||||
# definition of section line:
|
# definition of section line:
|
||||||
# - the line MUST start with an opening square bracket - it is the first section marker
|
# - the line MUST start with an opening square bracket - it is the first section marker
|
||||||
@@ -91,6 +91,9 @@ class LineType(Enum):
|
|||||||
BLANK = "blank"
|
BLANK = "blank"
|
||||||
|
|
||||||
|
|
||||||
|
_UNSET = object()
|
||||||
|
|
||||||
|
|
||||||
class NoSectionError(Exception):
|
class NoSectionError(Exception):
|
||||||
"""Raised when a section is not defined"""
|
"""Raised when a section is not defined"""
|
||||||
|
|
||||||
@@ -342,7 +345,7 @@ class SimpleConfigParser:
|
|||||||
for line in file:
|
for line in file:
|
||||||
self._parse_line(line)
|
self._parse_line(line)
|
||||||
|
|
||||||
def write_file(self, path: Union[str, Path]) -> None:
|
def write_file(self, path: str | Path) -> None:
|
||||||
"""Write the config to a file"""
|
"""Write the config to a file"""
|
||||||
if path is None:
|
if path is None:
|
||||||
raise ValueError("File path cannot be None")
|
raise ValueError("File path cannot be None")
|
||||||
@@ -418,9 +421,7 @@ class SimpleConfigParser:
|
|||||||
"""Check if an option exists in a section"""
|
"""Check if an option exists in a section"""
|
||||||
return self.has_section(section) and option in self.get_options(section)
|
return self.has_section(section) and option in self.get_options(section)
|
||||||
|
|
||||||
def set_option(
|
def set_option(self, section: str, option: str, value: str | List[str]) -> None:
|
||||||
self, section: str, option: str, value: Union[str, List[str]]
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Set the value of an option in a section. If the section does not exist,
|
Set the value of an option in a section. If the section does not exist,
|
||||||
it is created. If the option does not exist, it is created.
|
it is created. If the option does not exist, it is created.
|
||||||
@@ -467,8 +468,8 @@ class SimpleConfigParser:
|
|||||||
elif opt and isinstance(opt, Option) and isinstance(value, str):
|
elif opt and isinstance(opt, Option) and isinstance(value, str):
|
||||||
curr_val = opt.value
|
curr_val = opt.value
|
||||||
new_val = value
|
new_val = value
|
||||||
opt.value = value
|
opt.value = new_val
|
||||||
opt.raw.replace(curr_val, new_val)
|
opt.raw = opt.raw.replace(curr_val, new_val)
|
||||||
|
|
||||||
elif opt and isinstance(opt, MultiLineOption) and isinstance(value, list):
|
elif opt and isinstance(opt, MultiLineOption) and isinstance(value, list):
|
||||||
# note: we completely replace the existing values
|
# note: we completely replace the existing values
|
||||||
@@ -561,7 +562,7 @@ class SimpleConfigParser:
|
|||||||
else self._find_option_by_name(option, section=sects[0])
|
else self._find_option_by_name(option, section=sects[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
def getval(self, section: str, option: str, fallback: Optional[str] = None) -> str:
|
def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
|
||||||
"""
|
"""
|
||||||
Return the value of the given option in the given section
|
Return the value of the given option in the given section
|
||||||
|
|
||||||
@@ -576,12 +577,12 @@ class SimpleConfigParser:
|
|||||||
return opt.value if opt else ""
|
return opt.value if opt else ""
|
||||||
|
|
||||||
except (NoSectionError, NoOptionError):
|
except (NoSectionError, NoOptionError):
|
||||||
if fallback is None:
|
if fallback is _UNSET:
|
||||||
raise
|
raise
|
||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
def getvals(
|
def getvals(
|
||||||
self, section: str, option: str, fallback: Optional[List[str]] = None
|
self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the values of the given multi-line option in the given section
|
Return the values of the given multi-line option in the given section
|
||||||
@@ -597,22 +598,22 @@ class SimpleConfigParser:
|
|||||||
return [v.value for v in opt.values] if opt else []
|
return [v.value for v in opt.values] if opt else []
|
||||||
|
|
||||||
except (NoSectionError, NoOptionError):
|
except (NoSectionError, NoOptionError):
|
||||||
if fallback is None:
|
if fallback is _UNSET:
|
||||||
raise
|
raise
|
||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
def getint(self, section: str, option: str, fallback: Optional[int] = None) -> int:
|
def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int:
|
||||||
"""Return the value of the given option in the given section as an int"""
|
"""Return the value of the given option in the given section as an int"""
|
||||||
return self._get_conv(section, option, int, fallback=fallback)
|
return self._get_conv(section, option, int, fallback=fallback)
|
||||||
|
|
||||||
def getfloat(
|
def getfloat(
|
||||||
self, section: str, option: str, fallback: Optional[float] = None
|
self, section: str, option: str, fallback: float | _UNSET = _UNSET
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Return the value of the given option in the given section as a float"""
|
"""Return the value of the given option in the given section as a float"""
|
||||||
return self._get_conv(section, option, float, fallback=fallback)
|
return self._get_conv(section, option, float, fallback=fallback)
|
||||||
|
|
||||||
def getboolean(
|
def getboolean(
|
||||||
self, section: str, option: str, fallback: Optional[bool] = None
|
self, section: str, option: str, fallback: bool | _UNSET = _UNSET
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return the value of the given option in the given section as a boolean"""
|
"""Return the value of the given option in the given section as a boolean"""
|
||||||
return self._get_conv(
|
return self._get_conv(
|
||||||
@@ -631,14 +632,14 @@ class SimpleConfigParser:
|
|||||||
self,
|
self,
|
||||||
section: str,
|
section: str,
|
||||||
option: str,
|
option: str,
|
||||||
conv: Callable[[str], Union[int, float, bool]],
|
conv: Callable[[str], int | float | bool],
|
||||||
fallback: Optional[Any] = None,
|
fallback: Any = _UNSET,
|
||||||
) -> Union[int, float, bool]:
|
) -> int | float | bool:
|
||||||
"""Return the value of the given option in the given section as a converted value"""
|
"""Return the value of the given option in the given section as a converted value"""
|
||||||
try:
|
try:
|
||||||
return conv(self.getval(section, option, fallback))
|
return conv(self.getval(section, option, fallback))
|
||||||
except (ValueError, TypeError, AttributeError) as e:
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
if fallback is not None:
|
if fallback is not _UNSET:
|
||||||
return fallback
|
return fallback
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Cannot convert {self.getval(section, option)} to {conv.__name__}"
|
f"Cannot convert {self.getval(section, option)} to {conv.__name__}"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
# ======================================================================= #
|
# ======================================================================= #
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
from subprocess import CalledProcessError, run
|
from subprocess import CalledProcessError, run
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
@@ -311,13 +312,19 @@ class SpoolmanExtension(BaseExtension):
|
|||||||
mrsvc.load_instances()
|
mrsvc.load_instances()
|
||||||
mr_instances = mrsvc.get_all_instances()
|
mr_instances = mrsvc.get_all_instances()
|
||||||
for instance in mr_instances:
|
for instance in mr_instances:
|
||||||
asvc_path = instance.data_dir.joinpath("moonraker.asvc")
|
asvc_path: Path = instance.data_dir.joinpath("moonraker.asvc")
|
||||||
if asvc_path.exists():
|
if asvc_path.exists() and asvc_path.is_file():
|
||||||
if "Spoolman" in open(asvc_path).read():
|
with open(asvc_path, "a+") as f:
|
||||||
Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...")
|
if "Spoolman" in f.read():
|
||||||
continue
|
Logger.print_info(
|
||||||
|
f"Spoolman already in {asvc_path}. Skipping..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
content: List[str] = f.readlines()
|
||||||
|
if content and not content[-1].endswith("\n"):
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
with open(asvc_path, "a") as f:
|
|
||||||
f.write("Spoolman\n")
|
f.write("Spoolman\n")
|
||||||
|
|
||||||
Logger.print_ok(f"Spoolman added to {asvc_path}!")
|
Logger.print_ok(f"Spoolman added to {asvc_path}!")
|
||||||
|
|||||||
Reference in New Issue
Block a user