From 133238dfca3e436e3d9d8b43df90015a4d93c214 Mon Sep 17 00:00:00 2001 From: Ada Werefox Date: Sun, 31 May 2026 18:43:03 -0700 Subject: [PATCH] Initial commit. --- .env.template | 15 +++++ .gitignore | 6 ++ requirements.txt | 2 + restore_backup.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 .env.template create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100755 restore_backup.py diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..b61412f --- /dev/null +++ b/.env.template @@ -0,0 +1,15 @@ +# Nextcloud Connection Details +NC_URL="https://nextcloud.example.com" +NC_USER="user" +NC_PASS="password" + +# Configuration File Paths +NC_TEMP_DOWNLOAD="/tmp/temp_download.7z" +# Temporary local extract path +TEMP_EXTRACT_PATH="/tmp/temp_download" +# Temporary local backup path +TEMP_BACKUP_PATH="/temp/backup/path" +# Plugin Configs default path +PLUGIN_CONFIGS_PATH="/mnt/c/Users/[CHANGEME]/AppData/Roaming/XIVLauncher/pluginConfigs" +# FFXIV Configs default path +FFXIV_CONFIGS_PATH="/mnt/c/Users/[CHANGEME]/Documents/My Games/FINAL FANTASY XIV - A Realm Reborn" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a350ce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +backup/* +FINAL FANTASY XIV - A Realm Reborn/* +pluginConfigs/* +temp/* +*.pyc +.env \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d6d2b93 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +nextcloud-api-wrapper==0.2.3 +python-dotenv==1.2.2 diff --git a/restore_backup.py b/restore_backup.py new file mode 100755 index 0000000..c0e9e43 --- /dev/null +++ b/restore_backup.py @@ -0,0 +1,167 @@ +#!/bin/env python3 + +import os +import shutil +import argparse +import subprocess +from datetime import datetime +from dotenv import load_dotenv +from nextcloud import NextCloud + +# Load environment variables from .env file +load_dotenv() + +# --- 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): + """Downloads the requested backup archive from Nextcloud""" + # Connect to Nextcloud + print("Connecting to Nextcloud...") + nc = NextCloud(NC_URL, user=NC_USER, password=NC_PASS) + + # Download 7z archive from Nextcloud + print(f"Downloading '{remote_archive_path}' from Nextcloud...") + try: + downloaded_file = nc.get_file(remote_archive_path) + downloaded_file.download(target=TEMP_DOWNLOAD_NAME) + except Exception as e: + print(f"Error downloading file: {e}") + return + + +def decompress_backup(): + """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_NAME, 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_NAME): + os.remove(TEMP_DOWNLOAD_NAME) + + +def backup_and_restore( + backup_config_path: str, restore_config_path: str, is_ffxiv_config: bool +): + """Create backup of current config and restore downloaded backup""" + # Create backup of FFXIV config directory if it exists and has content + if os.path.exists(restore_config_path) and os.listdir(restore_config_path): + print( + "Creating safety backup of " + "ffxiv" + if is_ffxiv_config + else "plugins" + f" configs at:\n -> {backup_config_path}/game" + ) + try: + shutil.copytree(restore_config_path, f"{backup_config_path}/game") + shutil.rmtree(restore_config_path) + except Exception as e: + print(f"Failed during ffxiv config backup creation phase: {e}") + return + + # Restore FFXIV config backup files + try: + print("Attempting to restore ffxiv 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 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) + + # Generate a unique, timestamped backup folder name in the temp backup directory + parent_dir = os.path.dirname(TEMP_BACKUP_PATH) + 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) + + decompress_backup() + + backup_and_restore(backup_dir, FFXIV_CONFIGS_PATH, True) + + backup_and_restore(backup_dir, PLUGIN_CONFIG_PATH, False) + + # Remove temporary extract files + try: + shutil.rmtree(TEMP_EXTRACT_PATH) + except Exception as e: + print(f"Failed to remove temporary extracted backup files: {e}") + return + + +if __name__ == "__main__": + main()