1099 lines
32 KiB
Bash
1099 lines
32 KiB
Bash
#!/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 "$@" |