From 0f2cfce7adbceead5a42857d9b1527ba35d58f18 Mon Sep 17 00:00:00 2001 From: kj Date: Tue, 14 Jul 2026 11:51:45 -0300 Subject: [PATCH] first commit --- .gitignore | 1 + README.md | 152 ++++++++++++++++++ backup-rocknix.sh | 203 +++++++++++++++++++++++ config.sh | 86 ++++++++++ lib-common.sh | 343 +++++++++++++++++++++++++++++++++++++++ restore-rocknix.sh | 393 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1178 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 backup-rocknix.sh create mode 100755 config.sh create mode 100755 lib-common.sh create mode 100755 restore-rocknix.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9736ce --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/backups/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..81cdc26 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# ROCKNIX Backup & Restore + +Backup and restore script for ROCKNIX emulator configurations. Allows syncing optimal settings between multiple consoles via SSH. + +## Features + +- **Specific backup**: Full backup including all device/user-specific settings +- **Generic backup**: Portable backup with device-specific settings filtered out +- **Selective restore**: Choose which components to restore via interactive dialog +- **Safety backup**: Automatically saves current settings before restoring +- **Device detection**: Automatically identifies the target ROCKNIX device +- **SSH support**: Password and key-based authentication + +## Requirements + +```bash +# Fedora/RHEL +sudo dnf install tar dialog rsync sshpass + +# Debian/Ubuntu +sudo apt install tar dialog rsync sshpass +``` + +## Usage + +### Backup + +```bash +# Specific backup (includes all settings) +./backup-rocknix.sh + +# Generic backup (portable between devices) +./backup-rocknix.sh --type generic + +# Custom host and output directory +./backup-rocknix.sh --host myconsole.local --output /path/to/backups +``` + +### Restore + +```bash +# Interactive restore (lists available backups) +./restore-rocknix.sh + +# Restore specific backup file +./restore-rocknix.sh --backup backups/rocknix-config-specific-rg-cubexx-20260714-113010.tar.gz + +# Restore all components without prompting +./restore-rocknix.sh --backup backup.tar.gz --all +``` + +## Options + +### backup-rocknix.sh + +| Option | Description | Default | +|-------------------|-------------------------|------------------| +| `--host HOST` | Console hostname/IP | `rgcubexx.local` | +| `--user USER` | SSH username | `root` | +| `--password PASS` | SSH password | (prompts) | +| `--output DIR` | Backup output directory | `backups/` | +| `--type TYPE` | `specific` or `generic` | `specific` | +| `--help` | Show help | | + +### restore-rocknix.sh + +| Option | Description | Default | +|-------------------|----------------------------|------------------| +| `--host HOST` | Target console hostname/IP | `rgcubexx.local` | +| `--user USER` | SSH username | `root` | +| `--password PASS` | SSH password | (prompts) | +| `--backup FILE` | Backup file to restore | (interactive) | +| `--all` | Restore all components | `false` | +| `--help` | Show help | | + +## Components + +The following components can be backed up and restored: + +| Component | Description | +|--------------------------|--------------------------------------------| +| `retroarch-core-options` | RetroArch global core options | +| `retroarch-opt` | Per-core `.opt` files | +| `retroarch-rmp` | Button remaps `.rmp` files | +| `retroarch-config` | Per-core overrides (`config/`) | +| `shaders` | Shader packs and presets | +| `ppsspp` | PPSSPP (PSP) settings | +| `drastic` | DraStic (NDS) settings | +| `other-emu` | Flycast, DuckStation, Yabasanshiro configs | + +## Backup Types + +### Specific Backup + +Includes all settings, including device-specific values like: +- Screen resolution and scaling +- Video/audio drivers +- Input mappings +- Account credentials (RetroAchievements, cloud sync) + +Best for: Backing up the same device or identical hardware. + +### Generic Backup + +Filters out device-specific settings for portability: +- Screen dimensions (`video_fullscreen_x/y`) +- Scale factors (`menu_scale_factor`) +- Drivers (`video_driver`, `audio_driver`) +- Credentials and tokens + +Best for: Sharing configs between different devices or users. + +## Configuration + +Edit `config.sh` to customize: + +- `ROCKNIX_HOST`: Default console hostname +- `ROCKNIX_STORAGE`: Storage path on device (default: `/storage`) +- `BACKUP_DIR`: Local backup directory +- `SPECIFIC_FILES`: List of files to back up +- `GENERIC_EXCLUDES`: Files excluded from generic backups +- `GENERIC_SETTINGS_FILTER`: Settings patterns removed from generic backups + +## Examples + +```bash +# First-time setup: create a specific backup +./backup-rocknix.sh --host rgcubexx.local + +# Clone settings to another device of the same model +./backup-rocknix.sh --type generic +./restore-rocknix.sh --host rgcubexx2.local --backup backups/rocknix-config-generic-*.tar.gz + +# List available backups +./restore-rocknix.sh # Shows interactive menu +``` + +## File Structure + +``` +rocknix-backup/ +├── backup-rocknix.sh # Backup script +├── restore-rocknix.sh # Restore script +├── lib-common.sh # Shared functions (SSH, rsync, logging) +├── config.sh # Default configuration +├── backups/ # Backup files (*.tar.gz) +└── PRE-ANALISIS-ROCKNIX.md # Technical analysis document +``` + +## License + +MIT diff --git a/backup-rocknix.sh b/backup-rocknix.sh new file mode 100755 index 0000000..0c107f8 --- /dev/null +++ b/backup-rocknix.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# backup-rocknix.sh - Backup script for ROCKNIX configurations +# +# Usage: +# ./backup-rocknix.sh [OPTIONS] +# +# Options: +# --host HOST Console host (default: rgeubexx.local) +# --user USER SSH user (default: root) +# --output DIR Output directory for backups +# --type TYPE Backup type: specific or generic (default: specific) +# --help Show help + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib-common.sh" + +# Default values +BACKUP_TYPE="specific" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --host) + ROCKNIX_HOST="$2" + shift 2 + ;; + --user) + ROCKNIX_USER="$2" + shift 2 + ;; + --password) + ROCKNIX_PASSWORD="$2" + shift 2 + ;; + --output) + BACKUP_DIR="$2" + shift 2 + ;; + --type) + BACKUP_TYPE="$2" + shift 2 + ;; + --help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --host HOST Console host (default: $ROCKNIX_HOST)" + echo " --user USER SSH user (default: $ROCKNIX_USER)" + echo " --password PASS SSH password" + echo " --output DIR Output directory" + echo " --type TYPE Backup type: specific, generic (default: specific)" + echo " --help Show this help" + exit 0 + ;; + *) + log ERROR "Unknown option: $1" + exit 1 + ;; + esac +done + +# Verify valid type +if [[ "$BACKUP_TYPE" != "specific" && "$BACKUP_TYPE" != "generic" ]]; then + log ERROR "Invalid backup type: $BACKUP_TYPE (use: specific, generic)" + exit 1 +fi + +# Check dependencies +check_deps || exit 1 + +# Ask for password if not configured +ask_password || exit 1 + +# Verify SSH connection +ssh_check "$ROCKNIX_HOST" "$ROCKNIX_USER" || exit 1 + +# Detect device +DEVICE=$(detect_device "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null || echo "unknown") +DEVICE_SLUG=$(echo "$DEVICE" | tr ' ' '-' | tr '[:upper:]' '[:lower:]') + +# Create backups directory +mkdir -p "$BACKUP_DIR" + +# Create temporary directory +TMP_DIR=$(create_tmp_dir "rocknix-backup") +trap 'cleanup_tmp "$TMP_DIR"' EXIT + +log INFO "Starting ${BACKUP_TYPE} backup..." +log INFO "Device: ${DEVICE}" +log INFO "Host: ${ROCKNIX_USER}@${ROCKNIX_HOST}" + +# Select file list based on type +if [[ "$BACKUP_TYPE" == "specific" ]]; then + FILES_TO_BACKUP=("${SPECIFIC_FILES[@]}") +else + # For generic, exclude device-specific files + FILES_TO_BACKUP=() + for file in "${SPECIFIC_FILES[@]}"; do + skip=false + for exclude in "${GENERIC_EXCLUDES[@]}"; do + if [[ "$file" == "$exclude" ]]; then + skip=true + break + fi + done + if [[ "$skip" == "false" ]]; then + FILES_TO_BACKUP+=("$file") + fi + done +fi + +# Download files +log INFO "Downloading files..." +DOWNLOADED=0 +FAILED=0 + +for file in "${FILES_TO_BACKUP[@]}"; do + local_path="${TMP_DIR}/${file}" + remote_path="${file}" + + # Create local directory + mkdir -p "$(dirname "$local_path")" + + # Download + if [[ -d "${local_path}" ]] || [[ "$file" == */ ]]; then + # It's a directory + mkdir -p "$local_path" + if download_dir "$remote_path" "$local_path" "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + DOWNLOADED=$((DOWNLOADED + 1)) + log DEBUG "Downloaded: ${file}" + else + FAILED=$((FAILED + 1)) + log WARN "Could not download: ${file}" + fi + else + # It's a file + if download_file "$remote_path" "$local_path" "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + DOWNLOADED=$((DOWNLOADED + 1)) + log DEBUG "Downloaded: ${file}" + else + FAILED=$((FAILED + 1)) + log WARN "Could not download: ${file}" + fi + fi +done + +log INFO "Downloaded: ${DOWNLOADED} files" + +# For generic backup, filter device/user-specific settings +if [[ "$BACKUP_TYPE" == "generic" ]]; then + log INFO "Filtering device/user-specific settings..." + + # Filter retroarch-core-options.cfg if it exists + if [[ -f "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" ]]; then + filter_specific_settings \ + "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" \ + "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg.filtered" + mv "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg.filtered" \ + "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" + fi + + # Filter overrides (.cfg files) + find "${TMP_DIR}/.config/retroarch/config/" -name "*.cfg" -type f 2>/dev/null | while read -r cfg_file; do + filter_specific_settings "$cfg_file" "${cfg_file}.filtered" + mv "${cfg_file}.filtered" "$cfg_file" + done +fi + +# Create backup filename +TIMESTAMP=$(date '+%Y%m%d-%H%M%S') +BACKUP_NAME="rocknix-config-${BACKUP_TYPE}-${DEVICE_SLUG}-${TIMESTAMP}.tar.gz" +BACKUP_FILE="${BACKUP_DIR}/${BACKUP_NAME}" + +# Compress +log INFO "Compressing backup..." +tar -czf "$BACKUP_FILE" -C "$TMP_DIR" . + +# Verify +if [[ -f "$BACKUP_FILE" ]]; then + BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + BACKUP_FILES=$(tar -tzf "$BACKUP_FILE" | wc -l) + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Backup completed successfully${NC}" + echo -e "${GREEN}========================================${NC}" + echo "" + echo -e " Type: ${CYAN}${BACKUP_TYPE}${NC}" + echo -e " Device: ${CYAN}${DEVICE}${NC}" + echo -e " File: ${CYAN}${BACKUP_NAME}${NC}" + echo -e " Location: ${CYAN}${BACKUP_DIR}${NC}" + echo -e " Size: ${CYAN}${BACKUP_SIZE}${NC}" + echo -e " Files: ${CYAN}${BACKUP_FILES}${NC}" + echo "" + echo -e " ${YELLOW}To restore, run:${NC}" + echo -e " ${CYAN}./restore-rocknix.sh --backup ${BACKUP_FILE}${NC}" + echo "" +else + log ERROR "Could not create backup file" + exit 1 +fi diff --git a/config.sh b/config.sh new file mode 100755 index 0000000..1cf0721 --- /dev/null +++ b/config.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# config.sh - Default configuration for ROCKNIX backup/restore + +# ROCKNIX console host +ROCKNIX_HOST="${ROCKNIX_HOST:-rgcubexx.local}" +ROCKNIX_USER="${ROCKNIX_USER:-root}" +ROCKNIX_PASSWORD="${ROCKNIX_PASSWORD:-}" +ROCKNIX_STORAGE="${ROCKNIX_STORAGE:-/storage}" + +# Local backup directory (resolved in lib-common.sh) +BACKUP_DIR="${BACKUP_DIR:-backups}" + +# Files to back up (relative to /storage) +# Specific backup (includes everything) +SPECIFIC_FILES=( + # RetroArch global + ".config/retroarch/retroarch.cfg" + ".config/retroarch/retroarch-core-options.cfg" + ".config/retroarch/retroarch32bit-append.cfg" + ".config/retroarch/retroarch64bit-append.cfg" + + # RetroArch .opt (per-core options at root level) + ".config/retroarch/Flycast.opt" + ".config/retroarch/Supafaust.opt" + ".config/retroarch/ParaLLEl N64.opt" + + # RetroArch .rmp (remaps at root level) + ".config/retroarch/TATE-MAME 2003-Plus.rmp" + + # RetroArch overrides (complete directory) + ".config/retroarch/config/" + + # Shaders + "shaders/" + + # PPSSPP + ".config/ppsspp/PSP/SYSTEM/ppsspp.ini" + ".config/ppsspp/PSP/SYSTEM/controls.ini" + + # DraStic + ".config/drastic/config/" + + # Other emulators + ".config/game/configs/" + ".config/flycast/emu.cfg" + ".config/flycast/flycast.gptk" + ".config/flycast/mappings/" + ".config/duckstation/settings.ini" + ".config/yabasanshiro/.config" +) + +# Generic backup: exclude files with device/user-specific settings +GENERIC_EXCLUDES=( + ".config/retroarch/retroarch.cfg" + ".config/retroarch/retroarch32bit-append.cfg" + ".config/retroarch/retroarch64bit-append.cfg" +) + +# Settings patterns to filter from overrides in generic backup +GENERIC_SETTINGS_FILTER=( + "menu_scale_factor" + "menu_widget_scale_factor" + "video_fullscreen_x" + "video_fullscreen_y" + "video_windowed_position_width" + "video_windowed_position_height" + "video_refresh_rate" + "custom_viewport_width" + "custom_viewport_height" + "video_driver" + "audio_driver" + "input_driver" + "input_joypad_driver" + "cheevos_username" + "cheevos_password" + "cheevos_token" + "cheevos_custom_host" + "google_drive_refresh_token" + "webdav_password" + "webdav_username" + "kiosk_mode_password" + "content_show_settings_password" + "netplay_ip_address" + "netplay_ip_port" + "netplay_password" +) diff --git a/lib-common.sh b/lib-common.sh new file mode 100755 index 0000000..d775d12 --- /dev/null +++ b/lib-common.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# lib-common.sh - Shared functions for ROCKNIX backup/restore + +set -euo pipefail + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Load configuration +source "$SCRIPT_DIR/config.sh" + +# Resolve BACKUP_DIR relative to script +case "$BACKUP_DIR" in + /*) ;; # Absolute path, do nothing + *) BACKUP_DIR="${SCRIPT_DIR}/${BACKUP_DIR}" ;; +esac + +# Colores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Logging +log() { + local level="$1" + shift + local msg="$*" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + case "$level" in + INFO) echo -e "${GREEN}[${timestamp}] INFO:${NC} $msg" ;; + WARN) echo -e "${YELLOW}[${timestamp}] WARN:${NC} $msg" ;; + ERROR) echo -e "${RED}[${timestamp}] ERROR:${NC} $msg" >&2 ;; + DEBUG) echo -e "${BLUE}[${timestamp}] DEBUG:${NC} $msg" ;; + esac +} + +# Check dependencies +check_deps() { + local missing=() + for dep in tar dialog rsync sshpass; do + if ! command -v "$dep" &>/dev/null; then + missing+=("$dep") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + log ERROR "Missing dependencies: ${missing[*]}" + log INFO "Install with: sudo dnf install ${missing[*]}" + return 1 + fi + return 0 +} + +# Ask for password if not configured +ask_password() { + if [[ -z "$ROCKNIX_PASSWORD" ]]; then + read -s -p "Password for ${ROCKNIX_USER}@${ROCKNIX_HOST}: " ROCKNIX_PASSWORD + echo "" + if [[ -z "$ROCKNIX_PASSWORD" ]]; then + log ERROR "Empty password" + return 1 + fi + fi + export ROCKNIX_PASSWORD +} + +# Build SSH connection string +ssh_target() { + local host="${1:-$ROCKNIX_HOST}" + local user="${2:-$ROCKNIX_USER}" + echo "${user}@${host}" +} + +# Verify SSH connection +ssh_check() { + local host="${1:-$ROCKNIX_HOST}" + local user="${2:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + log INFO "Checking connection to ${target}..." + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + if sshpass -p "$ROCKNIX_PASSWORD" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$target" "echo ok" &>/dev/null; then + log INFO "Connection successful" + return 0 + fi + else + if ssh -o ConnectTimeout=5 -o BatchMode=yes "$target" "echo ok" &>/dev/null; then + log INFO "Connection successful" + return 0 + fi + fi + + log ERROR "Could not connect to ${target}" + log INFO "Make sure SSH is enabled on the ROCKNIX console" + return 1 +} + +# Execute remote command +remote_exec() { + local cmd="$1" + local host="${2:-$ROCKNIX_HOST}" + local user="${3:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$target" "$cmd" 2>&1 + else + ssh -o ConnectTimeout=10 "$target" "$cmd" 2>&1 + fi +} + +# List remote files +remote_list() { + local remote_path="$1" + local host="${2:-$ROCKNIX_HOST}" + local user="${3:-$ROCKNIX_USER}" + + remote_exec "ls -la ${ROCKNIX_STORAGE}/${remote_path}/ 2>/dev/null" "$host" "$user" +} + +# Download entire directory via rsync +download_dir() { + local remote_path="$1" + local local_path="$2" + local host="${3:-$ROCKNIX_HOST}" + local user="${4:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + mkdir -p "$local_path" + + local rsync_opts=(-avz --timeout=30 -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10") + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" rsync "${rsync_opts[@]}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_path}/" \ + "${local_path}/" 2>&1 + else + rsync "${rsync_opts[@]}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_path}/" \ + "${local_path}/" 2>&1 + fi +} + +# Upload entire directory via rsync +upload_dir() { + local local_path="$1" + local remote_path="$2" + local host="${3:-$ROCKNIX_HOST}" + local user="${4:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + local rsync_opts=(-avz --timeout=30 -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10") + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" rsync "${rsync_opts[@]}" \ + "${local_path}/" \ + "${target}:${ROCKNIX_STORAGE}/${remote_path}/" 2>&1 + else + rsync "${rsync_opts[@]}" \ + "${local_path}/" \ + "${target}:${ROCKNIX_STORAGE}/${remote_path}/" 2>&1 + fi +} + +# Download a single file +download_file() { + local remote_file="$1" + local local_file="$2" + local host="${3:-$ROCKNIX_HOST}" + local user="${4:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + mkdir -p "$(dirname "$local_file")" + + local rsync_opts=(-avz --timeout=30 -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10") + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" rsync "${rsync_opts[@]}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_file}" \ + "${local_file}" 2>&1 + else + rsync "${rsync_opts[@]}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_file}" \ + "${local_file}" 2>&1 + fi +} + +# Upload a single file +upload_file() { + local local_file="$1" + local remote_file="$2" + local host="${3:-$ROCKNIX_HOST}" + local user="${4:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + local rsync_opts=(-avz --timeout=30 -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10") + + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" rsync "${rsync_opts[@]}" \ + "${local_file}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_file}" 2>&1 + else + rsync "${rsync_opts[@]}" \ + "${local_file}" \ + "${target}:${ROCKNIX_STORAGE}/${remote_file}" 2>&1 + fi +} + +# Direct backup via tar over SSH (no temporary files) +backup_via_ssh() { + local files=("$@") + local host="${ROCKNIX_HOST}" + local user="${ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + # Build remote tar command + local tar_args="" + for file in "${files[@]}"; do + tar_args="${tar_args} ${ROCKNIX_STORAGE}/${file}" + done + + # Execute remote tar and compress locally + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + sshpass -p "$ROCKNIX_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$target" \ + "tar czf - -C ${ROCKNIX_STORAGE} ${tar_args}" 2>/dev/null + else + ssh -o ConnectTimeout=10 "$target" \ + "tar czf - -C ${ROCKNIX_STORAGE} ${tar_args}" 2>/dev/null + fi +} + +# Detect ROCKNIX device +detect_device() { + local host="${1:-$ROCKNIX_HOST}" + local user="${2:-$ROCKNIX_USER}" + local target + target=$(ssh_target "$host" "$user") + + log INFO "Detecting device..." >&2 + + local device_info + if [[ -n "$ROCKNIX_PASSWORD" ]]; then + device_info=$(sshpass -p "$ROCKNIX_PASSWORD" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$target" \ + "cat ${ROCKNIX_STORAGE}/device_info_ROCKNIX_*.txt 2>/dev/null" 2>/dev/null || true) + else + device_info=$(ssh -o ConnectTimeout=5 "$target" \ + "cat ${ROCKNIX_STORAGE}/device_info_ROCKNIX_*.txt 2>/dev/null" 2>/dev/null || true) + fi + + if [[ -n "$device_info" ]]; then + local device_name + device_name=$(echo "$device_info" | grep "^DEVICE_NAME=" | cut -d'=' -f2 | tr -d '"') + local cfw_version + cfw_version=$(echo "$device_info" | grep "^CFW_VERSION=" | cut -d'=' -f2 | tr -d '"') + + if [[ -n "$device_name" ]]; then + log INFO "Device: ${device_name} (ROCKNIX ${cfw_version})" >&2 + echo "${device_name}" + return 0 + fi + fi + + log WARN "Could not detect device" >&2 + echo "Unknown" + return 1 +} + +# Create temporary directory +create_tmp_dir() { + local prefix="${1:-rocknix-backup}" + local tmp_dir + tmp_dir=$(mktemp -d "/tmp/${prefix}-XXXXXX") + echo "$tmp_dir" +} + +# Clean up temporary directory +cleanup_tmp() { + local tmp_dir="$1" + if [[ -d "$tmp_dir" ]]; then + rm -rf "$tmp_dir" + fi +} + +# Get human-readable size +human_size() { + local bytes="$1" + if [[ $bytes -ge 1073741824 ]]; then + echo "$(echo "scale=1; $bytes/1073741824" | bc)G" + elif [[ $bytes -ge 1048576 ]]; then + echo "$(echo "scale=1; $bytes/1048576" | bc)M" + elif [[ $bytes -ge 1024 ]]; then + echo "$(echo "scale=1; $bytes/1024" | bc)K" + else + echo "${bytes}B" + fi +} + +# Filter device-specific settings from file +filter_specific_settings() { + local input_file="$1" + local output_file="$2" + + cp "$input_file" "$output_file" + + for setting in "${GENERIC_SETTINGS_FILTER[@]}"; do + sed -i "/^${setting} = /d" "$output_file" + done +} + +# Show contents of a backup file +show_backup_contents() { + local backup_file="$1" + + if [[ ! -f "$backup_file" ]]; then + log ERROR "File not found: $backup_file" + return 1 + fi + + echo "" + log INFO "Backup contents:" + tar -tzf "$backup_file" | head -50 + + local total_files + total_files=$(tar -tzf "$backup_file" | wc -l) + local total_size + total_size=$(du -h "$backup_file" | cut -f1) + + echo "" + echo -e "${CYAN}Total: ${total_files} archivos, ${total_size}${NC}" +} diff --git a/restore-rocknix.sh b/restore-rocknix.sh new file mode 100755 index 0000000..5ba4328 --- /dev/null +++ b/restore-rocknix.sh @@ -0,0 +1,393 @@ +#!/usr/bin/env bash +# restore-rocknix.sh - Restore script for ROCKNIX configurations +# +# Usage: +# ./restore-rocknix.sh [OPTIONS] +# +# Options: +# --host HOST Target console host +# --user USER SSH user (default: root) +# --backup FILE .tar.gz backup file to restore +# --all Restore all components without prompting +# --help Show help + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib-common.sh" + +# Default values +BACKUP_FILE="" +RESTORE_ALL=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --host) + ROCKNIX_HOST="$2" + shift 2 + ;; + --user) + ROCKNIX_USER="$2" + shift 2 + ;; + --password) + ROCKNIX_PASSWORD="$2" + shift 2 + ;; + --backup) + BACKUP_FILE="$2" + shift 2 + ;; + --all) + RESTORE_ALL=true + shift + ;; + --help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --host HOST Target console host" + echo " --user USER SSH user (default: $ROCKNIX_USER)" + echo " --password PASS SSH password" + echo " --backup FILE .tar.gz backup file to restore" + echo " --all Restore all components" + echo " --help Show this help" + exit 0 + ;; + *) + log ERROR "Unknown option: $1" + exit 1 + ;; + esac +done + +# Check dependencies +check_deps || exit 1 + +# Ask for password if not configured +ask_password || exit 1 + +# If no backup specified, list available ones +if [[ -z "$BACKUP_FILE" ]]; then + log INFO "Searching for backups in ${BACKUP_DIR}..." + + if [[ ! -d "$BACKUP_DIR" ]]; then + log ERROR "Backup directory not found: $BACKUP_DIR" + log INFO "Run first: ./backup-rocknix.sh" + exit 1 + fi + + BACKUPS=($(find "$BACKUP_DIR" -name "rocknix-config-*.tar.gz" -type f | sort -r)) + + if [[ ${#BACKUPS[@]} -eq 0 ]]; then + log ERROR "No backups found in ${BACKUP_DIR}" + log INFO "Run first: ./backup-rocknix.sh" + exit 1 + fi + + echo "" + echo -e "${CYAN}Available backups:${NC}" + echo "" + + for i in "${!BACKUPS[@]}"; do + local backup="${BACKUPS[$i]}" + local name=$(basename "$backup") + local size=$(du -h "$backup" | cut -f1) + local date=$(stat -c %y "$backup" 2>/dev/null | cut -d'.' -f1) + + echo -e " ${GREEN}$((i+1))${NC}) ${name}" + echo -e " Size: ${size} | Date: ${date}" + done + + echo "" + read -p "Select backup (1-${#BACKUPS[@]}): " choice + + if [[ "$choice" -lt 1 || "$choice" -gt ${#BACKUPS[@]} ]]; then + log ERROR "Invalid selection" + exit 1 + fi + + BACKUP_FILE="${BACKUPS[$((choice-1))]}" +fi + +# Verify backup file exists +if [[ ! -f "$BACKUP_FILE" ]]; then + log ERROR "Backup file not found: $BACKUP_FILE" + exit 1 +fi + +# Show backup contents +show_backup_contents "$BACKUP_FILE" + +# Verify connection with target console +echo "" +ssh_check "$ROCKNIX_HOST" "$ROCKNIX_USER" || exit 1 + +# Detect target device +DEVICE=$(detect_device "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null || echo "unknown") +log INFO "Target device: ${DEVICE}" + +# Check backup type +BACKUP_NAME=$(basename "$BACKUP_FILE") +if [[ "$BACKUP_NAME" == *"generic"* ]]; then + log WARN "This is a GENERIC backup (without device-specific settings)" + log WARN "Make sure the target device is compatible" + echo "" + read -p "Continue? (y/N): " confirm + if [[ "$confirm" != "s" && "$confirm" != "S" ]]; then + log INFO "Restore cancelled" + exit 0 + fi +fi + +# Create temporary directory +TMP_DIR=$(create_tmp_dir "rocknix-restore") +trap 'cleanup_tmp "$TMP_DIR"' EXIT + +# Extract backup +log INFO "Extracting backup..." +tar -xzf "$BACKUP_FILE" -C "$TMP_DIR" + +# Select components to restore +if [[ "$RESTORE_ALL" == "false" ]]; then + echo "" + echo -e "${CYAN}Which components do you want to restore?${NC}" + echo "" + + COMPONENTS=( + "retroarch-core-options:RetroArch core-options" + "retroarch-opt:RetroArch .opt (core options)" + "retroarch-rmp:RetroArch .rmp (remaps)" + "retroarch-config:RetroArch overrides (config/)" + "shaders:Shaders" + "ppsspp:PPSSPP" + "drastic:DraStic" + "other-emu:Other emulators" + ) + + SELECTION=() + + for i in "${!COMPONENTS[@]}"; do + IFS=':' read -r key label <<< "${COMPONENTS[$i]}" + + # Determine if there are files of this type + case "$key" in + retroarch-core-options) + [[ -f "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" ]] && available=true || available=false + ;; + retroarch-opt) + find "${TMP_DIR}/.config/retroarch/" -maxdepth 1 -name "*.opt" -type f 2>/dev/null | grep -q . && available=true || available=false + ;; + retroarch-rmp) + find "${TMP_DIR}/.config/retroarch/" -maxdepth 1 -name "*.rmp" -type f 2>/dev/null | grep -q . && available=true || available=false + ;; + retroarch-config) + [[ -d "${TMP_DIR}/.config/retroarch/config/" ]] && available=true || available=false + ;; + shaders) + [[ -d "${TMP_DIR}/shaders/" ]] && available=true || available=false + ;; + ppsspp) + [[ -f "${TMP_DIR}/.config/ppsspp/PSP/SYSTEM/ppsspp.ini" ]] && available=true || available=false + ;; + drastic) + [[ -d "${TMP_DIR}/.config/drastic/config/" ]] && available=true || available=false + ;; + other-emu) + [[ -d "${TMP_DIR}/.config/game/" ]] || \ + [[ -d "${TMP_DIR}/.config/flycast/" ]] || \ + [[ -f "${TMP_DIR}/.config/duckstation/settings.ini" ]] || \ + [[ -f "${TMP_DIR}/.config/yabasanshiro/.config" ]] && available=true || available=false + ;; + esac + + if [[ "$available" == "true" ]]; then + SELECTION+=("$key" "$label" "on") + else + SELECTION+=("$key" "$label" "off") + fi + done + + CHOICES=$(dialog --checklist "Components to restore:" \ + 20 60 12 \ + "${SELECTION[@]}" \ + 3>&1 1>&2 2>&3) + + # If user cancels + if [[ $? -ne 0 ]]; then + log INFO "Restore cancelled" + exit 0 + fi +else + # Restore all + CHOICES="retroarch-core-options retroarch-opt retroarch-rmp retroarch-config shaders ppsspp drastic other-emu" +fi + +# Confirm restore +echo "" +echo -e "${YELLOW}Restore the following components on ${ROCKNIX_HOST}?${NC}" +for choice in $CHOICES; do + case "$choice" in + retroarch-core-options) echo " - RetroArch core-options" ;; + retroarch-opt) echo " - RetroArch .opt" ;; + retroarch-rmp) echo " - RetroArch .rmp" ;; + retroarch-config) echo " - RetroArch overrides" ;; + shaders) echo " - Shaders" ;; + ppsspp) echo " - PPSSPP" ;; + drastic) echo " - DraStic" ;; + other-emu) echo " - Other emulators" ;; + esac +done + +echo "" +read -p "Confirm restore? (y/N): " confirm +if [[ "$confirm" != "s" && "$confirm" != "S" ]]; then + log INFO "Restore cancelled" + exit 0 +fi + +# Create safety backup on target console +log INFO "Creating safety backup on console..." +SAFETY_DIR="${TMP_DIR}/safety" +mkdir -p "$SAFETY_DIR" + +# Download current files before overwriting +for choice in $CHOICES; do + case "$choice" in + retroarch-core-options) + download_file ".config/retroarch/retroarch-core-options.cfg" \ + "${SAFETY_DIR}/retroarch-core-options.cfg" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null || true + ;; + # Add others if needed + esac +done + +log INFO "Safety backup created" + +# Restore files +log INFO "Restoring files..." +RESTORED=0 +FAILED=0 + +for choice in $CHOICES; do + log INFO "Restoring: ${choice}..." + + case "$choice" in + retroarch-core-options) + if [[ -f "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" ]]; then + if upload_file "${TMP_DIR}/.config/retroarch/retroarch-core-options.cfg" \ + ".config/retroarch/retroarch-core-options.cfg" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + log WARN "Error restoring retroarch-core-options.cfg" + fi + fi + ;; + retroarch-opt) + for opt_file in "${TMP_DIR}/.config/retroarch/"*.opt; do + if [[ -f "$opt_file" ]]; then + filename=$(basename "$opt_file") + if upload_file "$opt_file" \ + ".config/retroarch/${filename}" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + fi + fi + done + ;; + retroarch-rmp) + for rmp_file in "${TMP_DIR}/.config/retroarch/"*.rmp; do + if [[ -f "$rmp_file" ]]; then + filename=$(basename "$rmp_file") + if upload_file "$rmp_file" \ + ".config/retroarch/${filename}" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + fi + fi + done + ;; + retroarch-config) + if [[ -d "${TMP_DIR}/.config/retroarch/config/" ]]; then + if upload_dir "${TMP_DIR}/.config/retroarch/config/" \ + ".config/retroarch/config/" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + log WARN "Error restoring config/" + fi + fi + ;; + shaders) + if [[ -d "${TMP_DIR}/shaders/" ]]; then + if upload_dir "${TMP_DIR}/shaders/" \ + "shaders/" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + log WARN "Error restoring shaders/" + fi + fi + ;; + ppsspp) + if [[ -f "${TMP_DIR}/.config/ppsspp/PSP/SYSTEM/ppsspp.ini" ]]; then + upload_file "${TMP_DIR}/.config/ppsspp/PSP/SYSTEM/ppsspp.ini" \ + ".config/ppsspp/PSP/SYSTEM/ppsspp.ini" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null && RESTORED=$((RESTORED + 1)) || FAILED=$((FAILED + 1)) + fi + if [[ -f "${TMP_DIR}/.config/ppsspp/PSP/SYSTEM/controls.ini" ]]; then + upload_file "${TMP_DIR}/.config/ppsspp/PSP/SYSTEM/controls.ini" \ + ".config/ppsspp/PSP/SYSTEM/controls.ini" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null && RESTORED=$((RESTORED + 1)) || FAILED=$((FAILED + 1)) + fi + ;; + drastic) + if [[ -d "${TMP_DIR}/.config/drastic/config/" ]]; then + if upload_dir "${TMP_DIR}/.config/drastic/config/" \ + ".config/drastic/config/" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" &>/dev/null; then + RESTORED=$((RESTORED + 1)) + else + FAILED=$((FAILED + 1)) + log WARN "Error restoring drastic/config/" + fi + fi + ;; + other-emu) + for emu_path in ".config/game/configs" ".config/flycast" ".config/duckstation" ".config/yabasanshiro"; do + if [[ -e "${TMP_DIR}/${emu_path}" ]]; then + upload_dir "${TMP_DIR}/${emu_path}/" "${emu_path}/" \ + "$ROCKNIX_HOST" "$ROCKNIX_USER" 2>/dev/null && RESTORED=$((RESTORED + 1)) || FAILED=$((FAILED + 1)) + fi + done + ;; + esac +done + +# Clean up temporary safety backup +cleanup_tmp "${SAFETY_DIR}" 2>/dev/null || true + +# Summary +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Restore completed${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo -e " Device: ${CYAN}${DEVICE}${NC}" +echo -e " Restored: ${CYAN}${RESTORED}${NC}" +if [[ $FAILED -gt 0 ]]; then + echo -e " Failed: ${RED}${FAILED}${NC}" +fi +echo "" +echo -e " ${YELLOW}Note: Restart the console to apply changes${NC}" +echo ""