chore: Update everything
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Backup MariaDB database and website files", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--db-url", default=os.getenv("DB_URL"), help="Database URL (default: from DB_URL env var)")
|
||||
parser.add_argument("--site-dir", type=Path, required=True, help="Website directory to backup")
|
||||
parser.add_argument("--backup-dir", type=Path, default="/mnt/box/backup/droplet", help="Backup destination directory")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_db_url(db_url: str) -> dict:
|
||||
if not db_url:
|
||||
raise ValueError("Database URL is required")
|
||||
|
||||
parsed = urlparse(db_url)
|
||||
|
||||
if parsed.scheme != "mysql":
|
||||
raise ValueError("Unsupported database URL format")
|
||||
|
||||
return {
|
||||
"host": parsed.hostname or "localhost",
|
||||
"port": str(parsed.port or 3306),
|
||||
"user": parsed.username or "root",
|
||||
"password": parsed.password or "",
|
||||
"database": parsed.path.lstrip("/") if parsed.path else "",
|
||||
}
|
||||
|
||||
|
||||
def dump_database(db_params: dict, dump_path: Path) -> None:
|
||||
logger.info(f"Dumping database to {dump_path}")
|
||||
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
"--host",
|
||||
db_params["host"],
|
||||
"--port",
|
||||
db_params["port"],
|
||||
"--user",
|
||||
db_params["user"],
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
db_params["database"],
|
||||
]
|
||||
|
||||
if db_params["password"]:
|
||||
cmd.append(f"--password={db_params['password']}")
|
||||
|
||||
try:
|
||||
with dump_path.open("w") as f:
|
||||
subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True, check=True)
|
||||
logger.info("Database dump completed successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Database dump failed: {e.stderr}")
|
||||
raise
|
||||
|
||||
|
||||
def zip_directory(source_dir: Path, zip_path: Path) -> None:
|
||||
"""Zip a directory"""
|
||||
logger.info(f"Zipping directory {source_dir} to {zip_path}")
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in source_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
# Use relative path within the zip
|
||||
arcname = file_path.relative_to(source_dir.parent)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
logger.info(f"Directory zipped successfully: {zip_path}")
|
||||
|
||||
|
||||
def create_final_backup(temp_dir: Path, backup_dir: Path, timestamp: str) -> Path:
|
||||
final_backup_name = f"{timestamp}_ucsuzkalem.zip"
|
||||
final_backup_path = backup_dir / final_backup_name
|
||||
|
||||
logger.info(f"Creating final backup: {final_backup_path}")
|
||||
zip_directory(temp_dir, final_backup_path)
|
||||
|
||||
return final_backup_path
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if not args.db_url:
|
||||
raise ValueError("Database URL is required (set DB_URL env var or use --db-url)")
|
||||
|
||||
# Parse database URL
|
||||
db_params = parse_db_url(args.db_url)
|
||||
logger.info(f"Connecting to database: {db_params['host']}:{db_params['port']}/{db_params['database']}")
|
||||
|
||||
# Create timestamp
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
# Setup paths
|
||||
site_dir = Path(args.site_dir)
|
||||
backup_dir = Path(args.backup_dir)
|
||||
temp_dir = Path("/tmp") / f"backup_{timestamp}"
|
||||
|
||||
if not site_dir.exists():
|
||||
raise FileNotFoundError(f"Site directory does not exist: {site_dir}")
|
||||
|
||||
# Create directories
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Dump database
|
||||
db_dump_path = temp_dir / "database.sql"
|
||||
dump_database(db_params, db_dump_path)
|
||||
|
||||
# Zip website files
|
||||
|
||||
files_zip_path = temp_dir / "files.zip"
|
||||
zip_directory(site_dir, files_zip_path)
|
||||
|
||||
# Create final backup
|
||||
final_backup_path = create_final_backup(temp_dir, backup_dir, timestamp)
|
||||
|
||||
# Cleanup temp directory
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
logger.info(f"Backup completed successfully: {final_backup_path}")
|
||||
logger.info(f"Backup size: {final_backup_path.stat().st_size / (1024 * 1024):.1f} MB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user