From 9fdada5dbe0573de75a16ef0dd20218ae459d120 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 6 Mar 2026 08:51:28 +0100 Subject: [PATCH] prevent data in database and added migration from old databasename lexoffice --- app/database.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/app/database.py b/app/database.py index af24978..6960de8 100644 --- a/app/database.py +++ b/app/database.py @@ -1,8 +1,12 @@ +import logging import os import aiosqlite from cryptography.fernet import Fernet DB_PATH = os.environ.get("DB_PATH", "/data/belegimport.db") +SCHEMA_VERSION = 2 + +logger = logging.getLogger(__name__) _fernet = None @@ -78,9 +82,86 @@ def _decrypt(fernet: Fernet, value: str) -> str: return "" +async def _get_schema_version(db: aiosqlite.Connection) -> int: + """Read current schema version from DB. Returns 0 if not set.""" + try: + cursor = await db.execute( + "SELECT value FROM settings WHERE key = 'schema_version'" + ) + row = await cursor.fetchone() + return int(row[0]) if row else 0 + except Exception: + return 0 + + +async def _set_schema_version(db: aiosqlite.Connection, version: int): + await db.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', ?)", + (str(version),), + ) + + +async def _run_migrations(db: aiosqlite.Connection, current_version: int): + """Run all pending migrations sequentially.""" + + if current_version < 1: + logger.info("Migration v1: Initiale Tabellenstruktur") + # v1: Base tables (idempotent via IF NOT EXISTS) + await db.execute(""" + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS processing_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), + email_subject TEXT, + email_from TEXT, + attachments_count INTEGER DEFAULT 0, + status TEXT NOT NULL, + error_message TEXT + ) + """) + await db.commit() + await _set_schema_version(db, 1) + + if current_version < 2: + logger.info("Migration v2: lexoffice_email -> import_email") + # v2: Rename lexoffice_email -> import_email + cursor = await db.execute( + "SELECT value FROM settings WHERE key = 'lexoffice_email'" + ) + row = await cursor.fetchone() + if row and row[0]: + # Copy value to import_email if it's empty + cursor2 = await db.execute( + "SELECT value FROM settings WHERE key = 'import_email'" + ) + row2 = await cursor2.fetchone() + if not row2 or not row2[0]: + await db.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('import_email', ?)", + (row[0],), + ) + logger.info(" lexoffice_email Wert nach import_email übertragen") + await db.execute("DELETE FROM settings WHERE key = 'lexoffice_email'") + await db.commit() + await _set_schema_version(db, 2) + + # --- Future migrations go here --- + # if current_version < 3: + # logger.info("Migration v3: ...") + # await _set_schema_version(db, 3) + + await db.commit() + + async def init_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + os.makedirs(os.path.dirname(DB_PATH) or ".", exist_ok=True) async with aiosqlite.connect(DB_PATH) as db: + # Ensure base tables exist (needed before we can read schema_version) await db.execute(""" CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -100,7 +181,18 @@ async def init_db(): """) await db.commit() - # Insert default settings if not present + # Check version and run migrations + current_version = await _get_schema_version(db) + if current_version < SCHEMA_VERSION: + logger.info( + f"DB-Schema v{current_version} -> v{SCHEMA_VERSION}, starte Migrationen..." + ) + await _run_migrations(db, current_version) + logger.info(f"DB-Schema auf v{SCHEMA_VERSION} aktualisiert") + else: + logger.info(f"DB-Schema v{current_version} ist aktuell") + + # Insert default settings for any new keys (never overwrites existing values) for key, value in DEFAULT_SETTINGS.items(): await db.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", @@ -116,7 +208,7 @@ async def get_settings() -> dict: fernet = await _get_fernet() async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( - "SELECT key, value FROM settings WHERE key != 'encryption_key'" + "SELECT key, value FROM settings WHERE key NOT IN ('encryption_key', 'schema_version')" ) rows = await cursor.fetchall() @@ -133,7 +225,7 @@ async def save_settings(data: dict): fernet = await _get_fernet() async with aiosqlite.connect(DB_PATH) as db: for key, value in data.items(): - if key == "encryption_key": + if key in ("encryption_key", "schema_version"): continue store_value = _encrypt(fernet, value) if key in ENCRYPTED_KEYS else value await db.execute(