Add database corruption handling and integrity checks in HuntarrDatabase

- Implemented error handling for database connection to detect and manage corruption.
- Added a method to create backups of corrupted databases and start fresh.
- Introduced a database integrity check to ensure the database is intact before operations.
- Enhanced table creation process to handle potential corruption during setup, ensuring robustness in database management.
This commit is contained in:
Admin9705
2025-06-19 10:37:24 -04:00
parent c33f7b74a0
commit 2abe227150
2 changed files with 334 additions and 3 deletions

262
scripts/recover_database.py Normal file
View File

@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""
Huntarr Database Recovery Utility
This script helps recover from database corruption issues by:
1. Backing up the corrupted database
2. Creating a fresh database
3. Attempting to recover any salvageable data
Usage:
- For Docker: docker exec huntarr python /app/scripts/recover_database.py
- For local: python scripts/recover_database.py
"""
import sys
import os
import sqlite3
import json
import time
from pathlib import Path
# Add src to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
def get_database_path():
"""Get the database path based on environment"""
# Check if running in Docker
config_dir = Path("/config")
if config_dir.exists() and config_dir.is_dir():
return config_dir / "huntarr.db"
# Check for Windows config directory
windows_config = os.environ.get("HUNTARR_CONFIG_DIR")
if windows_config:
return Path(windows_config) / "huntarr.db"
# Check for Windows AppData
import platform
if platform.system() == "Windows":
appdata = os.environ.get("APPDATA", os.path.expanduser("~"))
return Path(appdata) / "Huntarr" / "huntarr.db"
# Local development
project_root = Path(__file__).parent.parent
return project_root / "data" / "huntarr.db"
def backup_corrupted_database(db_path):
"""Create backup of corrupted database"""
if not db_path.exists():
print(f"Database file does not exist: {db_path}")
return None
timestamp = int(time.time())
backup_path = db_path.parent / f"huntarr_corrupted_backup_{timestamp}.db"
try:
# Copy the corrupted file
import shutil
shutil.copy2(db_path, backup_path)
print(f"✓ Corrupted database backed up to: {backup_path}")
return backup_path
except Exception as e:
print(f"✗ Failed to backup corrupted database: {e}")
return None
def attempt_data_recovery(backup_path):
"""Attempt to recover data from corrupted database"""
if not backup_path or not backup_path.exists():
return {}
print("⚡ Attempting to recover data from corrupted database...")
recovered_data = {}
try:
# Try to connect and recover whatever we can
conn = sqlite3.connect(backup_path)
conn.row_factory = sqlite3.Row
# Try to recover users table
try:
cursor = conn.execute("SELECT * FROM users")
users = [dict(row) for row in cursor.fetchall()]
if users:
recovered_data['users'] = users
print(f"✓ Recovered {len(users)} user(s)")
except Exception as e:
print(f" Could not recover users: {e}")
# Try to recover general settings
try:
cursor = conn.execute("SELECT * FROM general_settings")
settings = [dict(row) for row in cursor.fetchall()]
if settings:
recovered_data['general_settings'] = settings
print(f"✓ Recovered {len(settings)} general setting(s)")
except Exception as e:
print(f" Could not recover general settings: {e}")
# Try to recover app configs
try:
cursor = conn.execute("SELECT * FROM app_configs")
configs = [dict(row) for row in cursor.fetchall()]
if configs:
recovered_data['app_configs'] = configs
print(f"✓ Recovered {len(configs)} app configuration(s)")
except Exception as e:
print(f" Could not recover app configs: {e}")
conn.close()
except Exception as e:
print(f"✗ Could not open corrupted database for recovery: {e}")
return recovered_data
def create_fresh_database(db_path):
"""Create a fresh database with proper structure"""
print("🔧 Creating fresh database...")
try:
# Remove corrupted file
if db_path.exists():
db_path.unlink()
# Import and create fresh database
from primary.utils.database import HuntarrDatabase
# This will create a fresh database with all tables
db = HuntarrDatabase()
print(f"✓ Fresh database created at: {db_path}")
return db
except Exception as e:
print(f"✗ Failed to create fresh database: {e}")
return None
def restore_recovered_data(db, recovered_data):
"""Restore recovered data to fresh database"""
if not recovered_data:
print("⚠ No data to restore")
return
print("📋 Restoring recovered data...")
# Restore users
if 'users' in recovered_data:
try:
for user in recovered_data['users']:
# Remove auto-generated fields
user_data = {k: v for k, v in user.items()
if k not in ['id', 'created_at', 'updated_at']}
db.create_user(
username=user_data.get('username', ''),
password=user_data.get('password', ''),
two_fa_enabled=user_data.get('two_fa_enabled', False),
two_fa_secret=user_data.get('two_fa_secret'),
plex_token=user_data.get('plex_token'),
plex_user_data=json.loads(user_data.get('plex_user_data', '{}')) if user_data.get('plex_user_data') else None
)
print(f"✓ Restored {len(recovered_data['users'])} user(s)")
except Exception as e:
print(f"✗ Failed to restore users: {e}")
# Restore general settings
if 'general_settings' in recovered_data:
try:
settings_dict = {}
for setting in recovered_data['general_settings']:
key = setting['setting_key']
value = setting['setting_value']
setting_type = setting.get('setting_type', 'string')
# Convert value based on type
if setting_type == 'boolean':
settings_dict[key] = value.lower() == 'true'
elif setting_type == 'integer':
settings_dict[key] = int(value)
elif setting_type == 'float':
settings_dict[key] = float(value)
elif setting_type == 'json':
try:
settings_dict[key] = json.loads(value)
except:
settings_dict[key] = value
else:
settings_dict[key] = value
db.save_general_settings(settings_dict)
print(f"✓ Restored {len(settings_dict)} general setting(s)")
except Exception as e:
print(f"✗ Failed to restore general settings: {e}")
# Restore app configs
if 'app_configs' in recovered_data:
try:
for config in recovered_data['app_configs']:
app_type = config['app_type']
config_data = json.loads(config['config_data'])
db.save_app_config(app_type, config_data)
print(f"✓ Restored {len(recovered_data['app_configs'])} app configuration(s)")
except Exception as e:
print(f"✗ Failed to restore app configs: {e}")
def main():
print("🔥 Huntarr Database Recovery Utility")
print("=" * 50)
# Get database path
db_path = get_database_path()
print(f"Database location: {db_path}")
if not db_path.exists():
print("✗ Database file does not exist. Nothing to recover.")
return
# Check if database is actually corrupted
try:
conn = sqlite3.connect(db_path)
conn.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1").fetchone()
conn.close()
print("✓ Database appears to be intact. No recovery needed.")
return
except sqlite3.DatabaseError as e:
if "file is not a database" in str(e) or "database disk image is malformed" in str(e):
print(f"⚠ Database corruption detected: {e}")
else:
print(f"✗ Database error: {e}")
return
# Backup corrupted database
backup_path = backup_corrupted_database(db_path)
# Attempt data recovery
recovered_data = attempt_data_recovery(backup_path)
# Create fresh database
db = create_fresh_database(db_path)
if not db:
print("✗ Failed to create fresh database. Recovery aborted.")
return
# Restore recovered data
restore_recovered_data(db, recovered_data)
print("\n🎉 Database recovery completed!")
print("=" * 50)
print("Summary:")
print(f"- Corrupted database backed up to: {backup_path}")
print(f"- Fresh database created at: {db_path}")
if recovered_data:
print("- Data recovered:")
for table, data in recovered_data.items():
print(f"{table}: {len(data)} record(s)")
else:
print("- No data could be recovered from corrupted database")
print("\nYou can now restart Huntarr.")
if __name__ == "__main__":
main()

View File

@@ -66,9 +66,22 @@ class HuntarrDatabase:
def get_connection(self):
"""Get a configured SQLite connection with Synology NAS compatibility"""
conn = sqlite3.connect(self.db_path)
self._configure_connection(conn)
return conn
try:
conn = sqlite3.connect(self.db_path)
self._configure_connection(conn)
# Test the connection by running a simple query
conn.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1").fetchone()
return conn
except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
if "file is not a database" in str(e) or "database disk image is malformed" in str(e):
logger.error(f"Database corruption detected: {e}")
self._handle_database_corruption()
# Try connecting again after recovery
conn = sqlite3.connect(self.db_path)
self._configure_connection(conn)
return conn
else:
raise
def _get_database_path(self) -> Path:
"""Get database path - use /config for Docker, Windows AppData, or local data directory"""
@@ -100,6 +113,48 @@ class HuntarrDatabase:
# Ensure directory exists
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir / "huntarr.db"
def _handle_database_corruption(self):
"""Handle database corruption by creating backup and starting fresh"""
import time
logger.error(f"Handling database corruption for: {self.db_path}")
try:
# Create backup of corrupted database if it exists
if self.db_path.exists():
backup_path = self.db_path.parent / f"huntarr_corrupted_backup_{int(time.time())}.db"
self.db_path.rename(backup_path)
logger.warning(f"Corrupted database backed up to: {backup_path}")
logger.warning("Starting with fresh database - all previous data has been backed up but will be lost")
# Ensure the corrupted file is completely removed
if self.db_path.exists():
self.db_path.unlink()
except Exception as backup_error:
logger.error(f"Error during database corruption recovery: {backup_error}")
# Force remove the corrupted file
try:
if self.db_path.exists():
self.db_path.unlink()
except:
pass
def _check_database_integrity(self) -> bool:
"""Check if database integrity is intact"""
try:
with self.get_connection() as conn:
# Run SQLite integrity check
result = conn.execute("PRAGMA integrity_check").fetchone()
if result and result[0] == "ok":
return True
else:
logger.error(f"Database integrity check failed: {result}")
return False
except Exception as e:
logger.error(f"Database integrity check failed with error: {e}")
return False
def ensure_database_exists(self):
"""Create database and all tables if they don't exist"""
@@ -129,6 +184,20 @@ class HuntarrDatabase:
logger.error(f"Error setting up database directory: {e}")
raise
# Create all tables with corruption recovery
try:
self._create_all_tables()
except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
if "file is not a database" in str(e) or "database disk image is malformed" in str(e):
logger.error(f"Database corruption detected during table creation: {e}")
self._handle_database_corruption()
# Try creating tables again after recovery
self._create_all_tables()
else:
raise
def _create_all_tables(self):
"""Create all database tables"""
with self.get_connection() as conn:
# Create app_configs table for all app settings