#!/bin/bash # patch_generator.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/generate_$(date +%Y%m%d_%H%M%S).log" TEMP_DIR=$(mktemp -d "/tmp/patch_gen_XXXXXX") # 颜色定义 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" ;; "TRACE") color="$PURPLE" ;; *) color="$NC" ;; esac # 日志级别过滤 case "$LOG_LEVEL" in "DEBUG") # DEBUG级别:输出所有日志 ;; "TRACE") # TRACE级别:输出所有日志 ;; "INFO") # INFO级别:只输出INFO、WARN和ERROR日志 if [[ "$level" == "DEBUG" ]] || [[ "$level" == "TRACE" ]]; then return 0 fi ;; "WARN") # WARN级别:只输出WARN和ERROR日志 if [[ "$level" == "DEBUG" ]] || [[ "$level" == "TRACE" ]] || [[ "$level" == "INFO" ]]; then return 0 fi ;; "ERROR") # ERROR级别:只输出ERROR日志 if [[ "$level" != "ERROR" ]]; then return 0 fi ;; *) # 其他级别:不输出任何日志 return 0 ;; esac echo -e "${color}[$timestamp] [$level]${NC} $message" | tee -a "$LOG_FILE" } debug() { log "DEBUG" "$1"; } trace() { log "TRACE" "$1"; } info() { log "INFO" "$1"; } warn() { log "WARN" "$1"; } error() { log "ERROR" "$1"; } # 配置加载 load_config() { if [[ ! -f "$CONFIG_FILE" ]]; then error "配置文件不存在: $CONFIG_FILE" exit 1 fi # 设置默认值 : ${PATCH_NAME:="unnamed-patch"} : ${PATCH_VERSION:="1.0.0"} : ${OUTPUT_DIRECTORY:="/opt/patches"} : ${LOG_LEVEL:="INFO"} : ${MAX_WORKERS:=4} : ${MAX_FILE_SIZE:="100MB"} : ${COMPRESSION_LEVEL:=6} : ${PERMISSIONS_FILTER:="false"} : ${UID_FILTER:="false"} : ${GID_FILTER:="false"} : ${CHECKSUM_ENABLED:="true"} : ${SIGNING_ENABLED:="false"} : ${COMPRESSION_ALGORITHM:="gzip"} : ${HASH_ALGORITHM:="sha256"} : ${COMPARISON_METHOD:="both"} : ${TIME_PRECISION:="second"} : ${LONG_PATH_SUPPORT:="true"} : ${IGNORE_LINE_ENDINGS:="false"} source "$CONFIG_FILE" info "配置文件加载完成" } # 环境设置 setup_environment() { # 创建必要目录 mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$OUTPUT_DIRECTORY" mkdir -p "$TEMP_DIR" # 注意:不在环境设置中启用调试模式,以免影响日志输出 # 如果需要详细调试,可以在脚本开头手动设置 set -x info "环境设置完成" info "日志文件: $LOG_FILE" info "输出目录: $OUTPUT_DIRECTORY" info "临时目录: $TEMP_DIR" } # 清理函数 cleanup() { if [[ -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" info "临时目录已清理: $TEMP_DIR" fi } trap cleanup EXIT # 依赖检查 check_dependencies() { local deps=("tar" "gzip" "jq" "find" "stat" "sha256sum" "date" "mkdir" "cp" "bc") 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 # 检查可选依赖 if [[ "$SIGNING_ENABLED" == "true" ]] && ! command -v gpg >/dev/null 2>&1; then warn "GPG未安装,签名功能将禁用" SIGNING_ENABLED="false" fi info "依赖检查通过" return 0 } # 文件工具函数 parse_size() { local size_str="$1" local unit=$(echo "$size_str" | sed 's/[0-9.]//g' | tr 'a-z' 'A-Z') local value=$(echo "$size_str" | sed 's/[^0-9.]//g') case "$unit" in "KB") echo "$(echo "$value * 1024" | bc)" ;; "MB") echo "$(echo "$value * 1024 * 1024" | bc)" ;; "GB") echo "$(echo "$value * 1024 * 1024 * 1024" | bc)" ;; *) echo "$value" ;; esac } get_file_hash() { local file_path="$1" local algorithm="${2:-sha256}" # 根据 $IGNORE_LINE_ENDINGS 是否为true,决定是否忽略行尾的换行符 local cmd_tr="" if [[ "$IGNORE_LINE_ENDINGS" == "true" ]]; then cmd_tr="tr -d '\r\n'" fi case "$algorithm" in "md5") if [[ -n "$cmd_tr" ]]; then cat "$file_path" | $cmd_tr | md5sum | cut -d' ' -f1 else cat "$file_path" | md5sum | cut -d' ' -f1 fi;; "sha1") if [[ -n "$cmd_tr" ]]; then cat "$file_path" | $cmd_tr | sha1sum | cut -d' ' -f1 else cat "$file_path" | sha1sum | cut -d' ' -f1 fi;; "sha256") if [[ -n "$cmd_tr" ]]; then cat "$file_path" | $cmd_tr | sha256sum | cut -d' ' -f1 else cat "$file_path" | sha256sum | cut -d' ' -f1 fi;; *) if [[ -n "$cmd_tr" ]]; then cat "$file_path" | $cmd_tr | sha256sum | cut -d' ' -f1 else cat "$file_path" | sha256sum | cut -d' ' -f1 fi;; esac } get_file_info() { local file_path="$1" if [[ ! -f "$file_path" ]]; then error "文件不存在: $file_path" return 1 fi local stat_output if stat --version 2>&1 | grep -q GNU; then # GNU stat stat_output=$(stat -c "%s|%Y|%Z|%a|%u|%g" "$file_path") else # BSD stat stat_output=$(stat -f "%z|%m|%c|%p|%u|%g" "$file_path") fi echo "$stat_output" } # 文件筛选 should_include_file() { local file_path="$1" local file_info="$2" IFS='|' read -r size mtime ctime permissions uid gid <<< "$file_info" # 文件大小过滤 local max_size=$(parse_size "$MAX_FILE_SIZE") local min_size=$(parse_size "$MIN_FILE_SIZE") if [[ $size -gt $max_size ]] || [[ $size -lt $min_size ]]; then warn "文件大小超出范围: $file_path ($size bytes, 限制:$min_size-$max_size bytes)" return 1 fi # 包含模式匹配 local include_match=false for pattern in "${INCLUDE_PATTERNS[@]}"; do if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then include_match=true trace "文件匹配包含模式<$pattern>: $file_path" break fi done if [[ ${#INCLUDE_PATTERNS[@]} -gt 0 ]] && ! $include_match; then warn "文件不匹配任何包含模式: $file_path" return 1 fi # 排除模式匹配 for pattern in "${EXCLUDE_PATTERNS[@]}"; do if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then warn "文件匹配排除模式<$pattern>: $file_path" return 1 fi done # 时间过滤 if [[ -n "$MODIFIED_AFTER" ]]; then local filter_time=$(date -d "$MODIFIED_AFTER" +%s 2>/dev/null || date +%s) if [[ $mtime -lt $filter_time ]]; then local mtime_str=$(date -d @$mtime +"%Y-%m-%d %H:%M:%S") warn "文件修改时间过早: $file_path ($mtime_str)" return 1 fi fi # 权限过滤 if [[ "$PERMISSIONS_FILTER" == "true" ]]; then if [[ $permissions -ne 644 ]] && [[ $permissions -ne 755 ]]; then warn "文件权限不符合要求: $file_path ($permissions)" return 1 fi fi # 用户过滤 if [[ "$UID_FILTER" == "true" ]]; then if [[ $uid -ne 0 ]] && [[ $uid -ne 1000 ]]; then warn "文件所有者不符合要求: $file_path ($uid)" return 1 fi fi # 组过滤 if [[ "$GID_FILTER" == "true" ]]; then if [[ $gid -ne 0 ]] && [[ $gid -ne 1000 ]]; then warn "文件组不符合要求: $file_path ($gid)" return 1 fi fi return 0 } # 目录扫描 scan_directory() { local base_dir="$1" local output_file="$2" info "开始扫描目录: $base_dir" if [[ ! -d "$base_dir" ]]; then error "目录不存在: $base_dir" return 1 fi # 清空输出文件 > "$output_file" local file_count=0 local scanned_count=0 # 使用find扫描文件 while IFS= read -r -d '' file_path; do ((scanned_count++)) # 获取文件信息 local file_info if file_info=$(get_file_info "$file_path" 2>/dev/null); then # 检查是否应该包含该文件 local relative_path="${file_path#$base_dir/}" # 这里一定要使用相对路径,方便在config中配置匹配模式 if should_include_file "$relative_path" "$file_info"; then local file_hash=$(get_file_hash "$file_path" "$HASH_ALGORITHM") echo "$relative_path|$file_info|$file_hash" >> "$output_file" ((file_count++)) if [[ $((file_count % 500)) -eq 0 ]]; then info "已扫描 $file_count 个文件..." fi fi else warn "无法获取文件信息: $file_path" fi done < <(find "$base_dir" -type f -print0 2>/dev/null) info "目录扫描完成: $base_dir (扫描: $scanned_count, 包含: $file_count)" info "-------------------------------------------------------------- " return 0 } # 并行目录扫描 scan_directory_parallel() { local base_dir="$1" local output_file="$2" info "开始并行扫描目录: $base_dir" if [[ ! -d "$base_dir" ]]; then error "目录不存在: $base_dir" return 1 fi # 清空输出文件 > "$output_file" # 创建临时文件存储处理结果 local temp_result="$TEMP_DIR/parallel_result_$$" # 加载函数文件 local func_file="$SCRIPT_DIR/patch_fnc.sh" # 使用xargs进行并行处理 find "$base_dir" -type f -print0 2>/dev/null | \ xargs -0 -P "$MAX_WORKERS" -I {} bash -c ' source "'"$func_file"'" file_path="{}" file_info=$(get_file_info "$file_path" 2>/dev/null) if [[ $? -eq 0 ]] && should_include_file "$file_path" "$file_info"; then file_hash=$(get_file_hash "$file_path" "'"$HASH_ALGORITHM"'") relative_path="${file_path#'"$base_dir"'/}" echo "$relative_path|$file_info|$file_hash" fi ' 2>>"$LOG_FILE" | \ tee -a "$temp_result" >> "$output_file" # 计算文件数量 local file_count=$(wc -l < "$temp_result") local scanned_count=$(find "$base_dir" -type f 2>/dev/null | wc -l) # 清理临时文件 rm -f "$temp_result" info "并行扫描完成: $base_dir (扫描: $scanned_count, 包含: $file_count)" return 0 } # 差异比较 compare_files() { local old_file="$1" local new_file="$2" local output_file="$3" info "开始比较文件差异" # 输出差异比较规则 info "比较规则: " info "比较方法配置: $COMPARISON_METHOD" info "哈希算法配置: $HASH_ALGORITHM" info "时间精度:$TIME_PRECISION" info "是否忽略换行符不同: $IGNORE_LINE_ENDINGS" declare -A old_files declare -A new_files # 加载旧版本文件信息 while IFS='|' read -r path file_info hash; do old_files["$path"]="$file_info|$hash" done < "$old_file" # 加载新版本文件信息 while IFS='|' read -r path file_info hash; do new_files["$path"]="$file_info|$hash" done < "$new_file" # 清空输出文件 > "$output_file" local changes=() # 检测新增文件 for path in "${!new_files[@]}"; do if [[ -z "${old_files[$path]:-}" ]]; then changes+=("ADDED|$path|${new_files[$path]}") warn "检测新增文件: $path" fi done # 检测删除文件 for path in "${!old_files[@]}"; do if [[ -z "${new_files[$path]:-}" ]]; then changes+=("DELETED|$path|${old_files[$path]}") warn "检测到删除文件: $path" fi done # 检测修改文件 for path in "${!new_files[@]}"; do if [[ -n "${old_files[$path]:-}" ]]; then IFS='|' read -r old_info old_hash <<< "${old_files[$path]}" IFS='|' read -r new_info new_hash <<< "${new_files[$path]}" local is_modified=false local old_short_hash="${old_hash##*|}" # 使用短哈希值,不使用复合哈希值,因为复合哈希值包含权限,用户和组信息, local new_short_hash="${new_hash##*|}" # 使用短哈希值,不使用复合哈希值,因为复合哈希值包含权限,用户和组信息, case "$COMPARISON_METHOD" in "content") [[ "$old_short_hash" != "$new_short_hash" ]] && is_modified=true if $is_modified; then trace "检测到修改文件: $path | 哈希值变化: <$old_short_hash> => <$new_short_hash>" fi ;; "time") IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid <<< "$old_info" IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid <<< "$new_info" if [[ "$TIME_PRECISION" == "second" ]]; then [[ $old_mtime -ne $new_mtime ]] && is_modified=true if $is_modified; then trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" fi else [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]] && is_modified=true if $is_modified; then trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" fi fi ;; "both") IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid <<< "$old_info" IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid <<< "$new_info" if [[ "$old_short_hash" != "$new_short_hash" ]]; then is_modified=true trace "检测到修改文件: $path | 哈希值变化: <$old_short_hash> => <$new_short_hash>" elif [[ "$TIME_PRECISION" == "second" ]] && [[ $old_mtime -ne $new_mtime ]]; then is_modified=true trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" elif [[ "$TIME_PRECISION" == "millisecond" ]] && [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]]; then is_modified=true trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" fi ;; esac if $is_modified; then changes+=("MODIFIED|$path|${old_files[$path]}|${new_files[$path]}") fi fi done # 写入变更到文件 for change in "${changes[@]}"; do echo "$change" >> "$output_file" done local change_count=${#changes[@]} info "差异比较完成: $change_count 个变更" return 0 } # 处理长路径复制 handle_long_path_copy() { local source="$1" local dest="$2" local max_path_length=200 local dest_length=${#dest} if [[ $dest_length -le $max_path_length ]]; then cp -p "$source" "$dest" return $? fi # 方法1: 使用相对路径 local source_dir=$(dirname "$source") local dest_dir=$(dirname "$dest") local filename=$(basename "$source") if pushd "$source_dir" >/dev/null 2>&1; then if cp -p "$filename" "$dest" 2>/dev/null; then popd >/dev/null 2>&1 return 0 fi popd >/dev/null 2>&1 fi # 方法2: 使用临时短路径 local temp_file="$TEMP_DIR/short_$(basename "$source")" if cp -p "$source" "$temp_file" && mv "$temp_file" "$dest"; then return 0 fi # 方法3: 使用rsync(如果可用) if command -v rsync >/dev/null 2>&1; then if rsync -a "$source" "$dest"; then return 0 fi fi error "无法复制长路径文件: $source -> $dest" return 1 } # 生成清单文件 generate_manifest() { local changes_file="$1" local patch_dir="$2" local manifest_file="$patch_dir/MANIFEST.MF" info "生成清单文件: $manifest_file" cat > "$manifest_file" << EOF # 补丁包清单 名称: $PATCH_NAME 版本: $PATCH_VERSION 描述: $PATCH_DESCRIPTION 作者: $PATCH_AUTHOR 邮箱: $PATCH_EMAIL 生成时间: $(date) 生成主机: $(hostname) 比较方法: $COMPARISON_METHOD 哈希算法: $HASH_ALGORITHM 包含目录: ${TARGET_DIRECTORIES[*]} # 变更统计 EOF # 统计变更类型 local added_count=$(grep -c "^ADDED" "$changes_file") local modified_count=$(grep -c "^MODIFIED" "$changes_file") local deleted_count=$(grep -c "^DELETED" "$changes_file") cat >> "$manifest_file" << EOF 新增文件: $added_count 修改文件: $modified_count 删除文件: $deleted_count 总变更数: $((added_count + modified_count + deleted_count)) # 变更详情 EOF # 写入变更详情 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED") IFS='|' read -r size mtime ctime perm uid gid hash <<< "$extra_info" printf "ADDED|%s|%d|%s|%s\n" "$path" "$size" "$hash" "$(date -d "@$mtime")" >> "$manifest_file" ;; "MODIFIED") IFS='|' read -r old_info new_info <<< "$extra_info" IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid old_hash <<< "$old_info" IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid new_hash <<< "$new_info" printf "MODIFIED|%s|%d->%d|%s->%s\n" "$path" "$old_size" "$new_size" "$old_hash" "$new_hash" >> "$manifest_file" ;; "DELETED") IFS='|' read -r size mtime ctime perm uid gid hash <<< "$extra_info" printf "DELETED|%s|%d|%s\n" "$path" "$size" "$hash" >> "$manifest_file" ;; esac done < "$changes_file" info "清单文件生成完成" } # 创建补丁包 create_patch_package() { local patch_dir="$1" local output_path="$2" info "创建补丁包: $output_path" info "压缩算法: $COMPRESSION_ALGORITHM" info "长路径支持: $LONG_PATH_SUPPORT" info "补丁内容目录: $patch_dir" # 确定压缩选项 local compression_flag="" case "$COMPRESSION_ALGORITHM" in "gzip") compression_flag="z" ;; "bzip2") compression_flag="j" ;; "xz") compression_flag="J" ;; *) compression_flag="" ;; esac # 处理长路径支持 local tar_options="-c${compression_flag}f" if [[ "$LONG_PATH_SUPPORT" == "true" ]]; then tar_options="--force-local $tar_options" fi # 创建补丁包 if pushd "$patch_dir" >/dev/null 2>&1; then if tar $tar_options "$output_path" .; then local size=$(du -h "$output_path" | cut -f1) info "✅ 补丁包创建成功: $output_path ($size)" popd >/dev/null 2>&1 return 0 else error "❌ 补丁包创建失败" popd >/dev/null 2>&1 return 1 fi else error "无法进入补丁目录: $patch_dir" return 1 fi } # 安全签名 sign_package() { local package_path="$1" if [[ "$SIGNING_ENABLED" != "true" ]]; then info "签名功能已禁用" return 0 fi if [[ ! -f "$PRIVATE_KEY" ]]; then warn "私钥文件不存在: $PRIVATE_KEY" return 1 fi info "开始签名补丁包" if command -v gpg >/dev/null 2>&1; then if gpg --homedir "/etc/patch/keys" \ --batch --yes --detach-sign \ --local-user "$PATCH_AUTHOR" \ --output "${package_path}.sig" \ "$package_path"; then info "✅ 补丁包签名完成: ${package_path}.sig" return 0 else error "❌ 签名失败" return 1 fi else warn "GPG未安装,跳过签名" return 1 fi } # 生成校验和 generate_checksum() { local package_path="$1" if [[ "$CHECKSUM_ENABLED" != "true" ]]; then info "校验和生成已禁用" return 0 fi info "生成校验和文件" local checksum_file="${package_path}.sha256" if sha256sum "$package_path" > "$checksum_file"; then info "✅ 校验和生成完成: $checksum_file" return 0 else error "❌ 校验和生成失败" return 1 fi } # 生成回滚包 generate_rollback_package() { if [[ "$ROLLBACK_ENABLED" != "true" ]]; then info "回滚包生成已禁用" return 0 fi local source_dir="$1" local changes_file="$2" local patch_path="$3" info "开始生成回滚包" local rollback_dir="$TEMP_DIR/rollback_content" mkdir -p "$rollback_dir" local rollback_files=() # 收集需要回滚的文件 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "MODIFIED"|"DELETED") local source_file="$source_dir/$path" if [[ -f "$source_file" ]]; then local dest_file="$rollback_dir/$path" local dest_dir=$(dirname "$dest_file") mkdir -p "$dest_dir" if handle_long_path_copy "$source_file" "$dest_file"; then rollback_files+=("$path") trace "回滚文件: $source_file" else warn "无法复制回滚文件: $source_file" fi fi ;; esac done < "$changes_file" if [[ ${#rollback_files[@]} -eq 0 ]]; then warn "没有需要回滚的文件" return 0 fi # 创建回滚包 local rollback_path="${patch_path%.*}.rollback.${patch_path##*.}" if create_patch_package "$rollback_dir" "$rollback_path"; then local size=$(du -h "$rollback_path" | cut -f1) info "✅ 回滚包生成完成: $rollback_path ($size)" echo "$rollback_path" return 0 else error "❌ 回滚包生成失败" return 1 fi } # 通知系统 send_notification() { local status="$1" local message="$2" local patch_path="${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_path" fi # 邮件通知(简化实现) if [[ -n "$EMAIL_RECIPIENTS" ]]; then send_email_notification "$subject" "$message" "$patch_path" fi } send_slack_notification() { local subject="$1" local message="$2" local color="$3" local patch_path="$4" local payload=$(cat << EOF { "attachments": [ { "color": "$color", "title": "$subject", "text": "$message", "fields": [ { "title": "补丁名称", "value": "$PATCH_NAME", "short": true }, { "title": "版本", "value": "$PATCH_VERSION", "short": true }, { "title": "文件", "value": "$(basename "$patch_path")", "short": true }, { "title": "时间", "value": "$(date)", "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 } send_email_notification() { local subject="$1" local message="$2" local patch_path="$3" # 简化实现 - 实际环境中应使用sendmail或mail命令 info "邮件通知: $subject - $message" local email_content=" 补丁生成通知 ======================================== 主题: $subject 时间: $(date) 主机: $(hostname) 补丁: $PATCH_NAME v$PATCH_VERSION 文件: $(basename "$patch_path") 描述: $message ======================================== " # 这里可以添加实际的邮件发送逻辑 echo "$email_content" > "$TEMP_DIR/email_notification.txt" info "邮件内容已保存: $TEMP_DIR/email_notification.txt" } # 主生成函数 generate_patch() { local source_dir="$1" local target_dir="$2" local output_path="$3" info "开始生成补丁包" send_notification "start" "开始生成补丁包" "$output_path" # 扫描目录 local old_manifest="$TEMP_DIR/old_manifest.txt" local new_manifest="$TEMP_DIR/new_manifest.txt" if [[ "$PARALLEL_PROCESSING" == "true" ]]; then scan_directory_parallel "$source_dir" "$old_manifest" scan_directory_parallel "$target_dir" "$new_manifest" else scan_directory "$source_dir" "$old_manifest" scan_directory "$target_dir" "$new_manifest" fi # 比较差异 local changes_file="$TEMP_DIR/changes.txt" compare_files "$old_manifest" "$new_manifest" "$changes_file" # 检查是否有变更 local change_count=$(wc -l < "$changes_file" 2>/dev/null || echo "0") if [[ $change_count -eq 0 ]]; then warn "未发现文件变更,无需生成补丁" send_notification "success" "未发现文件变更" "$output_path" return 0 fi # 创建补丁内容目录 local patch_content_dir="$TEMP_DIR/patch_content" mkdir -p "$patch_content_dir/files" # 复制变更文件 info "复制变更文件到补丁目录" local copied_count=0 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED"|"MODIFIED") local source_file="$target_dir/$path" local dest_file="$patch_content_dir/files/$path" local dest_dir=$(dirname "$dest_file") mkdir -p "$dest_dir" if handle_long_path_copy "$source_file" "$dest_file"; then ((copied_count++)) trace "复制文件: $source_file -> $dest_file" else warn "文件复制失败: $source_file" fi ;; esac done < "$changes_file" info "文件复制完成: $copied_count 个文件" # 生成清单 generate_manifest "$changes_file" "$patch_content_dir" # 创建补丁包 if create_patch_package "$patch_content_dir" "$output_path"; then # 生成校验和 generate_checksum "$output_path" # 签名补丁包 sign_package "$output_path" # 生成回滚包 generate_rollback_package "$source_dir" "$changes_file" "$output_path" send_notification "success" "补丁包生成成功" "$output_path" return 0 else send_notification "failure" "补丁包生成失败" "$output_path" return 1 fi } # 批量处理 batch_generate() { local config_dir="$1" if [[ ! -d "$config_dir" ]]; then error "配置目录不存在: $config_dir" return 1 fi info "开始批量生成补丁包" local success_count=0 local total_count=0 # 处理每个配置文件 for config_file in "$config_dir"/*.sh; do if [[ -f "$config_file" ]]; then ((total_count++)) info "处理配置: $(basename "$config_file")" # 备份当前配置 cp "$CONFIG_FILE" "$TEMP_DIR/original_config.sh" # 使用新配置 cp "$config_file" "$CONFIG_FILE" load_config # 生成补丁包 if main; then ((success_count++)) fi # 恢复原配置 cp "$TEMP_DIR/original_config.sh" "$CONFIG_FILE" load_config fi done info "批量生成完成: $success_count/$total_count 成功" if [[ $success_count -eq $total_count ]]; then return 0 else return 1 fi } # 主函数 main() { local source_dir="${1:-}" local target_dir="${2:-}" local output_path="${3:-}" # 参数验证 if [[ -z "$source_dir" ]] || [[ -z "$target_dir" ]]; then error "必须指定源目录和目标目录" echo "用法: $0 <源目录> <目标目录> [输出路径]" echo "示例:" echo " $0 /old/version /new/version" echo " $0 /old/version /new/version /opt/patches/patch.tar.gz" echo " $0 batch /path/to/configs" exit 1 fi # 批量处理模式 if [[ "$source_dir" == "batch" ]]; then batch_generate "$target_dir" return $? fi # 设置默认输出路径 if [[ -z "$output_path" ]]; then local timestamp=$(date +%Y%m%d_%H%M%S) output_path="$OUTPUT_DIRECTORY/patch-${PATCH_NAME}-${PATCH_VERSION}-${timestamp}.tar.gz" fi # 验证目录存在性 if [[ ! -d "$source_dir" ]]; then error "源目录不存在: $source_dir" exit 1 fi if [[ ! -d "$target_dir" ]]; then error "目标目录不存在: $target_dir" exit 1 fi # 执行补丁生成 if generate_patch "$source_dir" "$target_dir" "$output_path"; then info "🎉 补丁包生成成功完成!" info "📦 补丁文件: $output_path" info "📋 日志文件: $LOG_FILE" return 0 else error "💥 补丁包生成失败" return 1 fi } # 异常处理 trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM # 加载配置 load_config # 设置环境 setup_environment # 检查依赖 check_dependencies # 执行主函数 main "$@"