| | #!/bin/bash |
| |
|
| | set -euo pipefail |
| |
|
| | |
| | export TZ=UTC |
| |
|
| | |
| | DEFAULT_CONFIG_FILE="${CONFIG_FILE:-/home/user/config/persistence.conf}" |
| |
|
| | |
| | log() { |
| | local level="$1" |
| | shift |
| | |
| | mkdir -p "$(dirname "${LOG_FILE:-/home/user/log/persistence.log}")" |
| | echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "${LOG_FILE:-/home/user/log/persistence.log}" |
| | } |
| |
|
| | log_info() { log "INFO" "$@"; } |
| | log_warn() { log "WARN" "$@"; } |
| | log_error() { log "ERROR" "$@"; } |
| |
|
| | |
| | load_configuration() { |
| | local config_file="${1:-$DEFAULT_CONFIG_FILE}" |
| |
|
| | if [[ ! -f "$config_file" ]]; then |
| | log_warn "Configuration file does not exist: $config_file, using default configuration" |
| | return 0 |
| | fi |
| |
|
| | log_info "Loading configuration file: $config_file" |
| |
|
| | |
| | source "$config_file" |
| | } |
| |
|
| | |
| | set_default_configuration() { |
| | |
| | export HF_TOKEN="${HF_TOKEN:-}" |
| | export DATASET_ID="${DATASET_ID:-}" |
| | export ARCHIVE_PATHS="${ARCHIVE_PATHS:-}" |
| | export RESTORE_PATH="${RESTORE_PATH:-./}" |
| |
|
| | |
| | export SYNC_INTERVAL="${SYNC_INTERVAL:-}" |
| | export MAX_ARCHIVES="${MAX_ARCHIVES:-}" |
| | export COMPRESSION_LEVEL="${COMPRESSION_LEVEL:-}" |
| | export INITIAL_BACKUP_DELAY="${INITIAL_BACKUP_DELAY:-}" |
| |
|
| | |
| | export ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-}" |
| | export ARCHIVE_EXTENSION="${ARCHIVE_EXTENSION:-}" |
| | export EXCLUDE_PATTERNS="${EXCLUDE_PATTERNS:-}" |
| |
|
| | |
| | export APP_COMMAND="${APP_COMMAND:-}" |
| | export ENABLE_AUTO_RESTORE="${ENABLE_AUTO_RESTORE:-}" |
| | export ENABLE_AUTO_SYNC="${ENABLE_AUTO_SYNC:-}" |
| |
|
| | |
| | export FORCE_SYNC_RESTORE="${FORCE_SYNC_RESTORE:-}" |
| | export ENABLE_INTEGRITY_CHECK="${ENABLE_INTEGRITY_CHECK:-}" |
| |
|
| | |
| | export LOG_FILE="${LOG_FILE:-}" |
| | export LOG_LEVEL="${LOG_LEVEL:-}" |
| | } |
| |
|
| | |
| | validate_configuration() { |
| | local errors=0 |
| |
|
| | if [[ -z "$HF_TOKEN" ]]; then |
| | log_error "Missing required environment variable: HF_TOKEN" |
| | ((errors++)) |
| | fi |
| |
|
| | if [[ -z "$DATASET_ID" ]]; then |
| | log_error "Missing required environment variable: DATASET_ID" |
| | ((errors++)) |
| | fi |
| |
|
| | if [[ $errors -gt 0 ]]; then |
| | log_error "Configuration validation failed, starting application in non-persistent mode" |
| | return 1 |
| | fi |
| |
|
| | |
| | export HUGGING_FACE_HUB_TOKEN="$HF_TOKEN" |
| |
|
| | log_info "Configuration validation successful" |
| | return 0 |
| | } |
| |
|
| | |
| | create_archive() { |
| | local timestamp=$(date +%Y%m%d_%H%M%S) |
| | local archive_file="${ARCHIVE_PREFIX}_${timestamp}.${ARCHIVE_EXTENSION}" |
| | |
| | local temp_dir="/home/user/temp" |
| | mkdir -p "$temp_dir" |
| | local temp_archive="$temp_dir/${archive_file}" |
| |
|
| | log_info "Starting archive creation: $archive_file" >&2 |
| |
|
| | |
| | local exclude_args="" |
| | local default_excludes="__pycache__,*.tmp,*/temp,*/cache,*/.cache,*/log,*/logs" |
| | local combined_patterns="${EXCLUDE_PATTERNS:-},${default_excludes}" |
| |
|
| | if [[ -n "$combined_patterns" ]]; then |
| | IFS=',' read -ra patterns <<< "$combined_patterns" |
| | for pattern in "${patterns[@]}"; do |
| | pattern="${pattern// /}" |
| | if [[ -n "$pattern" ]]; then |
| | exclude_args+=" --exclude='${pattern}'" |
| | fi |
| | done |
| | fi |
| |
|
| | |
| | |
| | local tar_options="--ignore-failed-read --warning=no-file-changed --warning=no-file-removed --mtime='@$(date +%s)'" |
| |
|
| | |
| | local archive_paths_array |
| | IFS=',' read -ra archive_paths_array <<< "$ARCHIVE_PATHS" |
| |
|
| | local tar_cmd="tar -czf '$temp_archive' $tar_options $exclude_args" |
| | local valid_paths=() |
| |
|
| | for path in "${archive_paths_array[@]}"; do |
| | path="${path// /}" |
| | if [[ -e "$path" ]]; then |
| | |
| | if [[ -d "$path" ]] && [[ -z "$(ls -A "$path" 2>/dev/null)" ]]; then |
| | log_warn "Directory is empty, creating placeholder file: $path" >&2 |
| | echo "# Placeholder file for persistence backup" > "$path/.persistence_placeholder" |
| | fi |
| | tar_cmd+=" '$path'" |
| | valid_paths+=("$path") |
| | else |
| | log_warn "Archive path does not exist, skipping: $path" >&2 |
| | fi |
| | done |
| |
|
| | |
| | if [[ ${#valid_paths[@]} -eq 0 ]]; then |
| | log_error "No valid archive paths found" >&2 |
| | return 1 |
| | fi |
| |
|
| | log_info "Executing archive command: $tar_cmd" >&2 |
| |
|
| | |
| | local tar_exit_code=0 |
| | if eval "$tar_cmd" >&2; then |
| | tar_exit_code=$? |
| | else |
| | tar_exit_code=$? |
| | fi |
| |
|
| | |
| | |
| | if [[ $tar_exit_code -eq 0 || $tar_exit_code -eq 1 ]] && [[ -f "$temp_archive" ]]; then |
| | log_info "Archive file created successfully: $temp_archive" >&2 |
| | if [[ $tar_exit_code -eq 1 ]]; then |
| | log_warn "Some files changed during archiving (this is normal for active applications)" >&2 |
| | fi |
| | echo "$temp_archive" |
| | return 0 |
| | else |
| | log_error "Archive file creation failed with exit code: $tar_exit_code" >&2 |
| | return 1 |
| | fi |
| | } |
| |
|
| | |
| | run_upload_handler() { |
| | local archive_file="$1" |
| | local filename="$2" |
| | local dataset_id="$3" |
| | local backup_prefix="$4" |
| | local backup_extension="$5" |
| | local max_backups="$6" |
| | local token="$7" |
| |
|
| | |
| | local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| | |
| | |
| | python3 "${script_dir}/hf_persistence.py" upload \ |
| | --token "$token" \ |
| | --dataset-id "$dataset_id" \ |
| | --archive-file "$archive_file" \ |
| | --filename "$filename" \ |
| | --archive-prefix "$backup_prefix" \ |
| | --archive-extension "$backup_extension" \ |
| | --max-archives "$max_backups" |
| | } |
| |
|
| | |
| | upload_archive() { |
| | local archive_file="$1" |
| | local filename=$(basename "$archive_file") |
| |
|
| | log_info "Starting archive upload: $filename" |
| |
|
| | |
| | if run_upload_handler "$archive_file" "$filename" "$DATASET_ID" "$ARCHIVE_PREFIX" "$ARCHIVE_EXTENSION" "$MAX_ARCHIVES" "$HF_TOKEN"; then |
| | log_info "Archive upload completed" |
| | return 0 |
| | else |
| | log_error "Archive upload failed" |
| | return 1 |
| | fi |
| | } |
| |
|
| | |
| | perform_archive() { |
| | log_info "Starting archive operation" |
| |
|
| | local archive_file |
| | if archive_file=$(create_archive); then |
| | |
| | if [[ "$HF_TOKEN" == "test_token" ]]; then |
| | log_info "Test mode: Archive created successfully, skipping upload" |
| | log_info "Archive file: $archive_file" |
| | ls -la "$archive_file" |
| | log_info "Test mode: Keeping archive file for inspection" |
| | else |
| | if upload_archive "$archive_file"; then |
| | log_info "Archive operation completed successfully" |
| | else |
| | log_error "Archive upload failed" |
| | fi |
| | |
| | rm -f "$archive_file" |
| | fi |
| | else |
| | log_error "Archive creation failed" |
| | return 1 |
| | fi |
| | } |
| |
|
| | |
| | sync_daemon() { |
| | log_info "Starting sync daemon, interval: ${SYNC_INTERVAL} seconds" |
| |
|
| | |
| | local initial_delay="${INITIAL_BACKUP_DELAY:-300}" |
| | log_info "Waiting ${initial_delay} seconds for application to fully initialize before first backup" |
| | sleep "$initial_delay" |
| |
|
| | while true; do |
| | perform_archive |
| |
|
| | log_info "Next sync will execute in ${SYNC_INTERVAL} seconds" |
| | sleep "$SYNC_INTERVAL" |
| | done |
| | } |
| |
|
| | |
| | run_archive_lister() { |
| | local dataset_id="$1" |
| | local backup_prefix="$2" |
| | local backup_extension="$3" |
| | local token="$4" |
| |
|
| | |
| | local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| | |
| | |
| | python3 "${script_dir}/hf_persistence.py" list \ |
| | --token "$token" \ |
| | --dataset-id "$dataset_id" \ |
| | --archive-prefix "$backup_prefix" \ |
| | --archive-extension "$backup_extension" |
| | } |
| |
|
| | |
| | list_archives() { |
| | log_info "Getting available archive list" |
| |
|
| | |
| | if [[ "$HF_TOKEN" == "test_token" ]]; then |
| | log_info "Test mode: Simulating empty archive list" |
| | echo "No archive files found" |
| | return 0 |
| | fi |
| |
|
| | |
| | run_archive_lister "$DATASET_ID" "$ARCHIVE_PREFIX" "$ARCHIVE_EXTENSION" "$HF_TOKEN" |
| | } |
| |
|
| | |
| | run_download_handler() { |
| | local backup_name="$1" |
| | local dataset_id="$2" |
| | local restore_path="$3" |
| | local token="$4" |
| |
|
| | |
| | local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| | |
| | |
| | python3 "${script_dir}/hf_persistence.py" restore \ |
| | --token "$token" \ |
| | --dataset-id "$dataset_id" \ |
| | --archive-name "$backup_name" \ |
| | --restore-path "$restore_path" |
| | } |
| |
|
| | |
| | verify_data_integrity() { |
| | |
| | if [[ "$ENABLE_INTEGRITY_CHECK" != "true" ]]; then |
| | log_info "Data integrity verification is disabled, skipping" |
| | return 0 |
| | fi |
| |
|
| | local verification_failed=0 |
| |
|
| | log_info "Starting data integrity verification" |
| |
|
| | |
| | IFS=',' read -ra archive_paths_array <<< "$ARCHIVE_PATHS" |
| | for path in "${archive_paths_array[@]}"; do |
| | path="${path// /}" |
| | if [[ -n "$path" ]]; then |
| | if [[ -e "$path" ]]; then |
| | log_info "✓ Verified path exists: $path" |
| | |
| | if [[ -d "$path" ]] && [[ ! -r "$path" ]]; then |
| | log_error "✗ Directory exists but is not readable: $path" |
| | ((verification_failed++)) |
| | fi |
| | |
| | if [[ -d "$path" ]] && [[ ! -w "$path" ]]; then |
| | log_error "✗ Directory exists but is not writable: $path" |
| | ((verification_failed++)) |
| | fi |
| | else |
| | log_warn "⚠ Path does not exist after restoration: $path" |
| | |
| | fi |
| | fi |
| | done |
| |
|
| | |
| | |
| |
|
| | if [[ $verification_failed -gt 0 ]]; then |
| | log_error "Data integrity verification failed with $verification_failed errors" |
| | return 1 |
| | else |
| | log_info "✓ Data integrity verification passed" |
| | return 0 |
| | fi |
| | } |
| |
|
| | |
| | restore_archive() { |
| | local archive_name="${1:-latest}" |
| | local force_restore="${2:-false}" |
| |
|
| | log_info "Starting synchronous archive restoration: $archive_name" |
| |
|
| | |
| | if [[ "$archive_name" == "latest" ]]; then |
| | local archive_list_output |
| | if archive_list_output=$(list_archives 2>&1); then |
| | archive_name=$(echo "$archive_list_output" | grep "LATEST_BACKUP:" | cut -d: -f2) |
| | if [[ -z "$archive_name" ]]; then |
| | log_info "No archive files found, this appears to be the first run" |
| | if [[ "$force_restore" == "true" ]]; then |
| | log_info "Force restore requested but no archives available - this is normal for first run" |
| | log_info "Initializing fresh environment for first-time startup" |
| |
|
| | |
| | IFS=',' read -ra archive_paths_array <<< "$ARCHIVE_PATHS" |
| | for path in "${archive_paths_array[@]}"; do |
| | path="${path// /}" |
| | if [[ -n "$path" ]] && [[ ! -e "$path" ]]; then |
| | log_info "Creating directory for first run: $path" |
| | mkdir -p "$path" || log_warn "Failed to create directory: $path" |
| | fi |
| | done |
| |
|
| | log_info "✓ First-time environment initialization completed" |
| | return 0 |
| | else |
| | log_info "Continuing with fresh start (no archives available)" |
| | return 0 |
| | fi |
| | fi |
| | else |
| | |
| | if echo "$archive_list_output" | grep -q "No archive files found"; then |
| | log_info "No archive files found, this appears to be the first run" |
| | if [[ "$force_restore" == "true" ]]; then |
| | log_info "Force restore requested but no archives available - this is normal for first run" |
| | log_info "Initializing fresh environment for first-time startup" |
| |
|
| | |
| | IFS=',' read -ra archive_paths_array <<< "$ARCHIVE_PATHS" |
| | for path in "${archive_paths_array[@]}"; do |
| | path="${path// /}" |
| | if [[ -n "$path" ]] && [[ ! -e "$path" ]]; then |
| | log_info "Creating directory for first run: $path" |
| | mkdir -p "$path" || log_warn "Failed to create directory: $path" |
| | fi |
| | done |
| |
|
| | log_info "✓ First-time environment initialization completed" |
| | return 0 |
| | else |
| | log_info "Continuing with fresh start (no archives available)" |
| | return 0 |
| | fi |
| | else |
| | log_error "Failed to get archive list: $archive_list_output" |
| | return 1 |
| | fi |
| | fi |
| | fi |
| |
|
| | log_info "Restoring archive file: $archive_name" |
| |
|
| | |
| | if run_download_handler "$archive_name" "$DATASET_ID" "$RESTORE_PATH" "$HF_TOKEN"; then |
| | log_info "Archive extraction completed, verifying data integrity..." |
| |
|
| | |
| | if verify_data_integrity; then |
| | log_info "✓ Archive restoration completed successfully with integrity verification" |
| | return 0 |
| | else |
| | log_error "✗ Data integrity verification failed after restoration" |
| | return 1 |
| | fi |
| | else |
| | log_error "✗ Archive restoration failed during extraction" |
| | return 1 |
| | fi |
| | } |
| |
|
| | |
| | main() { |
| | local command="start" |
| | local config_file="$DEFAULT_CONFIG_FILE" |
| | local verbose=false |
| | local no_restore=false |
| | local no_sync=false |
| | local restore_target="" |
| |
|
| | |
| | while [[ $# -gt 0 ]]; do |
| | case $1 in |
| | -c|--config) |
| | config_file="$2" |
| | shift 2 |
| | ;; |
| | -v|--verbose) |
| | verbose=true |
| | shift |
| | ;; |
| | --no-restore) |
| | no_restore=true |
| | shift |
| | ;; |
| | --no-sync) |
| | no_sync=true |
| | shift |
| | ;; |
| | archive|restore|restore-sync|list|daemon|start) |
| | command="$1" |
| | shift |
| | ;; |
| | *) |
| | if [[ ("$command" == "restore" || "$command" == "restore-sync") && -z "$restore_target" ]]; then |
| | |
| | restore_target="$1" |
| | shift |
| | else |
| | log_error "Unknown parameter: $1" |
| | exit 1 |
| | fi |
| | ;; |
| | esac |
| | done |
| |
|
| | |
| | load_configuration "$config_file" |
| | set_default_configuration |
| |
|
| | |
| | if [[ "$verbose" == "true" ]]; then |
| | export LOG_LEVEL="DEBUG" |
| | fi |
| |
|
| | log_info "=== Data Persistence Single File Script Startup ===" |
| | log_info "Version: 4.0" |
| | log_info "Command: $command" |
| | log_info "Configuration file: $config_file" |
| |
|
| | |
| | case $command in |
| | archive) |
| | if validate_configuration; then |
| | perform_archive |
| | else |
| | exit 1 |
| | fi |
| | ;; |
| | restore) |
| | if validate_configuration; then |
| | restore_archive "${restore_target:-latest}" |
| | else |
| | exit 1 |
| | fi |
| | ;; |
| | restore-sync) |
| | |
| | if validate_configuration; then |
| | log_info "=== SYNCHRONOUS RESTORE MODE ===" |
| | log_info "This is a blocking operation that must complete successfully" |
| |
|
| | if restore_archive "${restore_target:-latest}" "true"; then |
| | log_info "✓ Synchronous restore completed successfully" |
| | exit 0 |
| | else |
| | log_error "✗ Synchronous restore failed" |
| | log_error "Operation aborted to prevent data inconsistency" |
| | exit 1 |
| | fi |
| | else |
| | log_error "Configuration validation failed" |
| | exit 1 |
| | fi |
| | ;; |
| | list) |
| | if validate_configuration; then |
| | list_archives |
| | else |
| | exit 1 |
| | fi |
| | ;; |
| | daemon) |
| | if validate_configuration; then |
| | sync_daemon |
| | else |
| | exit 1 |
| | fi |
| | ;; |
| | start) |
| | |
| | if validate_configuration; then |
| | |
| | if [[ "$ENABLE_AUTO_RESTORE" == "true" && "$no_restore" == "false" ]]; then |
| | log_info "=== SYNCHRONOUS DATA RESTORATION PHASE ===" |
| | log_info "Performing synchronous auto restore - this is a blocking operation" |
| | log_info "Force sync restore: $FORCE_SYNC_RESTORE" |
| | log_info "Integrity check: $ENABLE_INTEGRITY_CHECK" |
| |
|
| | if restore_archive "latest"; then |
| | log_info "✓ Synchronous data restoration completed successfully" |
| | log_info "All dependent services can now start safely" |
| | else |
| | if [[ "$FORCE_SYNC_RESTORE" == "true" ]]; then |
| | log_error "✗ Synchronous data restoration failed" |
| | log_error "FORCE_SYNC_RESTORE=true: Service startup will be aborted to prevent data inconsistency" |
| | log_error "Please check the logs and fix any issues before restarting" |
| | exit 1 |
| | else |
| | log_warn "✗ Synchronous data restoration failed" |
| | log_warn "FORCE_SYNC_RESTORE=false: Continuing with service startup (legacy behavior)" |
| | log_warn "This may result in data inconsistency" |
| | fi |
| | fi |
| |
|
| | log_info "=== DATA RESTORATION PHASE COMPLETED ===" |
| | else |
| | log_info "Auto restore is disabled, skipping data restoration" |
| | fi |
| |
|
| | |
| | if [[ "$ENABLE_AUTO_SYNC" == "true" && "$no_sync" == "false" ]]; then |
| | log_info "Starting sync daemon (data restoration completed)" |
| | sync_daemon & |
| | sync_pid=$! |
| | log_info "Sync daemon PID: $sync_pid" |
| | fi |
| | else |
| | log_warn "Configuration validation failed, starting application in non-persistent mode" |
| | log_warn "No data restoration will be performed" |
| | fi |
| |
|
| | |
| | log_info "=== STARTING MAIN APPLICATION ===" |
| | log_info "All data restoration and verification completed" |
| | log_info "Starting main application: $APP_COMMAND" |
| | exec $APP_COMMAND |
| | ;; |
| | *) |
| | log_error "Unknown command: $command" |
| | exit 1 |
| | ;; |
| | esac |
| | } |
| |
|
| | |
| | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then |
| | main "$@" |
| | fi |