"""PiCopy – Konfiguration, Pfade, Konstanten, Logging.""" import os import json import logging from pathlib import Path RAW_BASE = 'https://git.leuschner.dev/Tobias/PiCopy/raw/branch/main' VERSION_FILE = Path(__file__).parent.parent / 'version.txt' def load_installed_version(): try: return VERSION_FILE.read_text(encoding='utf-8').strip() or '1.0.4' except Exception: return 'X.X.X' VERSION = load_installed_version() BASE_DIR = Path('/opt/picopy') CONFIG_FILE = BASE_DIR / 'config.json' STATE_FILE = BASE_DIR / 'state.json' LOG_DIR = BASE_DIR / 'logs' LOG_FILE = LOG_DIR / 'picopy.log' INTERNAL_DEST_DIR = BASE_DIR / 'internal' LOG_DIR.mkdir(parents=True, exist_ok=True) HISTORY_FILE = BASE_DIR / 'history.json' MAX_HISTORY = 100 logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()] ) log = logging.getLogger('picopy') NM_AP_CON = 'PiCopy-AP' NM_CLIENT_CON = 'PiCopy-WiFi' WIFI_BOOT_WAIT = 25 # Sekunden warten beim Start bevor AP gestartet wird DEFAULT_CONFIG = { # USB 'source_ports': [], # [{port, label}, ...] 'source_port': None, 'source_label': '', # Migration legacy 'dest_port': None, 'dest_label': '', 'dest_type': 'usb', 'internal_dest_label': 'Interner Speicher', 'internal_share_enabled': False, 'folder_format': '%Y-%m-%d', 'add_time': True, 'subfolder': True, 'auto_copy': True, 'file_filter': '', 'exclude_system': True, 'duplicate_handling': 'skip', 'verify_checksum': False, 'delete_source': False, # WiFi 'wifi_ssid': '', 'wifi_password': '', 'ap_ssid': 'PiCopy', 'ap_password': 'PiCopy,', # WireGuard 'wireguard_auto': False, } def load_cfg(): cfg = DEFAULT_CONFIG.copy() try: if CONFIG_FILE.exists(): cfg.update(json.loads(CONFIG_FILE.read_text(encoding='utf-8'))) except (json.JSONDecodeError, ValueError) as e: log.error(f'config.json korrupt ({e}), verwende Standardwerte') try: CONFIG_FILE.rename(CONFIG_FILE.with_suffix('.corrupt')) except Exception: pass except Exception as e: log.warning(f'config.json nicht lesbar: {e}') return cfg def save_cfg(cfg): _atomic_write(CONFIG_FILE, json.dumps(cfg, indent=2)) def _atomic_write(path: Path, content: str) -> None: """Schreibt atomar: erst .tmp, dann os.replace() - sicher bei Stromausfall.""" tmp = path.with_suffix(path.suffix + '.tmp') try: tmp.write_text(content, encoding='utf-8') with open(tmp, 'rb') as fh: os.fsync(fh.fileno()) # Daten wirklich auf Datenträger schreiben os.replace(str(tmp), str(path)) # Atomares Umbenennen (POSIX-Garantie) except Exception: try: tmp.unlink(missing_ok=True) except Exception: pass raise def _fmt_bytes(b): if b < 1024: return f'{b} B' if b < 1024**2: return f'{b/1024:.1f} KB' if b < 1024**3: return f'{b/1024**2:.1f} MB' return f'{b/1024**3:.2f} GB'