diff --git a/immich/restore-immich.sh b/immich/restore-immich.sh index 92d0402..dc364da 100755 --- a/immich/restore-immich.sh +++ b/immich/restore-immich.sh @@ -208,6 +208,20 @@ validate_backup_files() { fi fi + # Check available disk space for database restoration + local db_size_bytes=$(stat -c%s "$DB_BACKUP" 2>/dev/null || echo "0") + local available_space=$(df /tmp | tail -1 | awk '{print $4}') + available_space=$((available_space * 1024)) # Convert to bytes + + # Estimate uncompressed size (conservative: 5x compressed size) + local estimated_uncompressed=$((db_size_bytes * 5)) + + if [ $estimated_uncompressed -gt $available_space ]; then + log_message "Error: Insufficient disk space for database restoration" + log_message "Estimated space needed: $(( estimated_uncompressed / 1024 / 1024 ))MB, Available: $(( available_space / 1024 / 1024 ))MB" + exit 1 + fi + local db_size=$(du -sh "$DB_BACKUP" 2>/dev/null | cut -f1 || echo "unknown") log_message "✓ Database backup validated: $DB_BACKUP ($db_size)" fi @@ -224,6 +238,20 @@ validate_backup_files() { exit 1 fi + # Check available disk space for uploads restoration + local uploads_size_bytes=$(stat -c%s "$UPLOADS_BACKUP" 2>/dev/null || echo "0") + local upload_dir_space=$(df "$(dirname "$UPLOAD_LOCATION")" | tail -1 | awk '{print $4}') + upload_dir_space=$((upload_dir_space * 1024)) # Convert to bytes + + # Estimate uncompressed size (conservative: 2x compressed size for media files) + local estimated_uploads_uncompressed=$((uploads_size_bytes * 2)) + + if [ $estimated_uploads_uncompressed -gt $upload_dir_space ]; then + log_message "Error: Insufficient disk space for uploads restoration" + log_message "Estimated space needed: $(( estimated_uploads_uncompressed / 1024 / 1024 ))MB, Available: $(( upload_dir_space / 1024 / 1024 ))MB" + exit 1 + fi + local uploads_size=$(du -sh "$UPLOADS_BACKUP" 2>/dev/null | cut -f1 || echo "unknown") log_message "✓ Uploads backup validated: $UPLOADS_BACKUP ($uploads_size)" fi @@ -272,7 +300,21 @@ manage_containers() { # Wait for postgres to be ready log_message "Waiting for PostgreSQL to be ready..." - sleep 10 + local max_attempts=15 + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if docker exec immich_postgres pg_isready -U "$DB_USERNAME" >/dev/null 2>&1; then + log_message "✓ PostgreSQL is ready" + break + fi + attempt=$((attempt + 1)) + sleep 2 + done + + if [ $attempt -eq $max_attempts ]; then + log_message "Warning: PostgreSQL may not be fully ready" + fi # Start immich_server docker start immich_server || { @@ -327,24 +369,32 @@ restore_database() { while [ $attempt -lt $max_attempts ]; do if docker exec immich_postgres pg_isready -U "$DB_USERNAME" >/dev/null 2>&1; then + log_message "✓ PostgreSQL is ready (attempt $((attempt + 1)))" break fi attempt=$((attempt + 1)) + log_message "Waiting for PostgreSQL... (attempt $attempt/$max_attempts)" sleep 2 done if [ $attempt -eq $max_attempts ]; then - log_message "Error: PostgreSQL did not become ready within timeout" + log_message "Error: PostgreSQL did not become ready within timeout (${max_attempts} attempts)" rm -rf "$temp_dir" exit 1 fi - log_message "✓ PostgreSQL is ready" - # Drop existing database and recreate (if it exists) log_message "Preparing database for restoration..." - docker exec immich_postgres psql -U "$DB_USERNAME" -c "DROP DATABASE IF EXISTS $DB_DATABASE_NAME;" 2>/dev/null || true - docker exec immich_postgres psql -U "$DB_USERNAME" -c "CREATE DATABASE $DB_DATABASE_NAME;" || { + + # Validate database and username names to prevent injection + if [[ ! "$DB_DATABASE_NAME" =~ ^[a-zA-Z0-9_]+$ ]] || [[ ! "$DB_USERNAME" =~ ^[a-zA-Z0-9_]+$ ]]; then + log_message "Error: Invalid characters in database name or username" + rm -rf "$temp_dir" + exit 1 + fi + + docker exec immich_postgres psql -U "$DB_USERNAME" -c "DROP DATABASE IF EXISTS \"$DB_DATABASE_NAME\";" 2>/dev/null || true + docker exec immich_postgres psql -U "$DB_USERNAME" -c "CREATE DATABASE \"$DB_DATABASE_NAME\";" || { log_message "Error: Failed to create database $DB_DATABASE_NAME" rm -rf "$temp_dir" exit 1 @@ -401,18 +451,51 @@ restore_uploads() { mkdir -p "$UPLOAD_LOCATION" fi - # Extract uploads backup + # Extract uploads backup with security safeguards log_message "Extracting uploads backup..." log_message "This may take a while depending on the size of your media library..." - # Extract to parent directory, then move content to correct location + # Extract to parent directory with security options local parent_dir=$(dirname "$UPLOAD_LOCATION") local upload_dirname=$(basename "$UPLOAD_LOCATION") - if tar -xzf "$UPLOADS_BACKUP" -C "$parent_dir"; then + # Create a secure temporary extraction directory + local temp_extract_dir=$(mktemp -d) + + # Extract with safety options to prevent path traversal + if tar --no-absolute-names --strip-components=0 -xzf "$UPLOADS_BACKUP" -C "$temp_extract_dir"; then + # Move extracted content to final location + if [ -d "$UPLOAD_LOCATION" ]; then + log_message "Backup existing upload directory..." + mv "$UPLOAD_LOCATION" "${UPLOAD_LOCATION}.backup.$(date +%s)" || { + log_message "Error: Failed to backup existing upload directory" + rm -rf "$temp_extract_dir" + exit 1 + } + fi + + # Move extracted content to final location + mv "$temp_extract_dir"/* "$UPLOAD_LOCATION" 2>/dev/null || { + # Handle case where extraction created nested directories + local extracted_dir=$(find "$temp_extract_dir" -mindepth 1 -maxdepth 1 -type d | head -1) + if [ -n "$extracted_dir" ]; then + mv "$extracted_dir" "$UPLOAD_LOCATION" || { + log_message "Error: Failed to move extracted files to upload location" + rm -rf "$temp_extract_dir" + exit 1 + } + else + log_message "Error: No content found in uploads backup" + rm -rf "$temp_extract_dir" + exit 1 + fi + } + + rm -rf "$temp_extract_dir" log_message "✅ Uploads restoration completed successfully" else log_message "Error: Failed to extract uploads backup" + rm -rf "$temp_extract_dir" exit 1 fi @@ -490,11 +573,15 @@ verify_restoration() { # Initialize variables CONTAINERS_STOPPED=false +RESTORE_START_TIME=$(date +%s) # Main restoration logic log_message "=== IMMICH RESTORATION STARTED ===" send_notification "🔄 Immich Restore Started" "Beginning restoration from backup files" "info" +# Validate backup files first (even in dry-run mode for safety) +validate_backup_files + if [ "$DRY_RUN" = true ]; then log_message "=== DRY RUN MODE - NO ACTUAL RESTORATION WILL BE PERFORMED ===" log_message "" @@ -515,9 +602,6 @@ if [ "$DRY_RUN" = true ]; then exit 0 fi -# Validate backup files -validate_backup_files - # Stop containers manage_containers stop @@ -530,7 +614,20 @@ verify_restoration # Calculate restoration time and send success notification RESTORE_END_TIME=$(date +%s) -TOTAL_RESTORE_TIME=$((RESTORE_END_TIME - $(date +%s))) +TOTAL_RESTORE_TIME=$((RESTORE_END_TIME - RESTORE_START_TIME)) +RESTORE_TIME_FORMATTED="" + +if [ $TOTAL_RESTORE_TIME -gt 3600 ]; then + HOURS=$((TOTAL_RESTORE_TIME / 3600)) + MINUTES=$(((TOTAL_RESTORE_TIME % 3600) / 60)) + RESTORE_TIME_FORMATTED="${HOURS}h ${MINUTES}m" +elif [ $TOTAL_RESTORE_TIME -gt 60 ]; then + MINUTES=$((TOTAL_RESTORE_TIME / 60)) + SECONDS=$((TOTAL_RESTORE_TIME % 60)) + RESTORE_TIME_FORMATTED="${MINUTES}m ${SECONDS}s" +else + RESTORE_TIME_FORMATTED="${TOTAL_RESTORE_TIME}s" +fi # Format file information for notification NOTIFICATION_MESSAGE="" @@ -550,7 +647,7 @@ if [ "$SKIP_UPLOADS" = false ]; then fi fi -NOTIFICATION_MESSAGE="${NOTIFICATION_MESSAGE}\n✅ Restoration completed successfully" +NOTIFICATION_MESSAGE="${NOTIFICATION_MESSAGE}\n✅ Restoration completed successfully in ${RESTORE_TIME_FORMATTED}" NOTIFICATION_MESSAGE="${NOTIFICATION_MESSAGE}\n🏠 Restored to: $(hostname)" send_notification "✅ Immich Restore Completed" "$NOTIFICATION_MESSAGE" "success"