diff --git a/scripts/update.sh b/scripts/update.sh index 0348571..4292414 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -3,13 +3,18 @@ # update.sh - Main update orchestrator # ============================================================================= # Performs a full system and service update: -# 1. Pulls latest changes from the git repository (git reset --hard + pull) -# 2. Updates Ubuntu system packages (apt-get update && upgrade) -# 3. Delegates to apply_update.sh for service updates +# 1. Backs up user-customizable directories (e.g., python-runner/) +# 2. Pulls latest changes from the git repository (git reset --hard + pull) +# 3. Restores backed up directories to preserve user modifications +# 4. Updates Ubuntu system packages (apt-get update && upgrade) +# 5. Delegates to apply_update.sh for service updates # # This two-stage approach ensures apply_update.sh itself gets updated before # running, so new update logic is always applied. # +# Preserved directories: Defined in PRESERVE_DIRS array in utils.sh. +# These directories contain user-customizable content that survives git reset. +# # Usage: make update OR sudo bash scripts/update.sh # ============================================================================= @@ -19,6 +24,20 @@ set -e source "$(dirname "$0")/utils.sh" init_paths +# Global variable to track backup path for cleanup +BACKUP_PATH="" + +# Cleanup function for interrupted updates +cleanup_on_exit() { + local exit_code=$? + if [ -n "$BACKUP_PATH" ] && [ -d "$BACKUP_PATH" ]; then + log_warning "Cleaning up backup directory: $BACKUP_PATH" + rm -rf "$BACKUP_PATH" + fi + exit $exit_code +} +trap cleanup_on_exit INT TERM + # Path to the apply_update.sh script APPLY_UPDATE_SCRIPT="$SCRIPT_DIR/apply_update.sh" @@ -44,10 +63,40 @@ if ! command -v git &> /dev/null; then else # Change to project root for git pull cd "$PROJECT_ROOT" || { log_error "Failed to change directory to $PROJECT_ROOT"; exit 1; } - git reset --hard HEAD || { log_warning "Failed to reset repository. Continuing update with potentially unreset local changes..."; } - git pull || { log_warning "Failed to pull latest repository changes. Continuing update with potentially old version of apply_update.sh..."; } - # Change back to script dir or ensure apply_update.sh uses absolute paths or cd's itself - # (apply_update.sh already handles cd to PROJECT_ROOT, so we're good) + + # Backup user-customizable directories before git reset (uses PRESERVE_DIRS from utils.sh) + if ! BACKUP_PATH=$(backup_preserved_dirs); then + log_error "Backup failed. Aborting update to prevent data loss." + exit 1 + fi + + if [ -n "$BACKUP_PATH" ]; then + log_info "Backup created at: $BACKUP_PATH" + fi + + # Git operations + if ! git reset --hard HEAD; then + log_error "Git reset failed." + restore_preserved_dirs "$BACKUP_PATH" + exit 1 + fi + + if ! git pull; then + log_error "Git pull failed." + restore_preserved_dirs "$BACKUP_PATH" + exit 1 + fi + + # Restore user-customizable directories after git pull + if ! restore_preserved_dirs "$BACKUP_PATH"; then + log_error "Failed to restore user directories from backup." + log_error "Backup may still be available at: $BACKUP_PATH" + BACKUP_PATH="" # Prevent cleanup from deleting it + exit 1 + fi + + # Clear backup path after successful restore + BACKUP_PATH="" fi # Update Ubuntu packages before running apply_update diff --git a/scripts/utils.sh b/scripts/utils.sh index af132aa..3ffa405 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -12,6 +12,7 @@ # - Validation helpers: require_command, require_file, ensure_file_exists # - Profile management: is_profile_active, update_compose_profiles # - Doctor output helpers: print_ok, print_warning, print_error +# - Directory preservation: backup_preserved_dirs, restore_preserved_dirs # # Usage: source "$(dirname "$0")/utils.sh" && init_paths # ============================================================================= @@ -603,3 +604,120 @@ wt_parse_choices() { local cleaned="${choices//\"/}" read -ra arr <<< "$cleaned" } + +#============================================================================= +# DIRECTORY PRESERVATION (for git updates) +#============================================================================= +# Directories containing user-customizable content that should survive git reset. +# Used by update.sh to backup before git operations and restore after. +PRESERVE_DIRS=("python-runner") + +# Backup preserved directories before git reset +# Usage: backup_path=$(backup_preserved_dirs) || exit 1 +# Returns: 0 on success (prints backup path), 1 on failure +# Default backup location: secure temp directory via mktemp +backup_preserved_dirs() { + local backup_base="" + local has_content=0 + + # Check if any directories need backup + for dir in "${PRESERVE_DIRS[@]}"; do + if [ -d "$PROJECT_ROOT/$dir" ] && [ -n "$(ls -A "$PROJECT_ROOT/$dir" 2>/dev/null)" ]; then + has_content=1 + break + fi + done + + # No content to backup + if [ $has_content -eq 0 ]; then + echo "" + return 0 + fi + + # Create secure temporary directory + backup_base=$(mktemp -d /tmp/n8n-install-backup.XXXXXXXXXX) || { + log_error "Failed to create backup directory" + return 1 + } + chmod 700 "$backup_base" + + # Backup each directory + for dir in "${PRESERVE_DIRS[@]}"; do + # Validate directory name (no path traversal) + if [[ "$dir" =~ \.\.|^/ ]]; then + log_error "Invalid directory name in PRESERVE_DIRS: $dir" + rm -rf "$backup_base" + return 1 + fi + + if [ -d "$PROJECT_ROOT/$dir" ] && [ -n "$(ls -A "$PROJECT_ROOT/$dir" 2>/dev/null)" ]; then + log_info "Backing up $dir/ before git reset..." + if ! cp -rp "$PROJECT_ROOT/$dir" "$backup_base/$dir"; then + log_error "Failed to backup $dir/. Aborting to prevent data loss." + rm -rf "$backup_base" + return 1 + fi + fi + done + + echo "$backup_base" + return 0 +} + +# Restore preserved directories after git pull +# Usage: restore_preserved_dirs +# Returns: 0 on success, 1 on failure +restore_preserved_dirs() { + local backup_base="$1" + + # Nothing to restore + if [ -z "$backup_base" ]; then + return 0 + fi + + if [ ! -d "$backup_base" ]; then + log_warning "Backup directory not found: $backup_base" + return 0 + fi + + # Safety checks for PROJECT_ROOT + if [ -z "$PROJECT_ROOT" ]; then + log_error "PROJECT_ROOT is not set. Refusing to restore." + return 1 + fi + + if [ "$PROJECT_ROOT" = "/" ] || [ "$PROJECT_ROOT" = "/root" ] || [ "$PROJECT_ROOT" = "/home" ]; then + log_error "PROJECT_ROOT is set to a dangerous path: $PROJECT_ROOT" + return 1 + fi + + for dir in "${PRESERVE_DIRS[@]}"; do + # Validate directory name + if [[ "$dir" =~ \.\.|^/ ]] || [ -z "$dir" ]; then + log_error "Invalid directory name: $dir" + continue + fi + + if [ -d "$backup_base/$dir" ]; then + log_info "Restoring $dir/ after git pull..." + + # Remove the git-restored version + if [ -d "$PROJECT_ROOT/$dir" ]; then + if ! rm -rf "$PROJECT_ROOT/$dir"; then + log_error "Failed to remove $PROJECT_ROOT/$dir" + return 1 + fi + fi + + # Restore from backup + if ! mv "$backup_base/$dir" "$PROJECT_ROOT/$dir"; then + log_error "Failed to restore $dir/ from backup" + return 1 + fi + fi + done + + # Cleanup backup directory + rm -rf "$backup_base" + return 0 +}