255 lines
7.6 KiB
Python
255 lines
7.6 KiB
Python
"""
|
|
SQLite Database Backup Script
|
|
Usage: python backup_db.py [backup|restore|list]
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Configuration
|
|
INSTANCE_DIR = Path(__file__).parent / 'instance'
|
|
DB_FILE = INSTANCE_DIR / 'machines.db'
|
|
BACKUP_DIR = INSTANCE_DIR / 'backups'
|
|
|
|
|
|
def ensure_backup_dir():
|
|
"""Create backup directory if it doesn't exist"""
|
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def backup_database():
|
|
"""Create a timestamped backup of the database"""
|
|
if not DB_FILE.exists():
|
|
print(f"❌ Database not found: {DB_FILE}")
|
|
return False
|
|
|
|
ensure_backup_dir()
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
|
|
|
|
try:
|
|
# Method 1: Simple file copy (faster)
|
|
shutil.copy2(DB_FILE, backup_file)
|
|
|
|
# Get file size
|
|
size = backup_file.stat().st_size
|
|
size_mb = size / (1024 * 1024)
|
|
|
|
print(f"✅ Backup created successfully!")
|
|
print(f" File: {backup_file.name}")
|
|
print(f" Size: {size_mb:.2f} MB")
|
|
print(f" Location: {backup_file}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"❌ Backup failed: {e}")
|
|
return False
|
|
|
|
|
|
def backup_database_with_integrity():
|
|
"""Create a backup using SQLite's built-in backup API (slower but safer)"""
|
|
if not DB_FILE.exists():
|
|
print(f"❌ Database not found: {DB_FILE}")
|
|
return False
|
|
|
|
ensure_backup_dir()
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
|
|
|
|
try:
|
|
# Connect to source database
|
|
source_conn = sqlite3.connect(str(DB_FILE))
|
|
|
|
# Connect to backup database
|
|
backup_conn = sqlite3.connect(str(backup_file))
|
|
|
|
# Perform backup
|
|
with backup_conn:
|
|
source_conn.backup(backup_conn)
|
|
|
|
source_conn.close()
|
|
backup_conn.close()
|
|
|
|
# Get file size
|
|
size = backup_file.stat().st_size
|
|
size_mb = size / (1024 * 1024)
|
|
|
|
print(f"✅ Backup created successfully (with integrity check)!")
|
|
print(f" File: {backup_file.name}")
|
|
print(f" Size: {size_mb:.2f} MB")
|
|
print(f" Location: {backup_file}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"❌ Backup failed: {e}")
|
|
if backup_file.exists():
|
|
backup_file.unlink() # Delete partial backup
|
|
return False
|
|
|
|
|
|
def list_backups():
|
|
"""List all available backups"""
|
|
ensure_backup_dir()
|
|
|
|
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
|
|
|
if not backups:
|
|
print("📂 No backups found")
|
|
return
|
|
|
|
print(f"\n📂 Available Backups ({len(backups)} total):")
|
|
print("=" * 80)
|
|
|
|
for i, backup in enumerate(backups, 1):
|
|
size = backup.stat().st_size / (1024 * 1024)
|
|
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
|
|
|
|
# Parse timestamp from filename
|
|
try:
|
|
parts = backup.stem.split('_')
|
|
date_str = parts[-2]
|
|
time_str = parts[-1]
|
|
backup_date = datetime.strptime(f"{date_str}_{time_str}", "%Y%m%d_%H%M%S")
|
|
date_display = backup_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
except:
|
|
date_display = mtime.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
print(f"{i:2d}. {backup.name}")
|
|
print(f" Date: {date_display}")
|
|
print(f" Size: {size:.2f} MB")
|
|
print()
|
|
|
|
|
|
def restore_database(backup_name=None):
|
|
"""Restore database from a backup"""
|
|
ensure_backup_dir()
|
|
|
|
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
|
|
|
if not backups:
|
|
print("❌ No backups found")
|
|
return False
|
|
|
|
# If no backup specified, show list and ask
|
|
if not backup_name:
|
|
list_backups()
|
|
print("=" * 80)
|
|
choice = input("Enter backup number to restore (or 'q' to quit): ").strip()
|
|
|
|
if choice.lower() == 'q':
|
|
print("❌ Restore cancelled")
|
|
return False
|
|
|
|
try:
|
|
index = int(choice) - 1
|
|
if index < 0 or index >= len(backups):
|
|
print("❌ Invalid backup number")
|
|
return False
|
|
backup_file = backups[index]
|
|
except ValueError:
|
|
print("❌ Invalid input")
|
|
return False
|
|
else:
|
|
# Find backup by name
|
|
backup_file = BACKUP_DIR / backup_name
|
|
if not backup_file.exists():
|
|
print(f"❌ Backup not found: {backup_name}")
|
|
return False
|
|
|
|
# Confirm restore
|
|
print(f"\n⚠️ WARNING: This will replace your current database!")
|
|
print(f" Current: {DB_FILE}")
|
|
print(f" Backup: {backup_file.name}")
|
|
confirm = input("\nType 'yes' to confirm restore: ").strip().lower()
|
|
|
|
if confirm != 'yes':
|
|
print("❌ Restore cancelled")
|
|
return False
|
|
|
|
try:
|
|
# Create a safety backup of current database
|
|
if DB_FILE.exists():
|
|
safety_backup = BACKUP_DIR / f'machines_before_restore_{datetime.now().strftime("%Y%m%d_%H%M%S")}.db'
|
|
shutil.copy2(DB_FILE, safety_backup)
|
|
print(f"✅ Safety backup created: {safety_backup.name}")
|
|
|
|
# Restore from backup
|
|
shutil.copy2(backup_file, DB_FILE)
|
|
|
|
print(f"✅ Database restored successfully!")
|
|
print(f" Restored from: {backup_file.name}")
|
|
print(f"\n⚠️ Remember to restart your Flask server!")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"❌ Restore failed: {e}")
|
|
return False
|
|
|
|
|
|
def cleanup_old_backups(keep_count=10):
|
|
"""Keep only the most recent N backups"""
|
|
ensure_backup_dir()
|
|
|
|
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
|
|
|
if len(backups) <= keep_count:
|
|
print(f"✅ No cleanup needed (found {len(backups)} backups, keeping {keep_count})")
|
|
return
|
|
|
|
to_delete = backups[keep_count:]
|
|
|
|
print(f"\n🗑️ Cleanup: Keeping {keep_count} most recent backups")
|
|
print(f" Deleting {len(to_delete)} old backups...")
|
|
|
|
for backup in to_delete:
|
|
try:
|
|
backup.unlink()
|
|
print(f" ✓ Deleted: {backup.name}")
|
|
except Exception as e:
|
|
print(f" ✗ Failed to delete {backup.name}: {e}")
|
|
|
|
print(f"✅ Cleanup complete!")
|
|
|
|
|
|
def main():
|
|
"""Main entry point"""
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python backup_db.py [backup|restore|list|cleanup]")
|
|
print("\nCommands:")
|
|
print(" backup - Create a new backup")
|
|
print(" restore - Restore from a backup")
|
|
print(" list - List all available backups")
|
|
print(" cleanup - Delete old backups (keep 10 most recent)")
|
|
return
|
|
|
|
command = sys.argv[1].lower()
|
|
|
|
if command == 'backup':
|
|
backup_database()
|
|
elif command == 'backup-safe':
|
|
backup_database_with_integrity()
|
|
elif command == 'restore':
|
|
backup_name = sys.argv[2] if len(sys.argv) > 2 else None
|
|
restore_database(backup_name)
|
|
elif command == 'list':
|
|
list_backups()
|
|
elif command == 'cleanup':
|
|
keep = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
|
cleanup_old_backups(keep)
|
|
else:
|
|
print(f"❌ Unknown command: {command}")
|
|
print(" Use: backup, restore, list, or cleanup")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |