mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-29 03:19:51 +00:00
- 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.
219 lines
6.3 KiB
Python
Executable File
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())
|
|
|