Files
patch-system-tools/patch_generator.sh

1040 lines
30 KiB
Bash
Raw 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_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
}
# 通知系统
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"
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 "$@"