Files
meshcore-bot/backup_database.py
agessaman 217d2a4089 Refactor database connection handling across multiple modules
- Replaced direct SQLite connection calls with a context manager in various modules to ensure proper resource management and prevent file descriptor leaks.
- Introduced a new `connection` method in `DBManager` to standardize connection handling.
- Updated all relevant database interactions in modules such as `feed_manager`, `scheduler`, `commands`, and others to utilize the new connection method.
- Improved code readability and maintainability by consolidating connection logic.
2026-03-01 14:12:22 -08:00

219 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Database Backup Script for MeshCore Bot
Creates timestamped backups of all SQLite database files
"""
import sqlite3
import shutil
import os
import sys
from contextlib import closing
from datetime import datetime
from pathlib import Path
def backup_database(db_path, backup_dir="backups"):
"""
Create a timestamped backup of a SQLite database
Args:
db_path: Path to the database file
backup_dir: Directory to store backups (default: backups)
Returns:
Path to the backup file if successful, None otherwise
"""
db_path = Path(db_path)
if not db_path.exists():
print(f"Warning: Database file {db_path} does not exist, skipping...")
return None
# Create backup directory if it doesn't exist
backup_path = Path(backup_dir)
backup_path.mkdir(exist_ok=True)
# Generate timestamp string
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Create backup filename with timestamp
db_name = db_path.stem
backup_filename = f"{db_name}_{timestamp}.db"
backup_file = backup_path / backup_filename
try:
# Use SQLite backup API for proper backup (handles WAL files correctly)
with closing(sqlite3.connect(str(db_path))) as source_conn, closing(sqlite3.connect(str(backup_file))) as backup_conn:
# Perform the backup
source_conn.backup(backup_conn)
# Get file size for reporting
file_size = backup_file.stat().st_size
file_size_mb = file_size / (1024 * 1024)
print(f"✓ Backed up {db_path.name} -> {backup_file.name} ({file_size_mb:.2f} MB)")
return str(backup_file)
except sqlite3.Error as e:
print(f"✗ Error backing up {db_path.name}: {e}")
# Clean up failed backup file
if backup_file.exists():
backup_file.unlink()
return None
except Exception as e:
print(f"✗ Unexpected error backing up {db_path.name}: {e}")
if backup_file.exists():
backup_file.unlink()
return None
def check_database_status(db_path):
"""
Check if a database file is actually used (has tables)
Args:
db_path: Path to the database file
Returns:
Tuple of (is_used, table_count, file_size_mb)
"""
db_path = Path(db_path)
if not db_path.exists():
return (False, 0, 0.0)
file_size_mb = db_path.stat().st_size / (1024 * 1024)
try:
conn = sqlite3.connect(str(db_path))
with closing(conn):
cursor = conn.cursor()
# Get list of tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = cursor.fetchall()
table_count = len(tables)
# Consider database used if it has tables or is larger than 1KB
is_used = table_count > 0 or file_size_mb > 0.001
return (is_used, table_count, file_size_mb)
except sqlite3.Error:
return (False, 0, file_size_mb)
def find_database_files(root_dir=".", check_usage=True):
"""
Find all SQLite database files in the root directory
Args:
root_dir: Root directory to search (default: current directory)
check_usage: If True, check which databases are actually used
Returns:
List of database file paths, optionally sorted by priority (main database first)
"""
root = Path(root_dir)
db_files = []
# Find all .db files in root directory (not in subdirectories)
for db_file in root.glob("*.db"):
# Skip WAL and SHM files (they're not standalone databases)
if db_file.suffix == ".db":
db_files.append(db_file)
# Sort by priority: meshcore_bot.db first (main database), then others
def sort_key(db_file):
name = db_file.name.lower()
if name == "meshcore_bot.db":
return (0, name)
elif name == "bot_data.db":
return (1, name)
else:
return (2, name)
return sorted(db_files, key=sort_key)
def main():
"""Main backup function"""
# Get script directory (project root)
script_dir = Path(__file__).parent.absolute()
os.chdir(script_dir)
print("MeshCore Bot Database Backup")
print("=" * 50)
print(f"Working directory: {script_dir}")
print()
# Find all database files
db_files = find_database_files(script_dir)
if not db_files:
print("No database files found in the project root.")
return 1
print(f"Found {len(db_files)} database file(s):")
# Check which databases are actually used
active_databases = []
inactive_databases = []
for db_file in db_files:
is_used, table_count, file_size = check_database_status(db_file)
status = "✓ ACTIVE" if is_used else "○ EMPTY/UNUSED"
print(f" {status}: {db_file.name} ({table_count} tables, {file_size:.2f} MB)")
if is_used:
active_databases.append(db_file)
else:
inactive_databases.append(db_file)
print()
if not active_databases:
print("No active databases found. Nothing to backup.")
return 0
if inactive_databases:
print(f"Note: {len(inactive_databases)} database(s) appear to be empty/unused and will be skipped.")
print()
# Create backups (only for active databases)
backup_dir = script_dir / "backups"
successful_backups = []
failed_backups = []
for db_file in active_databases:
result = backup_database(db_file, backup_dir)
if result:
successful_backups.append(result)
else:
failed_backups.append(db_file.name)
print()
print("=" * 50)
print(f"Backup Summary:")
print(f" Successful: {len(successful_backups)}")
print(f" Failed: {len(failed_backups)}")
if successful_backups:
print(f"\nBackups saved to: {backup_dir}")
print("\nRecent backups:")
for backup in successful_backups:
print(f" - {Path(backup).name}")
if failed_backups:
print(f"\nFailed to backup:")
for db in failed_backups:
print(f" - {db}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())