|
|
#!/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 |