diff --git a/src/lib/db/__init__.py b/src/lib/db/__init__.py index 1b21091..1d832df 100644 --- a/src/lib/db/__init__.py +++ b/src/lib/db/__init__.py @@ -3,10 +3,13 @@ import csv import json import logging import re +import os +import sys from playhouse.sqlite_ext import JSONField from .models import Archive, Log, Recipes, Session, Users, db, db_archive +from .models.base_model import handle_fatal_sqlite_error # Keep a unified reference for consumers like Crud_DB models_reference = { @@ -16,9 +19,32 @@ models_reference = { "users": Users, } +# Optional test flag to simulate on-disk SQLite corruption (archive DB only) +if "--kill-db" in sys.argv: + try: + _db = db_archive # Only corrupt the archive database + path = getattr(_db, "database", None) + if path: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "wb") as f: + f.write(b"NOT A DATABASE - corrupted for test") + except Exception: + pass + # Connect both databases and create tables in their respective DBs -db.connect() -db_archive.connect() +try: + db.connect() + db_archive.connect() +except Exception as e: + msg = str(e).lower() + if any(s in msg for s in ( + "malformed", + "not a database", + "file is encrypted or is not a database", + "database disk image is malformed", + )): + handle_fatal_sqlite_error(e) + raise # Create tables for the main database (exclude Archive which lives in db_archive) main_models = [Log, Recipes, Users] diff --git a/src/lib/db/models/base_model.py b/src/lib/db/models/base_model.py index 45c9425..58c19a4 100644 --- a/src/lib/db/models/base_model.py +++ b/src/lib/db/models/base_model.py @@ -1,13 +1,203 @@ import os +import sys +import shutil +import traceback from peewee import Model from playhouse.sqlite_ext import SqliteExtDatabase +# GUI popup support (create minimal app if needed) +try: + from PyQt5.QtWidgets import QApplication, QMessageBox +except Exception: + QApplication = None + QMessageBox = None + + db_path = "./data/database" os.makedirs(db_path, exist_ok=True) + +def _delete_archive_and_folder_and_exit(): + # Try to detach any attached archive data source alias 'e' if present + try: + if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False: + try: + globals()['db'].execute_sql('DETACH DATABASE e') + except Exception: + pass + except Exception: + pass + try: + if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False: + try: + globals()['db_archive'].execute_sql('DETACH DATABASE e') + except Exception: + pass + except Exception: + pass + + # Try closing DBs if already created + try: + if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False: + try: + globals()['db'].close() + except Exception: + pass + except Exception: + pass + try: + if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False: + try: + globals()['db_archive'].close() + except Exception: + pass + except Exception: + pass + + # Remove archive DB explicitly (base file and side-car files) + try: + base = os.path.join(db_path, 'sqlite_archive.db') + candidates = [ + base, + os.path.join(db_path, 'sqlite_archive'), + base + '-wal', + base + '-shm', + base + '-journal', + ] + for p in candidates: + try: + os.remove(p) + except Exception: + pass + except Exception: + pass + + # Try to recreate the new archive DB and import seed data + try: + # Reconnect will create the file if missing + if 'db_archive' in globals(): + try: + if getattr(globals()['db_archive'], 'is_closed', lambda: True)(): + globals()['db_archive'].connect() + except Exception: + # If connect fails, try reopen after ensuring close + try: + globals()['db_archive'].close() + except Exception: + pass + try: + globals()['db_archive'].connect() + except Exception: + pass + + # Lazy import to avoid circular + try: + from .archive import Archive # type: ignore + # Create table + try: + globals()['db_archive'].create_tables([Archive]) + except Exception: + pass + + # Import from CSV if present + try: + import csv, json, ast + from playhouse.sqlite_ext import JSONField as _JSONField + path = "src/lib/db/imports/archive.csv" + if os.path.exists(path): + with open(path, "r") as f: + reader = csv.DictReader(f) + fields = list(Archive._meta.fields) + count = 0 + with globals()['db_archive'].atomic(): + for row in reader: + obj = {} + for field in fields: + if type(Archive._meta.fields[field]) is _JSONField: + obj[field] = json.loads(row.get(field, "null")) + else: + try: + obj[field] = ast.literal_eval(row.get(field, "")) + except (SyntaxError, ValueError): + obj[field] = row.get(field) + try: + Archive.insert(**obj).on_conflict_replace().execute() + count += 1 + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # Hard exit to ensure restart + try: + os._exit(1) + except Exception: + sys.exit(1) + + +def handle_fatal_sqlite_error(exc=None): + """Show popup, print error to terminal, delete only the archive DB, try to re-import, then exit.""" + msg_text = "ATTENZIONE ERRORE FATALE DATABASE DI TEST PEFOVARORE PREMERE OK E RIAVVIAREIL PROGRAMMA" + + # Always print error details to the execution terminal + try: + sys.stderr.write("FATAL SQLITE ERROR DETECTED\n") + if exc is not None: + sys.stderr.write(f"Exception: {exc!r}\n") + try: + traceback.print_exc() + except Exception: + pass + sys.stderr.flush() + except Exception: + pass + + # If GUI available, show a critical popup. Create a temp app if needed. + app_created = False + app = None + try: + if QApplication is not None: + app = QApplication.instance() + if app is None: + app = QApplication([]) + app_created = True + if QMessageBox is not None: + QMessageBox.critical(None, "Errore Database", msg_text) + except Exception: + # Also echo the user-facing message to stderr in case GUI cannot be shown + try: + sys.stderr.write(msg_text + "\n") + sys.stderr.flush() + except Exception: + pass + finally: + # Ensure resources are cleaned up and exit (with attempted rebuild) + _delete_archive_and_folder_and_exit() + + +class SafeSqliteExtDatabase(SqliteExtDatabase): + def execute_sql(self, sql, params=None, commit=True): + try: + return super().execute_sql(sql, params=params, commit=commit) + except Exception as e: + msg = str(e).lower() + if any(s in msg for s in ( + "malformed", + "not a database", + "file is encrypted or is not a database", + "database disk image is malformed", + )): + handle_fatal_sqlite_error(e) + raise + + # Main application database -db = SqliteExtDatabase( +db = SafeSqliteExtDatabase( db_path + "/sqlite.db", pragmas={ # see https://www.sqlite.org/pragma.html "auto_vacuum": 1, @@ -22,7 +212,7 @@ db = SqliteExtDatabase( ) # Separate database dedicated to archive data -db_archive = SqliteExtDatabase( +db_archive = SafeSqliteExtDatabase( db_path + "/sqlite_archive.db", pragmas={ "auto_vacuum": 1,