from pathlib import Path from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from .db import db, utc_now def _normalize_timezone(value: str) -> str: timezone_name = value.strip() if not timezone_name: return "UTC" if timezone_name.startswith("/"): marker = "/zoneinfo/" if marker in timezone_name: timezone_name = timezone_name.split(marker, 1)[1] else: timezone_name = timezone_name.lstrip("/") try: ZoneInfo(timezone_name) except (ValueError, ZoneInfoNotFoundError): return "UTC" return timezone_name def _system_timezone() -> str: timezone_file = Path("/etc/timezone") if timezone_file.exists(): value = timezone_file.read_text().strip() if value: return _normalize_timezone(value) return "UTC" DEFAULT_SETTINGS = { "setup_complete": "false", "proxmox_node": "", "proxmox_storage": "", "local_backup_dir": "/var/lib/vz/dump", "rclone_path": "/usr/bin/rclone", "rclone_remote": "", "rclone_remote_path": "pve-cloud-backup", "discord_webhook_url": "", "allowed_cors_origins": "http://localhost:5173,http://127.0.0.1:5173", "default_compression": "zstd", "default_backup_mode": "snapshot", "default_retention_type": "days", "default_retention_value": "30", "max_concurrent_backups": "1", "timezone": _system_timezone(), } def run_migrations() -> None: with db() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL ) """ ) applied = { row["version"] for row in conn.execute("SELECT version FROM schema_migrations").fetchall() } if 1 not in applied: conn.executescript( """ CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE backup_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, guest_vmid INTEGER NOT NULL, guest_name TEXT NOT NULL, guest_type TEXT NOT NULL CHECK (guest_type IN ('vm','lxc')), node TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, cron_schedule TEXT NOT NULL, proxmox_storage TEXT NOT NULL, backup_mode TEXT NOT NULL, compression TEXT NOT NULL, retention_type TEXT NOT NULL CHECK (retention_type IN ('days','latest')), retention_value INTEGER NOT NULL, next_run_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE backups ( id TEXT PRIMARY KEY, job_id INTEGER, guest_vmid INTEGER NOT NULL, guest_name TEXT NOT NULL, guest_type TEXT NOT NULL, node TEXT NOT NULL, state TEXT NOT NULL, upid TEXT, local_path TEXT, remote_path TEXT, size_bytes INTEGER, error_message TEXT, started_at TEXT NOT NULL, pve_completed_at TEXT, upload_started_at TEXT, completed_at TEXT, deleted_at TEXT, updated_at TEXT NOT NULL, FOREIGN KEY (job_id) REFERENCES backup_jobs(id) ON DELETE SET NULL ); CREATE TABLE notification_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, backup_id TEXT, payload TEXT NOT NULL, success INTEGER NOT NULL, error_message TEXT, created_at TEXT NOT NULL, FOREIGN KEY (backup_id) REFERENCES backups(id) ON DELETE SET NULL ); CREATE INDEX idx_backup_jobs_enabled_next_run ON backup_jobs(enabled, next_run_at); CREATE INDEX idx_backups_guest_state ON backups(guest_vmid, state); CREATE INDEX idx_backups_state ON backups(state); """ ) now = utc_now() conn.executemany( "INSERT INTO settings(key, value, updated_at) VALUES (?, ?, ?)", [(key, value, now) for key, value in DEFAULT_SETTINGS.items()], ) conn.execute( "INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)", (1, now), ) if 2 not in applied: now = utc_now() conn.execute( """ INSERT INTO settings(key, value, updated_at) VALUES ('rclone_remote_path', 'pve-cloud-backup', ?) ON CONFLICT(key) DO NOTHING """, (now,), ) conn.execute( "INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)", (2, now), ) if 3 not in applied: now = utc_now() conn.execute( """ INSERT INTO settings(key, value, updated_at) VALUES ('timezone', ?, ?) ON CONFLICT(key) DO NOTHING """, (DEFAULT_SETTINGS["timezone"], now), ) conn.execute( "INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)", (3, now), ) if 4 not in applied: now = utc_now() row = conn.execute("SELECT value FROM settings WHERE key = 'timezone'").fetchone() if row: normalized_timezone = _normalize_timezone(row["value"]) if normalized_timezone != row["value"]: conn.execute( "UPDATE settings SET value = ?, updated_at = ? WHERE key = 'timezone'", (normalized_timezone, now), ) conn.execute( "INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)", (4, now), ) if 5 not in applied: now = utc_now() conn.execute("ALTER TABLE backup_jobs DROP COLUMN proxmox_storage") conn.execute( "INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)", (5, now), )