feat(update): preserve user directories during git reset

backup and restore user-customizable directories (like python-runner/)
during update to prevent data loss from git reset --hard

closes #22
This commit is contained in:
Yury Kossakovsky
2025-12-13 09:21:14 -07:00
parent 85d6730151
commit 7711fdbf89
2 changed files with 174 additions and 7 deletions

View File

@@ -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

View File

@@ -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 <backup_base_path>
# 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
}