Files
patch-system-tools/patch_applier.sh

1063 lines
28 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# patch_applier.sh - 企业级补丁包应用脚本
set -euo pipefail
shopt -s nullglob extglob
# 脚本目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh"
LOG_FILE="/var/log/patch_system/apply_$(date +%Y%m%d_%H%M%S).log"
LOCK_FILE="/tmp/patch_apply.lock"
ROLLBACK_INFO_DIR="/var/lib/patch_system/rollback_info"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
"INFO") color="$GREEN" ;;
"WARN") color="$YELLOW" ;;
"ERROR") color="$RED" ;;
"DEBUG") color="$BLUE" ;;
*) color="$NC" ;;
esac
echo -e "${color}[$timestamp] [$level]${NC} $message" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$1"; }
warn() { log "WARN" "$1"; }
error() { log "ERROR" "$1"; }
debug() { log "DEBUG" "$1"; }
# 配置和初始化
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
error "配置文件不存在: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
info "配置文件加载完成"
# 设置默认值
: ${LOG_LEVEL:="INFO"}
: ${BACKUP_DIR:="/var/backups/patch"}
: ${TEMP_DIR:="/tmp/patch_apply_$$"}
: ${ROLLBACK_ENABLED:="true"}
: ${ROLLBACK_INFO_DIR:="/var/lib/patch_system/rollback_info"}
: ${VALIDATION_ENABLED:="true"}
: ${NOTIFICATIONS_ENABLED:="true"}
: ${ATOMIC_OPERATIONS:="true"}
: ${MAX_BACKUP_COUNT:=10}
: ${PATCH_STAGING_DIR:="/tmp/patch_staging"}
}
setup_environment() {
mkdir -p "$(dirname "$LOG_FILE")"
mkdir -p "$BACKUP_DIR"
mkdir -p "$TEMP_DIR"
mkdir -p "$ROLLBACK_INFO_DIR"
mkdir -p "$PATCH_STAGING_DIR"
info "环境设置完成"
info "日志文件: $LOG_FILE"
info "备份目录: $BACKUP_DIR"
info "临时目录: $TEMP_DIR"
}
# 锁管理
acquire_lock() {
local max_retries=5
local retry_count=0
while [[ $retry_count -lt $max_retries ]]; do
if (set -C; echo $$ > "$LOCK_FILE") 2>/dev/null; then
info "🔒 获取应用锁"
return 0
fi
local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
if kill -0 "$lock_pid" 2>/dev/null; then
warn "补丁应用正在进行中 (PID: $lock_pid),等待..."
sleep 5
((retry_count++))
else
warn "清理过期的锁文件"
rm -f "$LOCK_FILE"
fi
done
error "无法获取锁,可能已有补丁应用在进行中"
exit 1
}
release_lock() {
rm -f "$LOCK_FILE"
info "🔓 释放应用锁"
}
cleanup() {
local exit_code=$?
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
info "临时目录已清理: $TEMP_DIR"
fi
release_lock
if [[ $exit_code -eq 0 ]]; then
info "✅ 补丁应用流程完成"
else
error "💥 补丁应用流程失败 (退出码: $exit_code)"
fi
exit $exit_code
}
trap cleanup EXIT INT TERM
# 依赖检查
check_dependencies() {
local deps=("tar" "gzip" "find" "stat" "sha256sum" "date" "mkdir" "cp" "jq")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" >/dev/null 2>&1; then
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
error "缺少依赖工具: ${missing[*]}"
return 1
fi
info "依赖检查通过"
return 0
}
# 补丁包验证
verify_patch_package() {
local patch_file="$1"
info "🔍 验证补丁包: $patch_file"
# 检查文件存在性
if [[ ! -f "$patch_file" ]]; then
error "补丁包不存在: $patch_file"
return 1
fi
# 检查文件大小
local size=$(stat -c%s "$patch_file" 2>/dev/null || echo "0")
if [[ $size -eq 0 ]]; then
error "补丁包为空: $patch_file"
return 1
fi
# 验证压缩包完整性
if ! tar -tzf "$patch_file" >/dev/null 2>&1; then
error "补丁包损坏或格式错误: $patch_file"
return 1
fi
# 验证校验和(如果存在)
local checksum_file="${patch_file}.sha256"
if [[ -f "$checksum_file" ]]; then
if sha256sum -c "$checksum_file" >/dev/null 2>&1; then
info "✅ 校验和验证通过"
else
error "❌ 校验和验证失败"
return 1
fi
fi
# 验证签名(如果存在)
local sig_file="${patch_file}.sig"
if [[ -f "$sig_file" ]] && command -v gpg >/dev/null 2>&1; then
if gpg --verify "$sig_file" "$patch_file" >/dev/null 2>&1; then
info "✅ 签名验证通过"
else
error "❌ 签名验证失败"
return 1
fi
fi
info "✅ 补丁包验证通过"
return 0
}
# 提取补丁包
extract_patch() {
local patch_file="$1"
local extract_dir="$2"
info "📦 提取补丁包到: $extract_dir"
if [[ ! -d "$extract_dir" ]]; then
mkdir -p "$extract_dir"
fi
if tar -xzf "$patch_file" -C "$extract_dir"; then
info "✅ 补丁包提取完成"
# 验证提取内容
if [[ -f "$extract_dir/MANIFEST.MF" ]]; then
info "✅ 清单文件验证通过"
return 0
else
error "❌ 补丁包缺少清单文件"
return 1
fi
else
error "❌ 补丁包提取失败"
return 1
fi
}
# 解析清单文件
parse_manifest() {
local manifest_file="$1"
info "📋 解析清单文件: $manifest_file" >&2
if [[ ! -f "$manifest_file" ]]; then
error "清单文件不存在: $manifest_file"
return 1
fi
# 读取基础信息
local patch_name=$(grep -i "名称:" "$manifest_file" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
local patch_version=$(grep -i "版本:" "$manifest_file" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
local change_count=$(grep -c -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file" 2>/dev/null || echo "0")
echo "补丁信息:"
echo " 名称: ${patch_name:-未知}"
echo " 版本: ${patch_version:-未知}"
echo " 变更数: $change_count"
echo ""
# 提取变更列表
local changes=()
while IFS='|' read -r change_type path extra_info; do
changes+=("$change_type|$path|$extra_info")
done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file")
if [[ ${#changes[@]} -eq 0 ]]; then
warn "⚠️ 清单中没有找到变更记录" >&2
fi
# 返回变更数组
printf "%s\n" "${changes[@]}"
return 0
}
# 创建备份
create_backup() {
local changes_file="$1"
local backup_dir="$2"
info "💾 创建应用前备份..." >&2
if [[ ! -s "$changes_file" ]]; then
warn "无变更需要备份" >&2
return 0
fi
local files_to_backup=()
# 分析需要备份的文件
while IFS='|' read -r change_type path extra_info; do
case "$change_type" in
"MODIFIED"|"DELETED")
local target_file="$path"
if [[ -f "$target_file" ]]; then
files_to_backup+=("$target_file")
debug "需要备份: $target_file" >&2
fi
;;
esac
done < "$changes_file"
if [[ ${#files_to_backup[@]} -eq 0 ]]; then
info "⚠️ 无文件需要备份" >&2
return 0
fi
# 创建备份
local backup_tar="$backup_dir/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
mkdir -p "$(dirname "$backup_tar")"
local work_dir="$(pwd)" # 默认是当前工作目录
# 创建临时目录用于快速构建压缩包
local temp_dir="/tmp/backup_fast_$$"
mkdir -p "$temp_dir/contents"
# 快速复制文件到临时目录(使用 cp --parents 保持目录结构)
for file in "${files_to_backup[@]}"; do
if [[ -f "$file" ]]; then
cp --parents "$file" "$temp_dir/contents/" 2>/dev/null || \
cp "$file" "$temp_dir/contents/$(basename "$file")"
fi
done
# 复制 changes_file 为 changes.txt
if [[ -f "$changes_file" ]]; then
cp "$changes_file" "$temp_dir/changes.txt"
fi
# 从临时目录打包(最简单可靠)
if tar -czf "$backup_tar" -C "$temp_dir" .; then
local size=$(du -h "$backup_tar" | cut -f1)
info "✅ 备份创建完成: $backup_tar ($size)" >&2
# 清理临时目录
rm -rf "$temp_dir"
echo "$backup_tar"
return 0
else
error "❌ 备份创建失败" >&2
# 清理临时目录
rm -rf "$temp_dir"
return 1
fi
}
# 应用文件变更
apply_file_changes() {
local extract_dir="$1"
local changes_file="$2"
local dry_run="${3:-false}"
info "🔄 开始应用文件变更 (干跑模式: $dry_run)" >&2
local applied_count=0
local failed_count=0
local skip_count=0
# 读取变更列表
while IFS='|' read -r change_type path extra_info; do
case "$change_type" in
"ADDED")
apply_file_addition "$extract_dir" "$path" "$dry_run"
result=$?
;;
"MODIFIED")
apply_file_modification "$extract_dir" "$path" "$dry_run"
result=$?
;;
"DELETED")
apply_file_deletion "$path" "$dry_run"
result=$?
;;
*)
warn "⚠️ 未知变更类型: $change_type" >&2
result=99
;;
esac
case $result in
0) ((applied_count++)) ;;
1) ((failed_count++)) ;;
2) ((skip_count++)) ;;
esac
done < "$changes_file"
info "📊 变更应用统计:" >&2
info " 成功: $applied_count" >&2
info " 失败: $failed_count" >&2
info " 跳过: $skip_count" >&2
info " 总计: $((applied_count + failed_count + skip_count))" >&2
if [[ $failed_count -gt 0 ]]; then
error "❌ 存在应用失败的变更" >&2
return 1
fi
if [[ "$dry_run" == "true" ]]; then
info "✅ 干跑模式完成 - 无实际变更" >&2
return 0
else
info "✅ 文件变更应用完成" >&2
return 0
fi
}
# 应用文件新增
apply_file_addition() {
local extract_dir="$1"
local file_path="$2"
local dry_run="$3"
local source_file="$extract_dir/files/$file_path"
local target_file="$file_path"
local target_dir=$(dirname "$target_file")
# 检查源文件是否存在
if [[ ! -f "$source_file" ]]; then
error "源文件不存在: $source_file" >&2
return 1
fi
if [[ "$dry_run" == "true" ]]; then
info "📋 [干跑] 新增文件: $file_path" >&2
return 0
fi
# 创建目标目录
if [[ ! -d "$target_dir" ]]; then
if ! mkdir -p "$target_dir"; then
error "创建目录失败: $target_dir" >&2
return 1
fi
debug "创建目录: $target_dir" >&2
fi
# 检查目标文件是否已存在
if [[ -f "$target_file" ]]; then
warn "目标文件已存在,将被覆盖: $target_file" >&2
fi
# 复制文件
if cp "$source_file" "$target_file"; then
# 设置权限
set_file_permissions "$target_file" "644"
info "✅ 新增文件: $file_path" >&2
return 0
else
error "❌ 文件新增失败: $file_path" >&2
return 1
fi
}
# 应用文件修改
apply_file_modification() {
local extract_dir="$1"
local file_path="$2"
local dry_run="$3"
local source_file="$extract_dir/files/$file_path"
local target_file="$file_path"
info "🔄 开始应用文件修改: $file_path" >&2
info "当前工作目录: $(pwd)" >&2
info "源文件: $source_file" >&2
info "目标文件: $target_file" >&2
# 检查源文件
if [[ ! -f "$source_file" ]]; then
error "源文件不存在: $source_file" >&2
return 1
fi
# 检查目标文件
if [[ ! -f "$target_file" ]]; then
warn "目标文件不存在,将作为新增处理: $target_file" >&2
return apply_file_addition "$extract_dir" "$file_path" "$dry_run"
fi
if [[ "$dry_run" == "true" ]]; then
info "📋 [干跑] 修改文件: $file_path" >&2
return 0
fi
# 备份原文件
local backup_file="$TEMP_DIR/backup/$file_path"
local backup_dir=$(dirname "$backup_file")
mkdir -p "$backup_dir"
if ! cp -p "$target_file" "$backup_file"; then
error "备份原文件失败: $target_file" >&2
return 1
fi
# 应用修改
if cp "$source_file" "$target_file"; then
# 保持原权限
local original_perm=$(stat -c%a "$backup_file" 2>/dev/null || echo "644")
set_file_permissions "$target_file" "$original_perm"
info "✅ 修改文件: $file_path" >&2
return 0
else
error "❌ 文件修改失败: $file_path" >&2
# 恢复备份
cp "$backup_file" "$target_file"
return 1
fi
}
# 应用文件删除
apply_file_deletion() {
local file_path="$1"
local dry_run="$2"
local target_file="$file_path"
if [[ ! -f "$target_file" ]]; then
warn "文件不存在,无需删除: $target_file" >&2
return 2 # 跳过
fi
if [[ "$dry_run" == "true" ]]; then
info "📋 [干跑] 删除文件: $file_path" >&2
return 0
fi
# 备份文件
local backup_file="$TEMP_DIR/backup/$file_path"
local backup_dir=$(dirname "$backup_file")
mkdir -p "$backup_dir"
if ! cp -p "$target_file" "$backup_file"; then
error "备份文件失败: $target_file" >&2
return 1
fi
# 删除文件
if rm -f "$target_file"; then
info "✅ 删除文件: $file_path" >&2
# 尝试清理空目录
clean_empty_directories "$(dirname "$target_file")"
return 0
else
error "❌ 文件删除失败: $file_path" >&2
return 1
fi
}
# 清理空目录
clean_empty_directories() {
local dir="$1"
while [[ "$dir" != "/" ]] && [[ -d "$dir" ]]; do
if ! rmdir "$dir" 2>/dev/null; then
break # 目录非空,停止清理
fi
debug "清理空目录: $dir" >&2
dir=$(dirname "$dir")
done
}
# 设置文件权限
set_file_permissions() {
local file="$1"
local permissions="${2:-644}"
if chmod "$permissions" "$file" 2>/dev/null; then
debug "设置权限: $file$permissions"
else
warn "权限设置失败: $file"
fi
}
# 验证应用结果
verify_application() {
local extract_dir="$1"
local changes_file="$2"
if [[ "$VALIDATION_ENABLED" != "true" ]]; then
info "验证功能已禁用" >&2
return 0
fi
info "🔍 验证应用结果..." >&2
local manifest_file="$extract_dir/MANIFEST.MF"
local verification_passed=true
local verified_count=0
local failed_count=0
# 读取变更列表进行验证
while IFS='|' read -r change_type path extra_info; do
case "$change_type" in
"ADDED"|"MODIFIED")
if verify_file_application "$extract_dir" "$path" "$change_type"; then
((verified_count++))
else
((failed_count++))
verification_passed=false
fi
;;
"DELETED")
if verify_file_deletion "$path"; then
((verified_count++))
else
((failed_count++))
verification_passed=false
fi
;;
esac
done < "$changes_file"
info "📊 验证统计:" >&2
info " 成功: $verified_count" >&2
info " 失败: $failed_count" >&2
if $verification_passed; then
info "✅ 应用验证通过" >&2
return 0
else
error "❌ 应用验证失败" >&2
return 1
fi
}
# 验证文件应用
verify_file_application() {
local extract_dir="$1"
local file_path="$2"
local change_type="$3"
local source_file="$extract_dir/files/$file_path"
local target_file="$file_path"
# 检查目标文件是否存在
if [[ ! -f "$target_file" ]]; then
error "❌ 目标文件不存在: $target_file"
return 1
fi
# 比较文件内容
local source_hash=$(sha256sum "$source_file" | cut -d' ' -f1)
local target_hash=$(sha256sum "$target_file" | cut -d' ' -f1)
if [[ "$source_hash" != "$target_hash" ]]; then
error "❌ 文件内容不匹配: $file_path"
error " 源哈希: $source_hash"
error " 目标哈希: $target_hash"
return 1
fi
debug "✅ 文件验证通过: $file_path"
return 0
}
# 验证文件删除
verify_file_deletion() {
local file_path="$1"
local target_file="$file_path"
if [[ -f "$target_file" ]]; then
error "❌ 文件未成功删除: $target_file"
return 1
fi
debug "✅ 文件删除验证通过: $file_path"
return 0
}
# 创建回滚点
create_rollback_point() {
local patch_file="$1"
local backup_file="$2"
local extract_dir="$3"
local changes_file="$4"
if [[ "$ROLLBACK_ENABLED" != "true" ]]; then
info "回滚功能已禁用"
return 0
fi
info "🔄 创建回滚点..."
local patch_name=$(basename "$patch_file" .tar.gz)
local timestamp=$(date +%Y%m%d_%H%M%S)
local rollback_info_file="$ROLLBACK_INFO_DIR/${patch_name}_${timestamp}.json"
# 读取清单信息
local manifest_file="$extract_dir/MANIFEST.MF"
local patch_info=""
if [[ -f "$manifest_file" ]]; then
patch_info=$(grep -E "^(名称|版本|描述):" "$manifest_file" | head -3 | tr '\n' ';')
fi
# 统计变更类型
local added_count=$(grep -c "^ADDED" "$changes_file")
local modified_count=$(grep -c "^MODIFIED" "$changes_file")
local deleted_count=$(grep -c "^DELETED" "$changes_file")
# 创建回滚信息JSON
local rollback_info=$(cat << EOF
{
"patch_name": "$patch_name",
"apply_time": "$(date -Iseconds)",
"backup_path": "$backup_file",
"rollback_dir": "$BACKUP_DIR/rollback_$timestamp",
"manifest_info": "$patch_info",
"status": "applied",
"changes": {
"added": $added_count,
"modified": $modified_count,
"deleted": $deleted_count,
"total": $((added_count + modified_count + deleted_count))
},
"system_info": {
"hostname": "$(hostname)",
"user": "$(whoami)",
"working_directory": "$PWD"
}
}
EOF
)
echo "$rollback_info" > "$rollback_info_file"
info "✅ 回滚点创建完成: $rollback_info_file"
# 清理旧回滚点
cleanup_old_rollback_points
}
# 清理旧回滚点
cleanup_old_rollback_points() {
local max_points="${1:-$MAX_BACKUP_COUNT}"
info "🧹 清理旧回滚点 (保留最近 $max_points 个)"
if [[ ! -d "$ROLLBACK_INFO_DIR" ]]; then
return 0
fi
# 获取所有回滚点并按时间排序
local points=($(find "$ROLLBACK_INFO_DIR" -name "*.json" -type f -printf "%T@|%p\n" | sort -rn | cut -d'|' -f2))
local total_points=${#points[@]}
local points_to_remove=$((total_points - max_points))
if [[ $points_to_remove -le 0 ]]; then
debug "无需清理,当前 $total_points 个回滚点"
return 0
fi
info "清理 $points_to_remove 个旧回滚点"
for ((i=max_points; i<total_points; i++)); do
local old_file="${points[$i]}"
if [[ -f "$old_file" ]]; then
# 获取备份文件路径
local backup_path=$(jq -r '.backup_path // ""' "$old_file")
# 删除备份文件
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
rm -f "$backup_path"
debug "删除备份文件: $(basename "$backup_path")"
fi
# 删除回滚信息文件
rm -f "$old_file"
info "🗑️ 清理回滚点: $(basename "$old_file")"
fi
done
info "✅ 回滚点清理完成"
}
# 发送通知
send_notification() {
local status="$1"
local patch_file="$2"
local message="$3"
if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then
return 0
fi
local subject=""
local color=""
case "$status" in
"start")
subject="补丁应用开始"
color="#3498db"
;;
"success")
subject="补丁应用成功"
color="#2ecc71"
;;
"failure")
subject="补丁应用失败"
color="#e74c3c"
;;
*)
subject="补丁应用通知"
color="#95a5a6"
;;
esac
# Slack通知
if [[ -n "${SLACK_WEBHOOK:-}" ]]; then
send_slack_notification "$subject" "$message" "$color" "$patch_file"
fi
# 本地日志通知
log "NOTIFICATION" "$subject: $message"
}
send_slack_notification() {
local subject="$1"
local message="$2"
local color="$3"
local patch_file="$4"
local payload=$(cat << EOF
{
"attachments": [
{
"color": "$color",
"title": "$subject",
"text": "$message",
"fields": [
{
"title": "补丁文件",
"value": "$(basename "$patch_file")",
"short": true
},
{
"title": "应用时间",
"value": "$(date)",
"short": true
},
{
"title": "应用主机",
"value": "$(hostname)",
"short": true
}
]
}
]
}
EOF
)
if command -v curl >/dev/null 2>&1; then
if curl -s -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK" >/dev/null; then
info "✅ Slack通知发送成功"
else
warn "⚠️ Slack通知发送失败"
fi
fi
}
# 主应用函数
apply_patch() {
local patch_file="$1"
local dry_run="${2:-false}"
local target_dir="${3:-./}"
info "🚀 开始应用补丁包: $patch_file"
info "目标目录: $target_dir"
info "干跑模式: $dry_run"
# 发送开始通知
send_notification "start" "$patch_file" "开始应用补丁"
# 验证补丁包
if ! verify_patch_package "$patch_file"; then
send_notification "failure" "$patch_file" "补丁包验证失败"
return 1
fi
# 创建临时目录
local extract_dir="$TEMP_DIR/extract"
local changes_file="$TEMP_DIR/changes.txt"
# 提取补丁包
info ">>提取补丁包..."
if ! extract_patch "$patch_file" "$extract_dir"; then
send_notification "failure" "$patch_file" "补丁包解压失败"
return 1
fi
# 解析清单文件
info ">>解析清单文件..."
if ! parse_manifest "$extract_dir/MANIFEST.MF" > "$changes_file"; then
send_notification "failure" "$patch_file" "清单文件解析失败"
return 1
fi
# 检查是否有变更
info ">>检查变更记录..."
if [[ ! -s "$changes_file" ]]; then
warn "⚠️ 补丁包中没有变更记录"
send_notification "success" "$patch_file" "补丁包无变更记录"
return 0
fi
# 创建备份文件(非干跑模式)
local backup_file=""
if [[ "$dry_run" != "true" ]]; then
info ">>创建备份..."
backup_file=$(create_backup "$changes_file" "$BACKUP_DIR")
if [[ $? -ne 0 ]]; then
send_notification "failure" "$patch_file" "备份创建失败"
return 1
fi
fi
# 应用变更
info ">>应用变更..."
if ! apply_file_changes "$extract_dir" "$changes_file" "$dry_run"; then
send_notification "failure" "$patch_file" "文件应用失败"
return 1
fi
# 验证应用结果(非干跑模式)
info ">>验证应用结果..."
if [[ "$dry_run" != "true" ]]; then
if ! verify_application "$extract_dir" "$changes_file"; then
send_notification "failure" "$patch_file" "应用验证失败"
return 1
fi
# 创建回滚点
info ">>创建回滚点..."
create_rollback_point "$patch_file" "$backup_file" "$extract_dir" "$changes_file"
fi
send_notification "success" "$patch_file" "补丁应用成功"
info "✅ 补丁应用流程完成"
return 0
}
# 显示使用帮助
usage() {
cat << EOF
用法: $0 [选项] <补丁包路径>
选项:
-d, --dry-run 干跑模式(只验证不应用)
-t, --target DIR 目标目录(默认: /
-c, --config FILE 配置文件路径
-v, --verbose 详细输出
-q, --quiet 安静模式
-h, --help 显示此帮助
-V, --version 显示版本
示例:
$0 patch.tar.gz # 应用补丁
$0 --dry-run patch.tar.gz # 干跑模式
$0 --target /app patch.tar.gz # 应用到指定目录
$0 --config custom_config.sh patch.tar.gz
环境变量:
PATCH_CONFIG 配置文件路径
BACKUP_DIR 备份目录
LOG_LEVEL 日志级别
报告问题: 请检查日志文件 $LOG_FILE
EOF
}
# 显示版本信息
show_version() {
echo "patch_applier.sh v2.0.0"
echo "企业级补丁应用系统"
echo "支持原子性操作、完整验证、智能回滚点创建"
}
# 主函数
main() {
local patch_file=""
local dry_run="false"
local target_dir="/"
local verbose_mode="false"
# 初始化
load_config
setup_environment
check_dependencies
acquire_lock
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-d|--dry-run)
dry_run="true"
shift
;;
-t|--target)
target_dir="$2"
shift 2
;;
-c|--config)
CONFIG_FILE="$2"
shift 2
;;
-v|--verbose)
verbose_mode="true"
shift
;;
-q|--quiet)
exec >/dev/null 2>&1
shift
;;
-h|--help)
usage
exit 0
;;
-V|--version)
show_version
exit 0
;;
-*)
error "未知选项: $1"
usage
exit 1
;;
*)
patch_file="$1"
shift
;;
esac
done
# 检查参数
if [[ -z "$patch_file" ]]; then
error "必须指定补丁包路径"
usage
exit 1
fi
if [[ ! -f "$patch_file" ]]; then
error "补丁包不存在: $patch_file"
exit 1
fi
# 执行补丁应用
if apply_patch "$patch_file" "$dry_run" "$target_dir"; then
if [[ "$dry_run" == "true" ]]; then
info "🎉 干跑模式完成 - 可安全应用补丁"
else
info "🎉 补丁应用成功完成!"
fi
exit 0
else
error "💥 补丁应用失败"
exit 1
fi
}
# 异常处理
trap 'error "脚本执行中断"; exit 1' INT TERM
# 执行主函数
main "$@"