Files
nosys_libs/app/updater.py
2026-01-25 13:55:46 +10:00

135 lines
5.1 KiB
Python

import sys
import subprocess
import urllib.request
import os
import json
import types
import logging
import time
import zipfile
from pathlib import Path
from threading import Thread
from libs.app.common.config import Config
from libs.app.common.logging import get_logger
from libs.app.common.paths import LIBS_DIR
logger = get_logger("updater", buffer=True)
class Updater:
"""
Updater class responsible for managing library updates.
"""
def __init__(self, config:Config):
self.config = config
self.repositories = self.config.get("app", "repositories", default=[])
self.updated_libs: dict[str, any] = {}
self._event_listeners: list[callable] = []
def update_libs(self):
"""Check and update all configured libraries in parallel."""
logger.debug("Checking libraries updates...")
threads: list[Thread] = []
for lib in self.config.get("app", "libs", default=[]):
lib_id = lib.get("id")
self._update_lib_status(lib_id, "Reading configuration")
if lib.get("update", {"checkUpdates":True}).get("checkUpdates", True):
self._update_lib_status(lib_id, "Checking updates")
thread = Thread(target=self.check_lib_update, args=(lib,))
thread.start()
threads.append(thread)
else:
self._update_lib_status(lib_id, "Checking updates disabled")
for thread in threads:
thread.join()
return self.updated_libs
def check_lib_update(self, lib:dict):
"""
Check for a library update in repositories and install if newer version is available.
"""
lib_id = lib.get("id")
lib_config = self.config.get(lib_id)
lib_repositories = lib.get("update", {"repositories":[]}).get("repositories", [])
lib_path = LIBS_DIR / lib_id
if not lib_config:
Path(lib_path).mkdir(parents=True, exist_ok=True)
current_version = 0
else:
current_version = lib_config.get("info").get("version")
logger.debug(f"Checking updates of lib {lib_id} with current version {current_version}")
repositories = list(self.repositories)
repositories.extend(lib_repositories)
latest_version = (current_version, None)
for repo in repositories:
try:
info_url = f"{repo}/{lib_id}/info.json"
logger.debug(f"Reading remote info of {lib_id} from repository {repo}")
with urllib.request.urlopen(info_url, timeout=2) as response:
remote_info = json.load(response)
logger.debug(f"{lib_id} remote info: {remote_info}")
remote_version = remote_info.get("version")
if remote_version > latest_version[0]:
latest_version = (remote_version, f"{repo}/{lib_id}")
except Exception as e:
logger.error(f"Error reading remote info of {lib_id} from {repo}: {e}")
if latest_version[1]:
lib_url = f"{latest_version[1]}/{lib_id}.zip"
lib_file = LIBS_DIR / f"{lib_id}.zip"
logger.debug(f"Downloading lib from {lib_url} to {lib_file}")
self._update_lib_status(lib_id, f"Downloading lastest lib version {latest_version[0]}")
self.download_lib(lib_url, lib_file)
self._update_lib_status(lib_id, f"Installing lib requirements")
self.pip_install_requirements(lib_path)
self._update_lib_status(lib_id, f"Success updated")
else:
self._update_lib_status(lib_id, f"Current version is the latest")
def download_lib(self, url: str, path: str) -> None:
"""Download and extract a lib from repository."""
urllib.request.urlretrieve(url, path)
logger.info(f"Extracting all from {path}")
with zipfile.ZipFile(path, "r") as zip_ref:
zip_ref.extractall(Path(path).parent.absolute())
def pip_install_requirements(self, lib_path: str) -> None:
"""Run pip install on requirements.txt if present."""
requirements_path = os.path.join(lib_path, "requirements.txt")
if os.path.exists(requirements_path):
logger.info(f"Installing requirements from {requirements_path}")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_path])
else:
logger.info(f"No requirements.txt found for {lib_path}")
def register_listener(self, callback: callable):
"""
Register a listener callback that will be called with
each new event.
"""
if callback not in self._event_listeners:
self._event_listeners.append(callback)
def emit_event(self, **kwargs):
event = types.SimpleNamespace()
for k, v in kwargs.items():
setattr(event, k, v)
for listener in self._event_listeners:
listener(event)
def _update_lib_status(self, lib, status):
self.updated_libs[lib] = status
self.emit_event(name="status_lib", lib=lib, status=status)