957 lines
24 KiB
Bash
957 lines
24 KiB
Bash
#!/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"
|
||
LOCK_FILE="/tmp/patch_apply.lock"
|
||
|
||
# 颜色定义
|
||
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"}
|
||
: ${LOG_FILE:="/tmp/patch_apply_$(date +%Y%m%d_%H%M%S).log"}
|
||
: ${BACKUP_DIR:="/var/backups/patch"}
|
||
: ${TEMP_DIR:="/tmp/patch_apply_$$"}
|
||
: ${ROLLBACK_ENABLED:="true"}
|
||
: ${VALIDATION_ENABLED:="true"}
|
||
: ${NOTIFICATIONS_ENABLED:="true"}
|
||
: ${ATOMIC_OPERATIONS:="true"}
|
||
: ${MAX_BACKUP_COUNT:=10}
|
||
}
|
||
|
||
setup_environment() {
|
||
mkdir -p "$(dirname "$LOG_FILE")"
|
||
mkdir -p "$BACKUP_DIR"
|
||
mkdir -p "$TEMP_DIR"
|
||
|
||
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" "sha256sum" "cp" "mkdir" "find")
|
||
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)" # 默认是当前工作目录
|
||
|
||
if tar -czf "$backup_tar" -C "${work_dir}" "${files_to_backup[@]/#\//}"; then
|
||
local size=$(du -h "$backup_tar" | cut -f1)
|
||
info "✅ 备份创建完成: $backup_tar ($size)" >&2
|
||
echo "$backup_tar"
|
||
return 0
|
||
else
|
||
error "❌ 备份创建失败" >&2
|
||
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"
|
||
|
||
if [[ "$ROLLBACK_ENABLED" != "true" ]]; then
|
||
info "回滚功能已禁用"
|
||
return 0
|
||
fi
|
||
|
||
info "🔄 创建回滚点..."
|
||
|
||
local rollback_info_file="$BACKUP_DIR/rollback_info.json"
|
||
local patch_name=$(basename "$patch_file")
|
||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||
|
||
# 读取清单信息
|
||
local manifest_file="$extract_dir/MANIFEST.MF"
|
||
local patch_info=""
|
||
if [[ -f "$manifest_file" ]]; then
|
||
patch_info=$(grep -E "^(名称|版本|描述):" "$manifest_file" | head -3)
|
||
fi
|
||
|
||
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": "$(echo "$patch_info" | tr '\n' ';')",
|
||
"status": "applied"
|
||
}
|
||
EOF
|
||
)
|
||
|
||
echo "$rollback_info" > "$rollback_info_file"
|
||
info "✅ 回滚点创建完成: $rollback_info_file"
|
||
}
|
||
|
||
# 发送通知
|
||
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"
|
||
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 显示此帮助
|
||
|
||
示例:
|
||
$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
|
||
}
|
||
|
||
# 主函数
|
||
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
|
||
;;
|
||
-*)
|
||
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 "$@"
|