#!/bin/env python3 import os import shutil import argparse import subprocess from datetime import datetime from dotenv import load_dotenv from nextcloud import NextCloud # type: ignore # Load environment variables from .env file load_dotenv(os.curdir + "\\.env") # --- CONFIGURATION FROM ENVIRONMENT --- NC_URL = os.getenv("NC_URL") NC_USER = os.getenv("NC_USER") NC_PASS = os.getenv("NC_PASS") TEMP_DOWNLOAD_NAME = os.getenv("NC_TEMP_DOWNLOAD", "\tmp\temp_download.7z") TEMP_EXTRACT_PATH = os.getenv("TEMP_EXTRACT_PATH") TEMP_BACKUP_PATH = os.getenv("TEMP_BACKUP_PATH") PLUGIN_CONFIG_PATH = os.getenv("PLUGIN_CONFIGS_PATH") FFXIV_CONFIGS_PATH = os.getenv("FFXIV_CONFIGS_PATH") # -------------------------------------- def validate_environment(): """Validates that all necessary Nextcloud environment variables are set.""" missing = [] if not NC_URL: missing.append("NC_URL") if not NC_USER: missing.append("NC_USER") if not NC_PASS: missing.append("NC_PASS") if not NC_USER: missing.append("PLUGIN_CONFIGS_PATH") if not NC_PASS: missing.append("FFXIV_CONFIGS_PATH") if missing: raise ValueError( f"Missing required environment variables in .env: {', '.join(missing)}" ) def parse_arguments(): """Defines and parses command-line arguments.""" parser = argparse.ArgumentParser(description=""" Download a .7z archive from Nextcloud, backup local files, and replace them.""") parser.add_argument( "-r", "--remote", required=True, help="Path to the .7z archive file inside Nextcloud (e.g., 'backups\\data.7z')", ) return parser.parse_args() def download_backup( remote_archive_path: str, hostname=NC_URL, user=NC_USER, password=NC_PASS, temp_download_path=TEMP_DOWNLOAD_NAME, ): """Downloads the requested backup archive from Nextcloud""" # Connect to Nextcloud print("Connecting to Nextcloud...") nc = NextCloud(hostname, user=user, password=password) # Download 7z archive from Nextcloud print(f"Downloading '{remote_archive_path}' from Nextcloud...") try: downloaded_file = nc.get_file(remote_archive_path) # type: ignore downloaded_file.download(target=temp_download_path) except Exception as e: print(f"Error downloading file: {e}") return def decompress_backup( temp_extract_path=TEMP_EXTRACT_PATH, temp_download_path=TEMP_DOWNLOAD_NAME, ): """Extract the files from the backup""" # Uncompress the downloaded .7z archive into target directory print(f"Extracting .7z files to:\n -> {temp_extract_path}") # 7z flags used: # 'x' = extract with full paths # '-o...' = target output directory (no space between -o and the path) # '-y' = assume Yes on all queries (overwrite prompts) cmd = ["7z", "x", temp_download_path, f"-o{temp_extract_path}", "-y"] try: # Run command and capture output; raises CalledProcessError if return code != 0 subprocess.run( cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) print("Extraction completed successfully!") except subprocess.CalledProcessError as e: print(f"Error: 7z extraction failed. {e}") return finally: # Clean up downloaded archive file from configured path if os.path.exists(temp_download_path): os.remove(temp_download_path) def backup_and_restore( backup_config_path: str, restore_config_path: str, is_ffxiv_config: bool, temp_extract_path=TEMP_EXTRACT_PATH, ): """Create backup of current config and restore downloaded backup""" # Create backup of FFXIV config directory if it exists and has content print(backup_config_path) if os.path.exists(restore_config_path) and os.listdir(restore_config_path): print( "Creating safety backup of " + f"{'ffxiv' if is_ffxiv_config else 'plugins'} configs at:" + f"-> {backup_config_path}\\{'game' if is_ffxiv_config else 'plugins'}" ) try: shutil.copytree( restore_config_path, f"{backup_config_path}\\{'game' if is_ffxiv_config else 'plugins'}", dirs_exist_ok=True, ) shutil.rmtree(restore_config_path) except Exception as e: print( f"Failed during {'ffxiv' if is_ffxiv_config else 'plugins'} config backup creation phase: {e}" ) return # Restore FFXIV config backup files try: print( f"Attempting to restore {'ffxiv' if is_ffxiv_config else 'plugins'} config backups" ) shutil.copytree( f"{temp_extract_path}\\{"game" if is_ffxiv_config else "plugins"}\\", restore_config_path, dirs_exist_ok=True, ) except Exception as e: print(f"Failed during FFXIV config restore phase: {e}") return def temp_cleanup(temp_extract_path=TEMP_EXTRACT_PATH): # Remove temporary extract files try: shutil.rmtree(temp_extract_path) # type: ignore except Exception as e: print(f"Failed to remove temporary extracted backup files: {e}") return def create_backup_dir(temp_backup_path=TEMP_BACKUP_PATH) -> str: """Create the unique directory that will store the extracted backup files""" # Generate a unique, timestamped backup folder name in the temp backup directory parent_dir = os.path.dirname(temp_backup_path) # type: ignore timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") backup_dir = os.path.join(parent_dir, f"backup_{timestamp}") os.makedirs(backup_dir, exist_ok=True) return backup_dir def main(): # Parse command-line inputs and validate env args = parse_arguments() remote_archive_path = args.remote try: validate_environment() except ValueError as e: print(f"Configuration Error: {e}") return download_backup(remote_archive_path) backup_dir = create_backup_dir() decompress_backup() backup_and_restore(backup_dir, FFXIV_CONFIGS_PATH, True) # type: ignore backup_and_restore(backup_dir, PLUGIN_CONFIG_PATH, False) # type: ignore temp_cleanup() if __name__ == "__main__": main()