import os import sqlite3 import smbclient import tempfile import json import threading from unittest.mock import patch, MagicMock # --- App Initialization & Path Configuration --- APP_ROOT = os.path.dirname(os.path.abspath(__file__)) INSTANCE_PATH = os.path.join(os.path.dirname(APP_ROOT), 'instance') LOCAL_DB_PATH = "local_cache.db" CONFIG_FILE = "config.json" REMOTE_DB_LOCK = threading.Lock() DB_PATH = "//servervm.local/main/appdata/stashapp/config/stash-go.sqlite" credentials = {} def get_local_db(): db_path = os.path.join(INSTANCE_PATH, LOCAL_DB_PATH) conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row return conn def init_local_db(): """Initializes and resets cache tables to ensure schema is correct.""" try: os.makedirs(INSTANCE_PATH) except OSError: pass # Already exists with get_local_db() as conn: cursor = conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)") cursor.execute("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value TEXT)") # cursor.execute("CREATE TABLE IF NOT EXISTS scan_history (id INTEGER PRIMARY KEY AUTOINCREMENT, scan_type TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT NOT NULL, message TEXT, log TEXT)") # cursor.execute("CREATE TABLE IF NOT EXISTS duplicate_results (id INTEGER PRIMARY KEY AUTOINCREMENT, scan_id INTEGER NOT NULL, file_path TEXT NOT NULL, file_size INTEGER, file_basename TEXT, set_id INTEGER NOT NULL, FOREIGN KEY (scan_id) REFERENCES scan_history (id) ON DELETE CASCADE)") # cursor.execute("CREATE TABLE IF NOT EXISTS transcode_plan_results (id INTEGER PRIMARY KEY AUTOINCREMENT, scan_id INTEGER NOT NULL, original_path TEXT NOT NULL, transcoded_path TEXT NOT NULL, FOREIGN KEY (scan_id) REFERENCES scan_history (id) ON DELETE CASCADE)") print("Rebuilding local cache tables to ensure schema is up-to-date.") cursor.execute("DROP TABLE IF EXISTS scenes") cursor.execute("DROP TABLE IF EXISTS files") cursor.execute("DROP TABLE IF EXISTS paths") cursor.execute("CREATE TABLE scenes (id INTEGER PRIMARY KEY, oshash TEXT)") cursor.execute("CREATE TABLE files (scene_id INTEGER, path TEXT, basename TEXT, path_id INTEGER)") cursor.execute("CREATE TABLE paths (id INTEGER PRIMARY KEY, path TEXT)") conn.commit() def load_credentials(): global credentials # In a test script, we can simplify this to just load from the migrated file # if it exists, for simplicity. old_config_path = os.path.join(os.path.dirname(APP_ROOT), CONFIG_FILE + ".migrated") if os.path.exists(old_config_path): print("Migrating credentials from config.json.migrated...") try: with open(old_config_path, "r") as f: credentials = json.load(f) print("Successfully loaded credentials from migrated config.") except (json.JSONDecodeError, OSError) as e: print(f"Error loading from config.json.migrated: {e}") credentials = {} else: print("config.json.migrated not found. No credentials loaded.") def get_smb_credentials(path): server = path.split("/")[2].split('@')[-1] return credentials.get(server, {}) def sync_remote_db(): print("Background sync thread started.") print("Sync thread trying to acquire DB lock...") with REMOTE_DB_LOCK: print("Sync thread acquired DB lock.") try: creds = get_smb_credentials(DB_PATH) remote_stat = smbclient.stat(DB_PATH, **creds) remote_mtime = remote_stat.st_mtime except Exception as e: print(f"Sync thread failed to check remote DB status: {e}") return with get_local_db() as conn_local: row = conn_local.execute("SELECT value FROM sync_metadata WHERE key = 'last_sync_mtime'").fetchone() last_sync_mtime = float(row['value']) if (row and row['value']) else 0 # For testing, let's always sync # if remote_mtime <= last_sync_mtime: # print("Local database is already up to date.") # return print(f"Remote DB is newer. Syncing from {remote_mtime} > {last_sync_mtime}.") temp_db_path = None try: with smbclient.open_file(DB_PATH, mode='rb', **creds) as smb_file: with tempfile.NamedTemporaryFile(delete=False, suffix=".sqlite") as temp_db: temp_db.write(smb_file.read()) temp_db_path = temp_db.name with sqlite3.connect(temp_db_path) as conn_remote: conn_local.execute("DELETE FROM scenes"); conn_local.execute("DELETE FROM files"); conn_local.execute("DELETE FROM paths") cursor_remote_scenes = conn_remote.execute("SELECT id, oshash FROM scenes") conn_local.executemany("INSERT INTO scenes (id, oshash) VALUES (?, ?)", cursor_remote_scenes) cursor_remote_files = conn_remote.execute("SELECT scene_id, path, basename, path_id FROM files") conn_local.executemany("INSERT INTO files (scene_id, path, basename, path_id) VALUES (?, ?, ?, ?)", cursor_remote_files) cursor_remote_paths = conn_remote.execute("SELECT id, path FROM paths") conn_local.executemany("INSERT INTO paths (id, path) VALUES (?, ?)", cursor_remote_paths) conn_local.execute("REPLACE INTO sync_metadata (key, value) VALUES (?, ?)", ('last_sync_mtime', remote_mtime)) conn_local.commit() print("Database sync completed successfully.") except sqlite3.Error as e: print(f"Error during database sync: {e}") conn_local.rollback() finally: if temp_db_path and os.path.exists(temp_db_path): os.remove(temp_db_path) print("Sync thread released DB lock.") if __name__ == '__main__': print("Setting up test environment...") init_local_db() load_credentials() print("\nStarting DB sync test...") sync_remote_db() print("\nDB sync test finished.") print("\nVerifying local database content...") with get_local_db() as conn: scenes_count = conn.execute("SELECT COUNT(*) FROM scenes").fetchone()[0] files_count = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0] paths_count = conn.execute("SELECT COUNT(*) FROM paths").fetchone()[0] print(f"Scenes table count: {scenes_count}") print(f"Files table count: {files_count}") print(f"Paths table count: {paths_count}") if scenes_count > 0 and files_count > 0 and paths_count > 0: print("\nVerification successful: Local database appears to be populated.") else: print("\nVerification failed: Local database is empty after sync.")