611 lines
23 KiB
Python
611 lines
23 KiB
Python
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from app.config_model import AppConfig, BackupExecutionJob, BackupJob, BackupSummary, UserAccount
|
|
from app.crypto import CryptoManager
|
|
|
|
|
|
class ConfigStore:
|
|
def __init__(self, db_path: Path) -> None:
|
|
self.db_path = db_path
|
|
self.crypto = CryptoManager(db_path.parent / "secret.key")
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def ensure_schema(self) -> None:
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS app_config (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
ip TEXT,
|
|
port INTEGER,
|
|
debug INTEGER,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS backups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
source_protocol TEXT NOT NULL,
|
|
source_host TEXT NOT NULL,
|
|
source_port INTEGER,
|
|
source_share TEXT NOT NULL,
|
|
source_subpath TEXT NOT NULL,
|
|
source_username TEXT NOT NULL,
|
|
source_password_enc TEXT NOT NULL,
|
|
destination_protocol TEXT NOT NULL,
|
|
destination_host TEXT NOT NULL,
|
|
destination_port INTEGER,
|
|
destination_share TEXT NOT NULL,
|
|
destination_subpath TEXT NOT NULL,
|
|
destination_username TEXT NOT NULL,
|
|
destination_password_enc TEXT NOT NULL,
|
|
source_entry_type TEXT NOT NULL DEFAULT 'directory',
|
|
target_kind TEXT NOT NULL DEFAULT 'folder',
|
|
target_pattern TEXT NOT NULL,
|
|
compression_method TEXT NOT NULL,
|
|
encryption_mode TEXT NOT NULL DEFAULT 'none',
|
|
archive_password_enc TEXT NOT NULL,
|
|
schedule_mode TEXT NOT NULL DEFAULT 'daily',
|
|
schedule_time TEXT NOT NULL DEFAULT '02:00',
|
|
schedule_weekday TEXT NOT NULL DEFAULT '1',
|
|
schedule_day_of_month TEXT NOT NULL DEFAULT '1',
|
|
schedule_interval INTEGER,
|
|
schedule_cron TEXT NOT NULL DEFAULT '',
|
|
last_run_at TEXT,
|
|
last_size_bytes INTEGER,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
|
|
backup_columns = {
|
|
row["name"].lower()
|
|
for row in conn.execute("PRAGMA table_info(backups)").fetchall()
|
|
}
|
|
if "source_port" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN source_port INTEGER")
|
|
if "destination_port" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN destination_port INTEGER")
|
|
if "source_entry_type" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN source_entry_type TEXT NOT NULL DEFAULT 'directory'")
|
|
if "target_kind" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN target_kind TEXT NOT NULL DEFAULT 'folder'")
|
|
if "encryption_mode" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN encryption_mode TEXT NOT NULL DEFAULT 'none'")
|
|
if "schedule_mode" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_mode TEXT NOT NULL DEFAULT 'daily'")
|
|
if "schedule_time" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_time TEXT NOT NULL DEFAULT '02:00'")
|
|
if "schedule_weekday" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_weekday TEXT NOT NULL DEFAULT '1'")
|
|
if "schedule_day_of_month" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_day_of_month TEXT NOT NULL DEFAULT '1'")
|
|
if "schedule_interval" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_interval INTEGER")
|
|
if "schedule_cron" not in backup_columns:
|
|
conn.execute("ALTER TABLE backups ADD COLUMN schedule_cron TEXT NOT NULL DEFAULT ''")
|
|
|
|
columns = {
|
|
row["name"].lower()
|
|
for row in conn.execute("PRAGMA table_info(app_config)").fetchall()
|
|
}
|
|
|
|
# Legacy-Felder koennen in bestehenden Datenbanken fehlen/enthalten.
|
|
# Falls vorhanden, werden sie fuer Migration gelesen, danach nicht mehr genutzt.
|
|
if "adminuser" not in columns:
|
|
conn.execute("ALTER TABLE app_config ADD COLUMN adminuser TEXT")
|
|
if "adminpassword" not in columns:
|
|
conn.execute("ALTER TABLE app_config ADD COLUMN adminpassword TEXT")
|
|
|
|
conn.commit()
|
|
|
|
self._migrate_legacy_admin_if_needed()
|
|
|
|
def _migrate_legacy_admin_if_needed(self) -> None:
|
|
with self._connect() as conn:
|
|
user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
|
if user_count > 0:
|
|
return
|
|
|
|
row = conn.execute(
|
|
"SELECT adminuser, adminpassword FROM app_config WHERE id = 1"
|
|
).fetchone()
|
|
if row is None:
|
|
return
|
|
|
|
legacy_user = row["adminuser"]
|
|
legacy_hash = row["adminpassword"]
|
|
if not isinstance(legacy_user, str) or not legacy_user.strip():
|
|
return
|
|
if not isinstance(legacy_hash, str) or not legacy_hash.strip():
|
|
return
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO users (username, password_hash, is_admin, updated_at)
|
|
VALUES (?, ?, 1, CURRENT_TIMESTAMP)
|
|
""",
|
|
(legacy_user.strip(), legacy_hash.strip()),
|
|
)
|
|
conn.commit()
|
|
|
|
def load_config(self) -> AppConfig | None:
|
|
with self._connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT ip, port, debug "
|
|
"FROM app_config WHERE id = 1"
|
|
).fetchone()
|
|
|
|
if row is None:
|
|
return None
|
|
|
|
ip = row["ip"]
|
|
port = row["port"]
|
|
debug = row["debug"]
|
|
|
|
if not isinstance(ip, str) or not ip.strip():
|
|
return None
|
|
if not isinstance(port, int):
|
|
return None
|
|
if debug not in (0, 1):
|
|
return None
|
|
|
|
return AppConfig(
|
|
ip=ip.strip(),
|
|
port=port,
|
|
debug=bool(debug),
|
|
)
|
|
|
|
def save_config(self, config: AppConfig) -> None:
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO app_config (id, ip, port, debug, updated_at)
|
|
VALUES (1, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
ip = excluded.ip,
|
|
port = excluded.port,
|
|
debug = excluded.debug,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
""",
|
|
config.as_db_tuple(),
|
|
)
|
|
conn.commit()
|
|
|
|
def has_any_user(self) -> bool:
|
|
with self._connect() as conn:
|
|
count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
|
return count > 0
|
|
|
|
def create_user(self, username: str, password_hash: str, is_admin: bool = False) -> bool:
|
|
name = username.strip()
|
|
if not name:
|
|
return False
|
|
|
|
with self._connect() as conn:
|
|
try:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO users (username, password_hash, is_admin, updated_at)
|
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
""",
|
|
(name, password_hash, 1 if is_admin else 0),
|
|
)
|
|
conn.commit()
|
|
return True
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
|
|
def list_users(self) -> list[UserAccount]:
|
|
with self._connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT username, password_hash, is_admin FROM users ORDER BY username ASC"
|
|
).fetchall()
|
|
|
|
return [
|
|
UserAccount(
|
|
username=row["username"],
|
|
password_hash=row["password_hash"],
|
|
is_admin=bool(row["is_admin"]),
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
def get_user(self, username: str) -> UserAccount | None:
|
|
with self._connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT username, password_hash, is_admin FROM users WHERE username = ?",
|
|
(username.strip(),),
|
|
).fetchone()
|
|
|
|
if row is None:
|
|
return None
|
|
|
|
return UserAccount(
|
|
username=row["username"],
|
|
password_hash=row["password_hash"],
|
|
is_admin=bool(row["is_admin"]),
|
|
)
|
|
|
|
def update_user_credentials(
|
|
self,
|
|
current_username: str,
|
|
new_username: str,
|
|
new_password_hash: str,
|
|
) -> bool:
|
|
with self._connect() as conn:
|
|
try:
|
|
result = conn.execute(
|
|
"""
|
|
UPDATE users
|
|
SET username = ?, password_hash = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE username = ?
|
|
""",
|
|
(new_username.strip(), new_password_hash, current_username.strip()),
|
|
)
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
|
|
return result.rowcount == 1
|
|
|
|
def create_backup(self, backup: BackupJob) -> bool:
|
|
with self._connect() as conn:
|
|
try:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO backups (
|
|
name,
|
|
source_protocol,
|
|
source_host,
|
|
source_port,
|
|
source_share,
|
|
source_subpath,
|
|
source_username,
|
|
source_password_enc,
|
|
destination_protocol,
|
|
destination_host,
|
|
destination_port,
|
|
destination_share,
|
|
destination_subpath,
|
|
destination_username,
|
|
destination_password_enc,
|
|
source_entry_type,
|
|
target_kind,
|
|
target_pattern,
|
|
compression_method,
|
|
encryption_mode,
|
|
archive_password_enc,
|
|
schedule_mode,
|
|
schedule_time,
|
|
schedule_weekday,
|
|
schedule_day_of_month,
|
|
schedule_interval,
|
|
schedule_cron,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
""",
|
|
(
|
|
backup.name,
|
|
backup.source_protocol,
|
|
backup.source_host,
|
|
backup.source_port,
|
|
backup.source_share,
|
|
backup.source_subpath,
|
|
backup.source_username,
|
|
self.crypto.encrypt_text(backup.source_password),
|
|
backup.destination_protocol,
|
|
backup.destination_host,
|
|
backup.destination_port,
|
|
backup.destination_share,
|
|
backup.destination_subpath,
|
|
backup.destination_username,
|
|
self.crypto.encrypt_text(backup.destination_password),
|
|
backup.source_entry_type,
|
|
backup.target_kind,
|
|
backup.target_pattern,
|
|
backup.compression_method,
|
|
backup.encryption_mode,
|
|
self.crypto.encrypt_text(backup.archive_password),
|
|
backup.schedule_mode,
|
|
backup.schedule_time,
|
|
backup.schedule_weekday,
|
|
backup.schedule_day_of_month,
|
|
backup.schedule_interval,
|
|
backup.schedule_cron,
|
|
),
|
|
)
|
|
conn.commit()
|
|
return True
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
|
|
def list_backups(self) -> list[BackupSummary]:
|
|
with self._connect() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
name,
|
|
source_protocol,
|
|
source_host,
|
|
source_port,
|
|
source_share,
|
|
source_subpath,
|
|
destination_protocol,
|
|
destination_host,
|
|
destination_port,
|
|
destination_share,
|
|
destination_subpath,
|
|
target_pattern,
|
|
compression_method,
|
|
schedule_mode,
|
|
last_run_at,
|
|
last_size_bytes
|
|
FROM backups
|
|
ORDER BY name ASC
|
|
"""
|
|
).fetchall()
|
|
|
|
items: list[BackupSummary] = []
|
|
for row in rows:
|
|
source_host = row["source_host"] or "-"
|
|
source_share = row["source_share"] or ""
|
|
source_subpath = row["source_subpath"] or "/"
|
|
destination_host = row["destination_host"] or "-"
|
|
destination_share = row["destination_share"] or ""
|
|
destination_subpath = row["destination_subpath"] or "/"
|
|
|
|
source_port = row["source_port"]
|
|
destination_port = row["destination_port"]
|
|
source_port_suffix = f":{source_port}" if source_port else ""
|
|
destination_port_suffix = f":{destination_port}" if destination_port else ""
|
|
|
|
source_label = (
|
|
f"{row['source_protocol']}://{source_host}{source_port_suffix}/"
|
|
f"{source_share}{source_subpath}"
|
|
)
|
|
destination_label = (
|
|
f"{row['destination_protocol']}://{destination_host}{destination_port_suffix}/"
|
|
f"{destination_share}{destination_subpath}"
|
|
)
|
|
items.append(
|
|
BackupSummary(
|
|
backup_id=row["id"],
|
|
name=row["name"],
|
|
source_label=source_label,
|
|
destination_label=destination_label,
|
|
target_pattern=row["target_pattern"],
|
|
compression_method=row["compression_method"],
|
|
schedule_mode=row["schedule_mode"] if row["schedule_mode"] else "daily",
|
|
last_run_at=row["last_run_at"],
|
|
last_size_bytes=row["last_size_bytes"],
|
|
)
|
|
)
|
|
|
|
return items
|
|
|
|
def get_backup_edit_data(self, backup_id: int) -> dict | None:
|
|
with self._connect() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
name,
|
|
target_pattern,
|
|
compression_method,
|
|
encryption_mode,
|
|
schedule_mode,
|
|
schedule_time,
|
|
schedule_weekday,
|
|
schedule_day_of_month,
|
|
schedule_interval,
|
|
schedule_cron
|
|
FROM backups
|
|
WHERE id = ?
|
|
""",
|
|
(backup_id,),
|
|
).fetchone()
|
|
|
|
if row is None:
|
|
return None
|
|
|
|
return {
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"target_pattern": row["target_pattern"],
|
|
"compression_method": row["compression_method"],
|
|
"encryption_mode": row["encryption_mode"],
|
|
"schedule_mode": row["schedule_mode"],
|
|
"schedule_time": row["schedule_time"],
|
|
"schedule_weekday": row["schedule_weekday"],
|
|
"schedule_day_of_month": row["schedule_day_of_month"],
|
|
"schedule_interval": row["schedule_interval"],
|
|
"schedule_cron": row["schedule_cron"],
|
|
}
|
|
|
|
def get_backup_execution_job(self, backup_id: int) -> BackupExecutionJob | None:
|
|
with self._connect() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT
|
|
id,
|
|
name,
|
|
source_protocol,
|
|
source_host,
|
|
source_port,
|
|
source_share,
|
|
source_subpath,
|
|
source_username,
|
|
source_password_enc,
|
|
destination_protocol,
|
|
destination_host,
|
|
destination_port,
|
|
destination_share,
|
|
destination_subpath,
|
|
destination_username,
|
|
destination_password_enc
|
|
FROM backups
|
|
WHERE id = ?
|
|
""",
|
|
(backup_id,),
|
|
).fetchone()
|
|
|
|
if row is None:
|
|
return None
|
|
|
|
return BackupExecutionJob(
|
|
backup_id=row["id"],
|
|
name=row["name"],
|
|
source_protocol=row["source_protocol"],
|
|
source_host=row["source_host"],
|
|
source_port=row["source_port"],
|
|
source_share=row["source_share"],
|
|
source_subpath=row["source_subpath"],
|
|
source_username=row["source_username"],
|
|
source_password=self.crypto.decrypt_text(row["source_password_enc"]),
|
|
destination_protocol=row["destination_protocol"],
|
|
destination_host=row["destination_host"],
|
|
destination_port=row["destination_port"],
|
|
destination_share=row["destination_share"],
|
|
destination_subpath=row["destination_subpath"],
|
|
destination_username=row["destination_username"],
|
|
destination_password=self.crypto.decrypt_text(row["destination_password_enc"]),
|
|
)
|
|
|
|
def update_backup_edit_data(
|
|
self,
|
|
backup_id: int,
|
|
name: str,
|
|
target_pattern: str,
|
|
compression_method: str,
|
|
encryption_mode: str,
|
|
archive_password: str,
|
|
schedule_mode: str,
|
|
schedule_time: str,
|
|
schedule_weekday: str,
|
|
schedule_day_of_month: str,
|
|
schedule_interval: int | None,
|
|
schedule_cron: str,
|
|
) -> bool:
|
|
with self._connect() as conn:
|
|
try:
|
|
if archive_password:
|
|
result = conn.execute(
|
|
"""
|
|
UPDATE backups
|
|
SET
|
|
name = ?,
|
|
target_pattern = ?,
|
|
compression_method = ?,
|
|
encryption_mode = ?,
|
|
archive_password_enc = ?,
|
|
schedule_mode = ?,
|
|
schedule_time = ?,
|
|
schedule_weekday = ?,
|
|
schedule_day_of_month = ?,
|
|
schedule_interval = ?,
|
|
schedule_cron = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(
|
|
name,
|
|
target_pattern,
|
|
compression_method,
|
|
encryption_mode,
|
|
self.crypto.encrypt_text(archive_password),
|
|
schedule_mode,
|
|
schedule_time,
|
|
schedule_weekday,
|
|
schedule_day_of_month,
|
|
schedule_interval,
|
|
schedule_cron,
|
|
backup_id,
|
|
),
|
|
)
|
|
else:
|
|
result = conn.execute(
|
|
"""
|
|
UPDATE backups
|
|
SET
|
|
name = ?,
|
|
target_pattern = ?,
|
|
compression_method = ?,
|
|
encryption_mode = ?,
|
|
schedule_mode = ?,
|
|
schedule_time = ?,
|
|
schedule_weekday = ?,
|
|
schedule_day_of_month = ?,
|
|
schedule_interval = ?,
|
|
schedule_cron = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(
|
|
name,
|
|
target_pattern,
|
|
compression_method,
|
|
encryption_mode,
|
|
schedule_mode,
|
|
schedule_time,
|
|
schedule_weekday,
|
|
schedule_day_of_month,
|
|
schedule_interval,
|
|
schedule_cron,
|
|
backup_id,
|
|
),
|
|
)
|
|
conn.commit()
|
|
return result.rowcount == 1
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
|
|
def mark_backup_run(self, backup_id: int) -> bool:
|
|
with self._connect() as conn:
|
|
result = conn.execute(
|
|
"""
|
|
UPDATE backups
|
|
SET
|
|
last_run_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'now', 'localtime'),
|
|
last_size_bytes = COALESCE(last_size_bytes, 0),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(backup_id,),
|
|
)
|
|
conn.commit()
|
|
return result.rowcount == 1
|
|
|
|
def delete_backup(self, backup_id: int) -> bool:
|
|
with self._connect() as conn:
|
|
result = conn.execute("DELETE FROM backups WHERE id = ?", (backup_id,))
|
|
conn.commit()
|
|
return result.rowcount == 1
|