Files
bak-and-rest/src/bakrest/tray_app.py

161 lines
5.5 KiB
Python

from __future__ import annotations
import argparse
import json
import logging
import threading
import time
import tkinter as tk
from tkinter import messagebox
from pathlib import Path
from PIL import Image, ImageDraw
import pystray
from .backup import run_backup, shutdown_after_backup
from .backup_nogui import run_nogui_backup
from .config import AppConfig, load_config
from .logging_setup import configure_logging
from .registry import ChangeRegistry
class BakRestTrayApp:
def __init__(self, config: AppConfig, logger: logging.Logger) -> None:
self.config = config
self.logger = logger
self.registry = ChangeRegistry(config.storage.change_registry)
self.stop_event = threading.Event()
self.last_alert_reachable: bool | None = None
self.icon = pystray.Icon(
"BakRestTray",
_create_icon_image(),
"Bak&Rest",
self._build_menu(),
)
def run(self) -> None:
self.logger.info("BakRestTray started")
threading.Thread(target=self._server_status_monitor, daemon=True).start()
self.icon.run()
def _build_menu(self) -> pystray.Menu:
return pystray.Menu(
pystray.MenuItem(lambda item: self._status_text(), None, enabled=False),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Backup e spegni", self._backup_and_shutdown),
pystray.MenuItem("Aggiorna stato", self._refresh),
pystray.MenuItem("Esci", self._quit),
)
def _status_text(self) -> str:
pending = self.registry.pending_count()
server = self._read_server_status()
return f"Server: {server} | File in attesa: {pending}"
def _read_server_status(self) -> str:
status = self._read_server_status_data()
if status is None:
return "sconosciuto"
return "raggiungibile" if status.get("reachable") else "non raggiungibile"
def _read_server_status_data(self) -> dict[str, object] | None:
path = self.config.storage.status_file
if not path.exists():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
def _backup_and_shutdown(self, icon: pystray.Icon, item: pystray.MenuItem) -> None:
threading.Thread(target=self._backup_and_shutdown_worker, daemon=True).start()
def _backup_and_shutdown_worker(self) -> None:
self.logger.info("User requested Backup e spegni")
self.icon.title = "Bak&Rest - backup in corso"
result = run_backup(self.config, self.logger)
if result.success:
self.icon.title = "Bak&Rest - backup completato"
shutdown_after_backup(self.config, self.logger)
else:
self.icon.title = "Bak&Rest - backup fallito"
if result.error_code == "server_unreachable":
_show_error_message("Bak&Rest", "Server non raggiungibile. E' spento?")
else:
_show_error_message("Bak&Rest", result.message)
self.icon.update_menu()
def _refresh(self, icon: pystray.Icon, item: pystray.MenuItem) -> None:
self.icon.update_menu()
def _quit(self, icon: pystray.Icon, item: pystray.MenuItem) -> None:
self.logger.info("BakRestTray stopped")
self.stop_event.set()
self.icon.stop()
def _server_status_monitor(self) -> None:
interval = min(max(self.config.backup.server_check.interval_seconds, 60), 600)
while not self.stop_event.wait(interval):
status = self._read_server_status_data()
if status is None:
continue
reachable = bool(status.get("reachable"))
if reachable:
self.last_alert_reachable = True
continue
if self.last_alert_reachable is not False:
self.last_alert_reachable = False
self.logger.warning("Showing backup server unreachable alert")
_show_error_message(
"Bak&Rest",
"Accendi il server di backup o verifica che sia connesso alla rete",
)
def _create_icon_image() -> Image.Image:
image = Image.new("RGBA", (64, 64), (26, 38, 52, 255))
draw = ImageDraw.Draw(image)
draw.rounded_rectangle((10, 14, 54, 50), radius=8, fill=(77, 163, 255, 255))
draw.rectangle((18, 24, 46, 44), fill=(245, 248, 252, 255))
draw.line((22, 34, 32, 42, 46, 26), fill=(26, 38, 52, 255), width=5)
return image
def _show_error_message(title: str, message: str) -> None:
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
try:
messagebox.showerror(title, message, parent=root)
finally:
root.destroy()
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Bak&Rest tray app")
parser.add_argument(
"--nogui",
action="store_true",
help="Run backup and shutdown without tray icon",
)
parser.add_argument(
"--no-shutdown",
action="store_true",
help="With --nogui, run the backup but do not shut down the computer",
)
args = parser.parse_args(argv)
if args.nogui:
return run_nogui_backup(shutdown=not args.no_shutdown)
config = load_config()
logger = configure_logging(config.storage.logs_dir, "tray")
app = BakRestTrayApp(config, logger)
app.run()
return 0
if __name__ == "__main__":
raise SystemExit(main())