From c0252a2dd222ca5e497daf63ef83ff1050a37691 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 15 Nov 2025 16:15:58 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E9=92=88=E5=AF=B9=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E5=8F=8A=E8=BF=98=E5=8E=9F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/deploy.md | 46 +- scripts/backup_restore/.env | 21 + scripts/backup_restore/backup_config.json | 59 + scripts/backup_restore/backup_config.php | 75 ++ scripts/backup_restore/backup_server.sh | 278 +++++ .../backup_restore/install_backup_system.sh | 111 ++ scripts/backup_restore/readme.md | 10 + scripts/backup_restore/restore_server.sh | 275 +++++ scripts/backup_restore/verify_backup.sh | 82 ++ scripts/deploy.php | 65 +- scripts/patch_tools/docker-compose.yml | 16 + scripts/patch_tools/install_patch_system.sh | 223 ++++ scripts/patch_tools/patch_applier.sh | 587 +++++++++ scripts/patch_tools/patch_config.sh | 177 +++ scripts/patch_tools/patch_config.sh.example | 202 ++++ scripts/patch_tools/patch_fnc.sh | 126 ++ scripts/patch_tools/patch_generator.sh | 1052 +++++++++++++++++ scripts/patch_tools/patch_rollback.sh | 356 ++++++ scripts/patch_tools/patch_verifier.sh | 538 +++++++++ scripts/patch_tools/patch_workflow.sh | 153 +++ scripts/patch_tools/shop_src_patch_config.sh | 210 ++++ 21 files changed, 4618 insertions(+), 44 deletions(-) create mode 100644 scripts/backup_restore/.env create mode 100644 scripts/backup_restore/backup_config.json create mode 100644 scripts/backup_restore/backup_config.php create mode 100644 scripts/backup_restore/backup_server.sh create mode 100644 scripts/backup_restore/install_backup_system.sh create mode 100644 scripts/backup_restore/readme.md create mode 100644 scripts/backup_restore/restore_server.sh create mode 100644 scripts/backup_restore/verify_backup.sh create mode 100644 scripts/patch_tools/docker-compose.yml create mode 100644 scripts/patch_tools/install_patch_system.sh create mode 100644 scripts/patch_tools/patch_applier.sh create mode 100644 scripts/patch_tools/patch_config.sh create mode 100644 scripts/patch_tools/patch_config.sh.example create mode 100644 scripts/patch_tools/patch_fnc.sh create mode 100644 scripts/patch_tools/patch_generator.sh create mode 100644 scripts/patch_tools/patch_rollback.sh create mode 100644 scripts/patch_tools/patch_verifier.sh create mode 100644 scripts/patch_tools/patch_workflow.sh create mode 100644 scripts/patch_tools/shop_src_patch_config.sh diff --git a/docs/deploy.md b/docs/deploy.md index 0f9a128ff..b091a023c 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,14 +1,56 @@ # 部署说明 +## 部署准备 + +1. 备份服务器上最新代码 + +``` bash +# 备份服务器上最新代码 +cd /www/tools/ +php backup_restore.php backup --source=/www/wwwroot/myweb/newfwq/ --backup-dir=./backup --config=./backup_config.php --verbose --dry-run +``` + +2. 查看依赖环境 + +- PHP版本:7.4.3nts +- 数据库:MySQL、Redis +- 服务器:Nginx + PHP-FPM +- 环境变量: .env文件 + +3. 下载备份文件到本地 + +作为二次备份及版本管理,下载备份文件到本地,以便后续需要时可以恢复。 + +## 还原备份 + +``` bash +php backup_restore.php restore --file=backup.tar.gz --target=/path/to/target + +# 示例,还原备份文件到指定目录 +cd /www/tools/ +php backup_restore.php restore --file=./backup/newfwq_backup_2025-11-12_134620.tar.gz --target=/www/tools/test-newfwq/ + + + +``` + ## 自动部署 +### 准备条件 + +- 1. 下载备份文件到本地 +- 2. 查看依赖环境 +- 3. 解压到本地的ftp-src目录,确保目录结构正确,相关目录,以备份文件为主。(也就是说,先清空ftp-src下的相关子目录,再解压备份文件到该目录) +- 4. 利用比较工具(如Beyond Compare)比较备份文件与当前代码的差异,确保没有重要的代码变更。 +- 5. 确认备份文件与当前代码的差异,无重要变更后,再进行创建部署包 + ### 创建部署包 ```bash # 代码示例 cd /d/projects/shop-projects/backend -/d/phpstudy_pro/Extensions/php/php7.4.3nts/php.exe ./scripts/generate_deploy_package.php --source ./src --target /d/projects/zips/php后端源码/2025-11-10 --output update.zip +php ./scripts/generate_deploy_package.php --source ./src --target ./ftp-src --output update.zip # Linux php ./scripts/generate_deploy_package.php --source ./src --target ./ftp-src --output update.zip @@ -16,6 +58,8 @@ php ./scripts/generate_deploy_package.php --source ./src --target ./ftp-src --ou ## 部署流程 + +### 测试环境 ``` bash # 部署流程 cd /var/www/all_source diff --git a/scripts/backup_restore/.env b/scripts/backup_restore/.env new file mode 100644 index 000000000..509be19aa --- /dev/null +++ b/scripts/backup_restore/.env @@ -0,0 +1,21 @@ +# 备份配置 +BACKUP_NAME="server-production" +BACKUP_SOURCE="/var/www/html /etc/nginx" +BACKUP_DEST="/backups" + +# 数据库配置 +DB_HOST="localhost" +DB_USER="backup_user" +DB_PASSWORD="secure_password" + +# 通知配置 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." +EMAIL_TO="admin@company.com" + +# 加密配置 +ENCRYPTION_KEY_PATH="/etc/backup/keys" +GPG_PASSPHRASE="your_secure_passphrase" + +# 长路径支持 +LONG_PATH_SUPPORT="true" +TAR_OPTIONS="--force-local" \ No newline at end of file diff --git a/scripts/backup_restore/backup_config.json b/scripts/backup_restore/backup_config.json new file mode 100644 index 000000000..332a1aba8 --- /dev/null +++ b/scripts/backup_restore/backup_config.json @@ -0,0 +1,59 @@ +{ + "version": "1.0.0", + "description": "服务器源码备份还原配置", + "backup": { + "source_dirs": [ + "/var/www/html", + "/etc/nginx", + "/etc/mysql", + "/home/user/projects" + ], + "exclude_patterns": [ + "*.log", + "*.tmp", + "node_modules", + ".git", + "__pycache__", + "*.pyc", + ".DS_Store", + "thumbs.db" + ], + "max_backup_size": "10G", + "backup_retention_days": 30, + "long_path_support": true + }, + "storage": { + "local_path": "/backups/server-src", + "remote_path": "user@backup-server:/backups", + "cloud_storage": { + "s3_bucket": "my-backup-bucket", + "azure_container": "backups", + "gcs_bucket": "server-backups" + } + }, + "database": { + "enabled": true, + "mysql": { + "host": "localhost", + "user": "backup_user", + "password": "secure_password", + "databases": ["app_db", "user_db"] + }, + "postgresql": { + "host": "localhost", + "user": "backup_user", + "password": "secure_password", + "databases": ["app_db"] + } + }, + "notifications": { + "email": "admin@company.com", + "slack_webhook": "https://hooks.slack.com/services/...", + "discord_webhook": "https://discord.com/api/webhooks/..." + }, + "encryption": { + "enabled": true, + "public_key": "/path/to/public.key", + "private_key": "/path/to/private.key" + } +} \ No newline at end of file diff --git a/scripts/backup_restore/backup_config.php b/scripts/backup_restore/backup_config.php new file mode 100644 index 000000000..66f48ec4a --- /dev/null +++ b/scripts/backup_restore/backup_config.php @@ -0,0 +1,75 @@ + '/var/backups', + + // 排除规则 - 使用正则表达式匹配需要排除的文件路径 + 'exclude_patterns' => [ + '/\.git/', // 排除git版本控制目录 + '/node_modules/', // 排除npm依赖 + '/\.log$/', // 排除日志文件 + '/\.tmp$/', // 排除临时文件 + '/cache/', // 排除缓存目录 + '/temp/', // 排除临时目录 + '/tmp/', // 排除临时目录 + '/logs/', // 排除日志目录 + '/runtime/', // 排除运行时目录 + '/uploads/', // 排除上传目录 + '/attachment/', // 排除附件目录 + ], + + // 包含规则 - 使用正则表达式匹配需要强制包含的文件路径 + // 即使这些文件被排除规则匹配,也会被包含 + 'include_patterns' => [ + '/.well-known/', // 包含well-known目录 + '/addon/', // 包含插件目录 + '/addons/', // 包含插件目录 + '/app/', // 包含应用目录 + '/config/', // 包含配置目录 + '/extend/', // 包含扩展目录 + // '/h5/', // 包含h5目录 + // '/hwapp/', // 包含hwapp目录 + '/public/', // 包含公共目录 + '/vendor/', // 包含composer依赖 + // '/web/', // 包含web目录 + + // 以下是包含的文件 + '/.404.html', // 包含404页面 + '/index.php', // 包含入口文件 + '/install.php', // 包含安装文件 + '/install.lock', // 包含安装锁文件 + + '/.env', // 包含环境变量文件 + '/.env.test', // 包含测试环境变量文件 + '/.env.production', // 包含生产环境变量文件 + '/.env.staging', // 包含预发布环境变量文件 + '/.env.development', // 包含开发环境变量文件 + '/.env.local', // 包含本地环境变量文件 + '/.gitignore', // 包含git忽略文件 + '/.htaccess', // 包含htaccess文件 + '/.user.ini', // 包含user.ini文件 + '/composer.json', // 包含composer.json文件 + '/composer.lock', // 包含composer.lock文件 + ], + + // 备份模式 - 使用通配符匹配文件类型 + // 这里使用了更全面的文件类型覆盖,确保支持所有常见文件 + 'backup_patterns' => [ + // 如果为空数组,将备份所有文件(不建议) + ], + + // 保留的最大备份文件数量,超过此数量会自动删除最旧的备份 + 'max_backups' => 30, + + // 压缩级别(1-9,1最快但压缩率最低,9最慢但压缩率最高) + 'compression_level' => 6, + + // 是否保留文件权限 + 'preserve_permissions' => true + +]; diff --git a/scripts/backup_restore/backup_server.sh b/scripts/backup_restore/backup_server.sh new file mode 100644 index 000000000..2fc5af7cf --- /dev/null +++ b/scripts/backup_restore/backup_server.sh @@ -0,0 +1,278 @@ +#!/bin/bash +# backup_server.sh - 支持长路径和配置文件的智能备份脚本 + +set -euo pipefail + +# 脚本配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/backup_config.json" +ENV_FILE="${SCRIPT_DIR}/.env" +LOG_FILE="${SCRIPT_DIR}/backup_$(date +%Y%m%d_%H%M%S).log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"; } +error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"; } +info() { echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$LOG_FILE"; } + +# 加载配置 +load_config() { + if [[ ! -f "$CONFIG_FILE" ]]; then + error "配置文件不存在: $CONFIG_FILE" + exit 1 + fi + + if [[ -f "$ENV_FILE" ]]; then + source "$ENV_FILE" + log "环境配置文件已加载" + fi + + # 解析JSON配置 + BACKUP_NAME=$(jq -r '.backup.name // "server-backup"' "$CONFIG_FILE") + SOURCE_DIRS=$(jq -r '.backup.source_dirs[]?' "$CONFIG_FILE" | tr '\n' ' ') + EXCLUDE_PATTERNS=$(jq -r '.backup.exclude_patterns[]?' "$CONFIG_FILE") + LONG_PATH_SUPPORT=$(jq -r '.backup.long_path_support // "true"' "$CONFIG_FILE") + LOCAL_BACKUP_PATH=$(jq -r '.storage.local_path // "/backups"' "$CONFIG_FILE") + + log "配置文件加载完成" +} + +# 检查长路径支持 +check_long_path_support() { + if [[ "$LONG_PATH_SUPPORT" == "true" ]]; then + # 检查tar版本是否支持长路径 + TAR_VERSION=$(tar --version | head -1 | grep -oE '[0-9]+\.[0-9]+') + if [[ $(echo "$TAR_VERSION >= 1.22" | bc -l 2>/dev/null) -eq 1 ]]; then + TAR_OPTIONS="--force-local" + log "tar版本 $TAR_VERSION 支持长路径" + else + warn "tar版本较低,长路径支持可能受限" + TAR_OPTIONS="" + fi + else + TAR_OPTIONS="" + info "长路径支持已禁用" + fi +} + +# 创建排除文件 +create_exclude_file() { + local exclude_file="${SCRIPT_DIR}/exclude_patterns.txt" + + # 从配置文件读取排除模式 + jq -r '.backup.exclude_patterns[]?' "$CONFIG_FILE" > "$exclude_file" + + # 添加系统默认排除 + cat >> "$exclude_file" << EOF +*.log +*.tmp +*.swp +*.swo +.DS_Store +Thumbs.db +._* +EOF + + echo "$exclude_file" +} + +# 备份数据库 +backup_databases() { + local db_enabled=$(jq -r '.database.enabled // "false"' "$CONFIG_FILE") + + if [[ "$db_enabled" != "true" ]]; then + info "数据库备份已禁用" + return 0 + fi + + local timestamp=$(date +%Y%m%d_%H%M%S) + local db_backup_dir="${LOCAL_BACKUP_PATH}/database_${timestamp}" + + mkdir -p "$db_backup_dir" + log "开始备份数据库..." + + # MySQL备份 + if command -v mysqldump > /dev/null; then + local mysql_config=$(jq -r '.database.mysql // {}' "$CONFIG_FILE") + if [[ "$mysql_config" != "{}" ]]; then + local mysql_host=$(jq -r '.host // "localhost"' <<< "$mysql_config") + local mysql_user=$(jq -r '.user // "root"' <<< "$mysql_config") + local mysql_password=$(jq -r '.password // ""' <<< "$mysql_config") + local databases=$(jq -r '.databases[]?' <<< "$mysql_config") + + for db in $databases; do + info "备份MySQL数据库: $db" + if [[ -n "$mysql_password" ]]; then + mysqldump -h "$mysql_host" -u "$mysql_user" -p"$mysql_password" "$db" > "${db_backup_dir}/${db}_mysql.sql" + else + mysqldump -h "$mysql_host" -u "$mysql_user" "$db" > "${db_backup_dir}/${db}_mysql.sql" + fi + done + fi + fi + + # PostgreSQL备份 + if command -v pg_dump > /dev/null; then + local pgsql_config=$(jq -r '.database.postgresql // {}' "$CONFIG_FILE") + if [[ "$pgsql_config" != "{}" ]]; then + local pgsql_host=$(jq -r '.host // "localhost"' <<< "$pgsql_config") + local pgsql_user=$(jq -r '.user // "postgres"' <<< "$pgsql_config") + local databases=$(jq -r '.databases[]?' <<< "$pgsql_config") + + for db in $databases; do + info "备份PostgreSQL数据库: $db" + pg_dump -h "$pgsql_host" -U "$pgsql_user" "$db" > "${db_backup_dir}/${db}_pgsql.sql" + done + fi + fi + + log "数据库备份完成: $db_backup_dir" +} + +# 主备份函数 +perform_backup() { + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_file="${LOCAL_BACKUP_PATH}/backup_${timestamp}.tar.gz" + local exclude_file=$(create_exclude_file) + + log "开始备份服务器源码..." + info "源目录: $SOURCE_DIRS" + info "备份文件: $backup_file" + + # 创建备份目录 + mkdir -p "$LOCAL_BACKUP_PATH" + + # 备份数据库 + backup_databases + + # 使用tar进行备份(支持长路径) + local tar_command="tar $TAR_OPTIONS -czf \"$backup_file\" --exclude-from=\"$exclude_file\" $SOURCE_DIRS" + + log "执行备份命令: $tar_command" + + if eval "$tar_command"; then + local backup_size=$(du -h "$backup_file" | cut -f1) + log "✅ 备份成功! 文件大小: $backup_size" + echo "$backup_file" > "${SCRIPT_DIR}/latest_backup.txt" + else + error "❌ 备份失败" + exit 1 + fi + + # 清理临时文件 + rm -f "$exclude_file" +} + +# 加密备份文件 +encrypt_backup() { + local encryption_enabled=$(jq -r '.encryption.enabled // "false"' "$CONFIG_FILE") + + if [[ "$encryption_enabled" != "true" ]]; then + info "加密功能已禁用" + return 0 + fi + + local backup_file=$(cat "${SCRIPT_DIR}/latest_backup.txt" 2>/dev/null || echo "") + if [[ -z "$backup_file" || ! -f "$backup_file" ]]; then + warn "未找到备份文件进行加密" + return 0 + fi + + local public_key=$(jq -r '.encryption.public_key // ""' "$CONFIG_FILE") + if [[ -z "$public_key" || ! -f "$public_key" ]]; then + error "加密公钥不存在: $public_key" + return 1 + fi + + log "开始加密备份文件..." + + if command -v gpg > /dev/null; then + gpg --encrypt --recipient-file "$public_key" --trust-model always "$backup_file" + if [[ $? -eq 0 ]]; then + local encrypted_file="${backup_file}.gpg" + log "✅ 加密完成: $encrypted_file" + # 可选:删除未加密文件 + # rm -f "$backup_file" + else + error "❌ 加密失败" + fi + else + error "GPG未安装,无法加密" + fi +} + +# 清理旧备份 +cleanup_old_backups() { + local retention_days=$(jq -r '.backup.backup_retention_days // 30' "$CONFIG_FILE") + + log "清理超过 $retention_days 天的旧备份..." + + find "$LOCAL_BACKUP_PATH" -name "backup_*.tar.gz" -mtime "+$retention_days" -delete + find "$LOCAL_BACKUP_PATH" -name "backup_*.tar.gz.gpg" -mtime "+$retention_days" -delete + find "$LOCAL_BACKUP_PATH" -name "database_*" -type d -mtime "+$retention_days" -exec rm -rf {} + + + log "旧备份清理完成" +} + +# 发送通知 +send_notification() { + local backup_file=$(cat "${SCRIPT_DIR}/latest_backup.txt" 2>/dev/null || echo "未知") + local backup_size=$(du -h "$backup_file" 2>/dev/null | cut -f1 || echo "未知") + + local message="✅ 服务器备份完成\n时间: $(date)\n文件: $(basename "$backup_file")\n大小: $backup_size" + + # Slack通知 + local slack_webhook=$(jq -r '.notifications.slack_webhook // ""' "$CONFIG_FILE") + if [[ -n "$slack_webhook" ]]; then + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"$message\"}" "$slack_webhook" > /dev/null 2>&1 && + log "Slack通知已发送" + fi + + # 邮件通知(需要配置邮件服务器) + local email_to=$(jq -r '.notifications.email // ""' "$CONFIG_FILE") + if [[ -n "$email_to" && -f "/usr/sbin/sendmail" ]]; then + echo -e "Subject: 服务器备份完成\n\n$message" | sendmail "$email_to" + log "邮件通知已发送" + fi +} + +# 主函数 +main() { + log "🚀 启动服务器源码备份流程" + log "========================================" + + # 加载配置 + load_config + + # 检查长路径支持 + check_long_path_support + + # 执行备份 + perform_backup + + # 加密备份 + encrypt_backup + + # 清理旧备份 + cleanup_old_backups + + # 发送通知 + send_notification + + log "🎉 备份流程完成!" + log "========================================" +} + +# 异常处理 +trap 'error "脚本执行中断"; exit 1' INT TERM +trap 'cleanup_old_backups' EXIT + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/scripts/backup_restore/install_backup_system.sh b/scripts/backup_restore/install_backup_system.sh new file mode 100644 index 000000000..8409a48df --- /dev/null +++ b/scripts/backup_restore/install_backup_system.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# install_backup_system.sh - 安装备份系统 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } + +install_dependencies() { + log "安装系统依赖..." + + # 检测系统类型 + if command -v apt-get > /dev/null; then + # Debian/Ubuntu + sudo apt-get update + sudo apt-get install -y tar gzip jq gpg mysql-client postgresql-client curl + elif command -v yum > /dev/null; then + # CentOS/RHEL + sudo yum install -y tar gzip jq gnupg mysql postgresql curl + elif command -v dnf > /dev/null; then + # Fedora + sudo dnf install -y tar gzip jq gnupg mysql postgresql curl + else + log "⚠️ 无法自动安装依赖,请手动安装: tar, gzip, jq, gpg, mysql-client, postgresql-client, curl" + fi +} + +setup_directories() { + log "创建目录结构..." + + local backup_dir=$(jq -r '.storage.local_path // "/opt/backups"' "${SCRIPT_DIR}/backup_config.json") + + sudo mkdir -p "$backup_dir" + sudo chmod 755 "$backup_dir" + sudo chown "$(whoami):$(whoami)" "$backup_dir" 2>/dev/null || true + + log "备份目录: $backup_dir" +} + +setup_crontab() { + log "设置定时任务..." + + local schedule="0 2 * * *" # 每天凌晨2点 + + # 检查是否已存在定时任务 + if crontab -l 2>/dev/null | grep -q "backup_server.sh"; then + log "⚠️ 定时任务已存在,跳过" + return 0 + fi + + # 添加定时任务 + (crontab -l 2>/dev/null; echo "$schedule ${SCRIPT_DIR}/backup_server.sh") | crontab - + + log "✅ 定时任务已添加: $schedule" +} + +generate_ssh_key() { + log "生成SSH密钥(用于远程备份)..." + + local ssh_dir="${SCRIPT_DIR}/.ssh" + mkdir -p "$ssh_dir" + chmod 700 "$ssh_dir" + + if [[ ! -f "${ssh_dir}/id_rsa" ]]; then + ssh-keygen -t rsa -b 4096 -f "${ssh_dir}/id_rsa" -N "" -q + log "✅ SSH密钥已生成: ${ssh_dir}/id_rsa.pub" + else + log "⚠️ SSH密钥已存在" + fi +} + +main() { + log "🚀 开始安装备份系统" + echo "========================================" + + # 检查配置文件 + if [[ ! -f "${SCRIPT_DIR}/backup_config.json" ]]; then + log "❌ 配置文件 backup_config.json 不存在" + exit 1 + fi + + # 安装依赖 + install_dependencies + + # 设置目录 + setup_directories + + # 设置定时任务 + setup_crontab + + # 生成SSH密钥 + generate_ssh_key + + # 设置脚本权限 + chmod +x "${SCRIPT_DIR}/backup_server.sh" + chmod +x "${SCRIPT_DIR}/restore_server.sh" + chmod +x "${SCRIPT_DIR}/verify_backup.sh" + + log "🎉 备份系统安装完成!" + echo "" + echo "📋 下一步操作:" + echo " 1. 编辑 backup_config.json 配置备份参数" + echo " 2. 编辑 .env 文件设置环境变量" + echo " 3. 测试备份: ${SCRIPT_DIR}/backup_server.sh" + echo " 4. 查看定时任务: crontab -l" + echo "" + echo "💡 提示: 确保配置中的目录路径有读写权限" +} + +main "$@" \ No newline at end of file diff --git a/scripts/backup_restore/readme.md b/scripts/backup_restore/readme.md new file mode 100644 index 000000000..92923eb43 --- /dev/null +++ b/scripts/backup_restore/readme.md @@ -0,0 +1,10 @@ +# 备份还原脚本 + + +## 功能 + +- 备份服务器源码 +- 还原服务器源码 +- 支持长文件路径 +- 支持根据配置文件备份 +- 支持根据配置文件还原 diff --git a/scripts/backup_restore/restore_server.sh b/scripts/backup_restore/restore_server.sh new file mode 100644 index 000000000..de856a531 --- /dev/null +++ b/scripts/backup_restore/restore_server.sh @@ -0,0 +1,275 @@ +#!/bin/bash +# restore_server.sh - 支持长路径和配置文件的还原脚本 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/backup_config.json" +RESTORE_LOG="${SCRIPT_DIR}/restore_$(date +%Y%m%d_%H%M%S).log" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$RESTORE_LOG"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$RESTORE_LOG"; } +error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$RESTORE_LOG"; } +info() { echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$RESTORE_LOG"; } + +# 显示可用备份 +list_backups() { + local backup_dir=$(jq -r '.storage.local_path // "/backups"' "$CONFIG_FILE") + + if [[ ! -d "$backup_dir" ]]; then + error "备份目录不存在: $backup_dir" + return 1 + fi + + log "可用的备份文件:" + find "$backup_dir" -name "backup_*.tar.gz" -o -name "backup_*.tar.gz.gpg" | sort | while read file; do + local size=$(du -h "$file" | cut -f1) + local date=$(stat -c %y "$file" | cut -d' ' -f1) + echo " 📦 $file ($size, $date)" + done +} + +# 解密备份文件 +decrypt_backup() { + local encrypted_file="$1" + local decrypted_file="${encrypted_file%.gpg}" + + local private_key=$(jq -r '.encryption.private_key // ""' "$CONFIG_FILE") + if [[ -z "$private_key" || ! -f "$private_key" ]]; then + error "解密私钥不存在: $private_key" + return 1 + fi + + log "解密备份文件: $encrypted_file" + + if command -v gpg > /dev/null; then + gpg --decrypt --pinentry-mode loopback --passphrase-file <(echo "$GPG_PASSPHRASE") \ + --output "$decrypted_file" "$encrypted_file" + + if [[ $? -eq 0 ]]; then + log "✅ 解密完成: $decrypted_file" + echo "$decrypted_file" + else + error "❌ 解密失败" + return 1 + fi + else + error "GPG未安装,无法解密" + return 1 + fi +} + +# 验证备份文件 +verify_backup() { + local backup_file="$1" + + log "验证备份文件: $backup_file" + + if [[ ! -f "$backup_file" ]]; then + error "备份文件不存在: $backup_file" + return 1 + fi + + # 检查文件完整性 + if tar -tzf "$backup_file" > /dev/null 2>&1; then + log "✅ 备份文件完整性验证通过" + return 0 + else + error "❌ 备份文件损坏或格式错误" + return 1 + fi +} + +# 还原数据库 +restore_databases() { + local backup_dir="$1" + local timestamp=$(basename "$backup_dir" | sed 's/database_//') + + if [[ ! -d "$backup_dir" ]]; then + warn "未找到数据库备份目录: $backup_dir" + return 0 + fi + + log "开始还原数据库..." + + # MySQL还原 + for sql_file in "$backup_dir"/*_mysql.sql; do + if [[ -f "$sql_file" ]]; then + local db_name=$(basename "$sql_file" | sed 's/_mysql.sql//') + info "还原MySQL数据库: $db_name" + + local mysql_config=$(jq -r '.database.mysql // {}' "$CONFIG_FILE") + local mysql_host=$(jq -r '.host // "localhost"' <<< "$mysql_config") + local mysql_user=$(jq -r '.user // "root"' <<< "$mysql_config") + local mysql_password=$(jq -r '.password // ""' <<< "$mysql_config") + + if [[ -n "$mysql_password" ]]; then + mysql -h "$mysql_host" -u "$mysql_user" -p"$mysql_password" "$db_name" < "$sql_file" + else + mysql -h "$mysql_host" -u "$mysql_user" "$db_name" < "$sql_file" + fi + fi + done + + # PostgreSQL还原 + for sql_file in "$backup_dir"/*_pgsql.sql; do + if [[ -f "$sql_file" ]]; then + local db_name=$(basename "$sql_file" | sed 's/_pgsql.sql//') + info "还原PostgreSQL数据库: $db_name" + + local pgsql_config=$(jq -r '.database.postgresql // {}' "$CONFIG_FILE") + local pgsql_host=$(jq -r '.host // "localhost"' <<< "$pgsql_config") + local pgsql_user=$(jq -r '.user // "postgres"' <<< "$pgsql_config") + + PGPASSWORD=$(jq -r '.password // ""' <<< "$pgsql_config") \ + psql -h "$pgsql_host" -U "$pgsql_user" -d "$db_name" -f "$sql_file" + fi + done + + log "数据库还原完成" +} + +# 主还原函数 +perform_restore() { + local backup_file="$1" + local restore_path="$2" + local decrypt="$3" + + # 解密备份文件(如果需要) + if [[ "$decrypt" == "true" && "$backup_file" == *.gpg ]]; then + backup_file=$(decrypt_backup "$backup_file") + if [[ $? -ne 0 ]]; then + return 1 + fi + fi + + # 验证备份文件 + verify_backup "$backup_file" || return 1 + + # 创建还原目录 + mkdir -p "$restore_path" + + log "开始还原到: $restore_path" + + # 检查长路径支持 + local long_path_support=$(jq -r '.backup.long_path_support // "true"' "$CONFIG_FILE") + local tar_options="" + if [[ "$long_path_support" == "true" ]]; then + tar_options="--force-local" + fi + + # 执行还原 + if tar $tar_options -xzf "$backup_file" -C "$restore_path"; then + log "✅ 文件还原成功" + + # 还原数据库 + local backup_dir=$(dirname "$backup_file") + local timestamp=$(basename "$backup_file" | sed 's/backup_//' | sed 's/\.tar\.gz\(\.gpg\)*//') + local db_backup_dir="${backup_dir}/database_${timestamp}" + + restore_databases "$db_backup_dir" + + return 0 + else + error "❌ 文件还原失败" + return 1 + fi +} + +# 交互式还原 +interactive_restore() { + log "🔍 交互式还原模式" + echo "========================================" + + # 显示可用备份 + list_backups + + echo "" + read -p "📥 请输入要还原的备份文件路径: " backup_file + + if [[ ! -f "$backup_file" ]]; then + error "文件不存在: $backup_file" + return 1 + fi + + # 询问还原路径 + read -p "📁 请输入还原路径 [默认: /tmp/restore]: " restore_path + restore_path=${restore_path:-/tmp/restore} + + # 询问是否需要解密 + local decrypt="false" + if [[ "$backup_file" == *.gpg ]]; then + read -p "🔐 是否需要解密? (y/N): " need_decrypt + if [[ "$need_decrypt" == "y" || "$need_decrypt" == "Y" ]]; then + decrypt="true" + fi + fi + + # 确认操作 + echo "" + echo "📋 还原配置:" + echo " 备份文件: $backup_file" + echo " 还原路径: $restore_path" + echo " 解密操作: $decrypt" + echo "" + + read -p "⚠️ 确认开始还原? (y/N): " confirm + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + log "操作取消" + return 0 + fi + + # 执行还原 + perform_restore "$backup_file" "$restore_path" "$decrypt" +} + +# 主函数 +main() { + local mode="${1:-interactive}" + + log "🔄 启动服务器源码还原流程" + log "========================================" + + if [[ ! -f "$CONFIG_FILE" ]]; then + error "配置文件不存在: $CONFIG_FILE" + exit 1 + fi + + case "$mode" in + "interactive") + interactive_restore + ;; + "auto") + local backup_file="${2:-}" + local restore_path="${3:-/tmp/restore}" + if [[ -z "$backup_file" ]]; then + error "自动模式需要指定备份文件路径" + exit 1 + fi + perform_restore "$backup_file" "$restore_path" "false" + ;; + "list") + list_backups + ;; + *) + echo "用法: $0 [interactive|auto|list] [backup_file] [restore_path]" + echo " interactive - 交互式还原" + echo " auto - 自动还原" + echo " list - 列出备份文件" + exit 1 + ;; + esac + + log "🎉 还原流程完成!" + log "========================================" +} + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/scripts/backup_restore/verify_backup.sh b/scripts/backup_restore/verify_backup.sh new file mode 100644 index 000000000..4d8a758ec --- /dev/null +++ b/scripts/backup_restore/verify_backup.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# verify_backup.sh - 验证备份完整性 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/backup_config.json" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } + +verify_backup_integrity() { + local backup_file="$1" + + log "验证备份完整性: $backup_file" + + # 检查文件是否存在 + if [[ ! -f "$backup_file" ]]; then + echo "❌ 备份文件不存在" + return 1 + fi + + # 检查文件大小 + local file_size=$(stat -c%s "$backup_file") + if [[ $file_size -eq 0 ]]; then + echo "❌ 备份文件为空" + return 1 + fi + + echo "✅ 文件大小: $(numfmt --to=iec-i --suffix=B $file_size)" + + # 检查tar归档完整性 + if tar -tzf "$backup_file" > /dev/null 2>&1; then + echo "✅ 归档完整性验证通过" + + # 统计文件数量 + local file_count=$(tar -tzf "$backup_file" | wc -l) + echo "✅ 包含文件数: $file_count" + + # 检查长路径文件 + local long_path_files=$(tar -tzf "$backup_file" | grep -c '/.\{100,\}') + if [[ $long_path_files -gt 0 ]]; then + echo "📁 长路径文件数: $long_path_files" + fi + + return 0 + else + echo "❌ 归档损坏或格式错误" + return 1 + fi +} + +# 主函数 +main() { + local backup_file="${1:-}" + + if [[ -z "$backup_file" ]]; then + # 使用最新备份 + local backup_dir=$(jq -r '.storage.local_path // "/backups"' "$CONFIG_FILE") + backup_file=$(find "$backup_dir" -name "backup_*.tar.gz" -o -name "backup_*.tar.gz.gpg" | sort | tail -1) + + if [[ -z "$backup_file" ]]; then + echo "❌ 未找到备份文件" + exit 1 + fi + fi + + echo "🔍 备份验证报告" + echo "========================================" + echo "备份文件: $backup_file" + echo "验证时间: $(date)" + echo "----------------------------------------" + + if verify_backup_integrity "$backup_file"; then + echo "✅ 备份验证通过" + exit 0 + else + echo "❌ 备份验证失败" + exit 1 + fi +} + +main "$@" \ No newline at end of file diff --git a/scripts/deploy.php b/scripts/deploy.php index 1d9ed1922..00bee5e56 100644 --- a/scripts/deploy.php +++ b/scripts/deploy.php @@ -75,12 +75,8 @@ class AutoDeployer $zipFiles = $this->analyzeZipStructure($zip); $this->logMessage('INFO', "ZIP包中包含 {$zipFiles['fileCount']} 个文件"); - // 分析目标目录结构 - $targetFiles = $this->analyzeTargetDirectory(); - $this->logMessage('INFO', "目标目录中包含 {$targetFiles['fileCount']} 个文件"); - - // 比较差异并部署 - $result = $this->compareAndDeploy($zip, $zipFiles, $targetFiles); + // 基于ZIP文件结构进行部署,不再预先分析目标目录 + $result = $this->compareAndDeploy($zip, $zipFiles); // 关闭ZIP文件 $zip->close(); @@ -136,49 +132,25 @@ class AutoDeployer } /** - * 分析目标目录结构 + * 获取单个文件信息 */ - private function analyzeTargetDirectory(): array + private function getFileInfo(string $filePath): ?array { - $files = []; - $fileCount = 0; - $totalSize = 0; - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( - $this->targetDir, - RecursiveDirectoryIterator::SKIP_DOTS - ), - RecursiveIteratorIterator::SELF_FIRST - ); - - foreach ($iterator as $file) { - if ($file->isDir()) { - continue; - } - - $relativePath = $this->getRelativePath($file->getPathname(), $this->targetDir); - $files[$relativePath] = [ - 'size' => $file->getSize(), - 'modified' => $file->getMTime(), - 'permissions' => $file->getPerms() - ]; - - $fileCount++; - $totalSize += $file->getSize(); + if (!file_exists($filePath)) { + return null; } return [ - 'files' => $files, - 'fileCount' => $fileCount, - 'totalSize' => $totalSize + 'size' => filesize($filePath), + 'modified' => filemtime($filePath), + 'permissions' => fileperms($filePath) ]; } /** * 比较差异并部署文件 */ - private function compareAndDeploy(ZipArchive $zip, array $zipFiles, array $targetFiles): array + private function compareAndDeploy(ZipArchive $zip, array $zipFiles): array { $results = [ 'added' => 0, @@ -190,10 +162,10 @@ class AutoDeployer foreach ($zipFiles['files'] as $filePath => $zipFileInfo) { $targetFilePath = $this->targetDir . DIRECTORY_SEPARATOR . $filePath; - $targetFileExists = isset($targetFiles['files'][$filePath]); try { - if (!$targetFileExists) { + // 直接检查文件是否存在,不再依赖预先分析的目标目录结构 + if (!file_exists($targetFilePath)) { // 新文件 - 需要创建目录并复制文件 $result = $this->deployNewFile($zip, $filePath, $targetFilePath); $results['added']++; @@ -204,7 +176,8 @@ class AutoDeployer ]; } else { // 文件已存在 - 检查是否需要更新 - $needsUpdate = $this->needsUpdate($zip, $filePath, $targetFiles['files'][$filePath]); + $targetFileInfo = $this->getFileInfo($targetFilePath); + $needsUpdate = $this->needsUpdate($zip, $filePath, $targetFileInfo); if ($needsUpdate) { $result = $this->deployUpdatedFile($zip, $filePath, $targetFilePath); @@ -313,8 +286,13 @@ class AutoDeployer /** * 检查文件是否需要更新 */ - private function needsUpdate(ZipArchive $zip, string $filePath, array $targetFileInfo): bool + private function needsUpdate(ZipArchive $zip, string $filePath, ?array $targetFileInfo): bool { + // 如果目标文件信息不存在,返回false(这种情况实际上不会发生,因为前面已经检查了文件存在) + if ($targetFileInfo === null) { + return false; + } + $zipFileInfo = $zip->statName($filePath); if ($zipFileInfo === false) { return false; @@ -333,7 +311,8 @@ class AutoDeployer } // 比较CRC32校验(最可靠的方法) - $targetCrc = hash_file('crc32b', $this->targetDir . DIRECTORY_SEPARATOR . $filePath); + $targetFilePath = $this->targetDir . DIRECTORY_SEPARATOR . $filePath; + $targetCrc = hash_file('crc32b', $targetFilePath); $zipCrc = sprintf('%08x', $zipFileInfo['crc']); if (strtolower($targetCrc) !== strtolower($zipCrc)) { diff --git a/scripts/patch_tools/docker-compose.yml b/scripts/patch_tools/docker-compose.yml new file mode 100644 index 000000000..12c69ec51 --- /dev/null +++ b/scripts/patch_tools/docker-compose.yml @@ -0,0 +1,16 @@ +# TODO: 创建一个基于alpine的docker镜像,用于运行patch工具 +# 镜像名称:patch-tools-alpine +# 基础镜像:alpine:3.18 +# 安装依赖:bash, tar, gzip, zip, diffutils, patch +# 复制脚本:patch_config.sh, patch.sh +# 入口点:/bin/bash /patch.sh + +services: + patch-tools: + image: ubuntu/squid:latest + container_name: patch-tools-alpine + volumes: + - ./:/working_dir/scripts + - ../patch-dir:/working_dir/patchs + - ../../ftp-src/app:/working_dir/old-shop-src + - ../../src/app:/working_dir/new-shop-src diff --git a/scripts/patch_tools/install_patch_system.sh b/scripts/patch_tools/install_patch_system.sh new file mode 100644 index 000000000..88ae022ab --- /dev/null +++ b/scripts/patch_tools/install_patch_system.sh @@ -0,0 +1,223 @@ +#!/bin/bash +# install_patch_system.sh - 补丁管理系统安装脚本 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="/opt/patch-management" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } +info() { log "INFO: $1"; } +error() { log "ERROR: $1"; exit 1; } +warn() { log "WARNING: $1"; } + +# 检测是否在Docker容器中运行 +is_docker_environment() { + # 检查多种Docker环境标识 + if [[ -f /.dockerenv ]]; then + return 0 + fi + + if grep -q docker /proc/1/cgroup 2>/dev/null; then + return 0 + fi + + if grep -q lxc /proc/1/cgroup 2>/dev/null; then + return 0 + fi + + if [[ -n "${container:-}" ]]; then + return 0 + fi + + if [[ -n "${DOCKER_CONTAINER:-}" ]]; then + return 0 + fi + + # 检查容器相关的环境变量组合 + local env_vars=( + "HOSTNAME" + "HOME" + "USER" + ) + + for var in "${env_vars[@]}"; do + if [[ -n "${!var:-}" ]] && [[ "${!var}" =~ ^[a-f0-9]{12,}$ ]]; then + # 检查环境变量值是否像容器ID + if [[ ${!var} =~ ^[a-f0-9]{12,64}$ ]]; then + return 0 + fi + fi + done + + return 1 +} + +# 获取适当的命令前缀(在Docker中使用空前缀,否则使用sudo) +get_cmd_prefix() { + if is_docker_environment; then + echo "" + else + echo "sudo" + fi +} + +install_dependencies() { + info "安装系统依赖..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + if command -v apt-get >/dev/null 2>&1; then + # Debian/Ubuntu + $sudo_prefix apt-get update + $sudo_prefix apt-get install -y tar gzip jq coreutils findutils util-linux bc + elif command -v yum >/dev/null 2>&1; then + # CentOS/RHEL + $sudo_prefix yum install -y tar gzip jq coreutils findutils util-linux bc + else + warn "无法自动安装依赖,请手动安装: tar, gzip, jq, coreutils, findutils, util-linux, bc" + fi + + # 安装GPG(用于签名验证) + if command -v apt-get >/dev/null 2>&1; then + $sudo_prefix apt-get install -y gnupg + elif command -v yum >/dev/null 2>&1; then + $sudo_prefix yum install -y gnupg + fi +} + +create_directories() { + info "创建目录结构..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + $sudo_prefix mkdir -p "$INSTALL_DIR" + $sudo_prefix mkdir -p "/var/backups/patch" + $sudo_prefix mkdir -p "/var/log/patch_system" + $sudo_prefix mkdir -p "/etc/patch/keys" + + # 设置权限 + $sudo_prefix chmod 755 "$INSTALL_DIR" + $sudo_prefix chmod 755 "/var/backups/patch" + $sudo_prefix chmod 755 "/var/log/patch_system" + $sudo_prefix chmod 700 "/etc/patch/keys" +} + +install_scripts() { + info "安装脚本文件..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + # 复制脚本文件, 存在则覆盖 + $sudo_prefix cp --force "$SCRIPT_DIR/patch_generator.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_fnc.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_applier.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_rollback.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_verifier.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_workflow.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_config.sh" "$INSTALL_DIR/" + + info "脚本文件已安装在: $INSTALL_DIR/" + + # 设置执行权限 + $sudo_prefix chmod +x "$INSTALL_DIR"/*.sh + + # 创建符号链接 + $sudo_prefix ln -sf "$INSTALL_DIR/patch_workflow.sh" "/usr/local/bin/patch-mgmt" + info "符号链接已创建: /usr/local/bin/patch-mgmt" +} + +setup_cron() { + info "设置定时任务..." + + if is_docker_environment; then + info "Docker环境,跳过系统定时任务设置" + info "如需定时任务,请考虑使用宿主机的crontab或Docker运行参数" + return 0 + fi + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + local cron_job="0 2 * * * $INSTALL_DIR/patch_verifier.sh /opt/patches batch > /var/log/patch_system/batch_verify.log 2>&1" + + if ! crontab -l 2>/dev/null | grep -q "patch_verifier.sh"; then + (crontab -l 2>/dev/null; echo "$cron_job") | crontab - + info "定时任务已添加" + else + info "定时任务已存在" + fi +} + +generate_keys() { + info "生成签名密钥..." + + local key_dir="/etc/patch/keys" + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + if [[ ! -f "$key_dir/private.pem" ]]; then + $sudo_prefix mkdir -p "$key_dir" + + # 生成RSA密钥对 + openssl genrsa -out "$key_dir/private.pem" 4096 + openssl rsa -in "$key_dir/private.pem" -pubout -out "$key_dir/public.pem" + + $sudo_prefix chmod 600 "$key_dir/private.pem" + $sudo_prefix chmod 644 "$key_dir/public.pem" + + info "密钥对已生成: $key_dir/" + else + info "密钥对已存在" + fi +} + +main() { + info "开始安装企业级补丁管理系统" + echo "========================================" + + # 检查运行环境 + if is_docker_environment; then + info "检测到Docker容器环境,将以root权限执行(不适用sudo)" + SUDO_CMD="" + else + info "检测到主机环境,将使用sudo执行管理员操作" + SUDO_CMD="sudo" + + # 在非Docker环境中,建议非root用户运行 + if [[ $EUID -eq 0 ]]; then + warn "检测到以root用户运行,建议在非Docker环境中使用具备sudo权限的普通用户" + fi + fi + + # 安装依赖 + install_dependencies + + # 创建目录 + create_directories + + # 安装脚本 + install_scripts + + # 设置定时任务 + setup_cron + + # 生成密钥 + generate_keys + + info "🎉 补丁管理系统安装完成!" + echo "" + echo "📁 安装目录: $INSTALL_DIR" + echo "🔧 使用命令: patch-mgmt" + echo "📋 配置文件: $INSTALL_DIR/patch_config.sh" + echo "" + echo "💡 下一步操作:" + echo " 1. 编辑配置文件: $INSTALL_DIR/patch_config.sh" + echo " 2. 测试系统: patch-mgmt --help" + echo " 3. 配置通知: 修改SLACK_WEBHOOK等设置" +} + +main "$@" \ No newline at end of file diff --git a/scripts/patch_tools/patch_applier.sh b/scripts/patch_tools/patch_applier.sh new file mode 100644 index 000000000..ae704e937 --- /dev/null +++ b/scripts/patch_tools/patch_applier.sh @@ -0,0 +1,587 @@ +#!/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" + +# 颜色定义 +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_$$"} +} + +setup_environment() { + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$BACKUP_DIR" + mkdir -p "$TEMP_DIR" + + info "日志文件: $LOG_FILE" + info "备份目录: $BACKUP_DIR" + info "临时目录: $TEMP_DIR" +} + +cleanup() { + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + info "临时目录已清理: $TEMP_DIR" + fi +} + +trap cleanup EXIT + +# 验证函数 +verify_package() { + local package_path="$1" + + info "开始验证补丁包: $package_path" + + # 检查文件存在性 + if [[ ! -f "$package_path" ]]; then + error "补丁包不存在: $package_path" + return 1 + fi + + # 检查文件大小 + local size=$(stat -c%s "$package_path" 2>/dev/null || echo "0") + if [[ $size -eq 0 ]]; then + error "补丁包为空: $package_path" + return 1 + fi + + # 验证校验和 + if [[ -f "${package_path}.sha256" ]]; then + info "验证校验和..." + if sha256sum -c "${package_path}.sha256" >/dev/null 2>&1; then + info "✅ 校验和验证通过" + else + error "❌ 校验和验证失败" + return 1 + fi + fi + + # 验证签名 + if [[ -f "${package_path}.sig" ]]; then + info "验证签名..." + if command -v gpg >/dev/null 2>&1; then + if gpg --verify "${package_path}.sig" "$package_path" >/dev/null 2>&1; then + info "✅ 签名验证通过" + else + error "❌ 签名验证失败" + return 1 + fi + else + warn "GPG未安装,跳过签名验证" + fi + fi + + # 验证压缩包完整性 + if ! tar -tzf "$package_path" >/dev/null 2>&1; then + error "❌ 压缩包损坏或格式错误" + return 1 + fi + + info "✅ 补丁包验证通过" + return 0 +} + +# 备份函数 +create_backup() { + local backup_name="backup_$(date +%Y%m%d_%H%M%S)" + local backup_path="$BACKUP_DIR/$backup_name.tar.gz" + + info "创建应用前备份: $backup_path" + + # 获取需要备份的文件列表 + local files_to_backup=() + while IFS='|' read -r change_type path old_info new_info; do + case "$change_type" in + "MODIFIED"|"DELETED") + local target_file="$path" + if [[ -f "$target_file" ]]; then + files_to_backup+=("$target_file") + fi + ;; + esac + done < "$TEMP_DIR/changes.txt" + + if [[ ${#files_to_backup[@]} -eq 0 ]]; then + info "无需备份文件" + return 0 + fi + + # 创建备份 + tar -czf "$backup_path" "${files_to_backup[@]}" + + local backup_size=$(du -h "$backup_path" | cut -f1) + info "✅ 备份创建完成: $backup_path ($backup_size)" + echo "$backup_path" +} + +# 补丁应用函数 +extract_package() { + local package_path="$1" + local extract_dir="$2" + + info "解压补丁包到: $extract_dir" + + mkdir -p "$extract_dir" + + if tar -xzf "$package_path" -C "$extract_dir"; then + info "✅ 补丁包解压完成" + return 0 + else + error "❌ 补丁包解压失败" + return 1 + fi +} + +apply_file_changes() { + local patch_dir="$1" + + info "开始应用文件变更..." + + local applied_count=0 + local failed_count=0 + + # 读取变更清单 + local manifest_file="$patch_dir/MANIFEST.MF" + if [[ ! -f "$manifest_file" ]]; then + error "清单文件不存在: $manifest_file" + return 1 + fi + + # 处理每个变更 + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED"|"MODIFIED") + apply_file_addition_modification "$patch_dir" "$path" "$change_type" + result=$? + ;; + "DELETED") + apply_file_deletion "$path" + result=$? + ;; + *) + warn "未知变更类型: $change_type" + result=1 + ;; + esac + + if [[ $result -eq 0 ]]; then + ((applied_count++)) + else + ((failed_count++)) + fi + done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") + + info "文件变更应用完成: $applied_count 成功, $failed_count 失败" + + if [[ $failed_count -gt 0 ]]; then + return 1 + fi + return 0 +} + +apply_file_addition_modification() { + local patch_dir="$1" + local file_path="$2" + local change_type="$3" + + local source_file="$patch_dir/files/$file_path" + local target_file="/$file_path" # 绝对路径 + local target_dir=$(dirname "$target_file") + + # 检查源文件是否存在 + if [[ ! -f "$source_file" ]]; then + error "源文件不存在: $source_file" + return 1 + fi + + # 创建目标目录 + if [[ ! -d "$target_dir" ]]; then + mkdir -p "$target_dir" + debug "创建目录: $target_dir" + fi + + # 备份原文件(如果是修改操作) + if [[ "$change_type" == "MODIFIED" ]] && [[ -f "$target_file" ]]; then + local backup_file="$TEMP_DIR/backup/$file_path" + mkdir -p "$(dirname "$backup_file")" + cp -p "$target_file" "$backup_file" + debug "备份原文件: $target_file -> $backup_file" + fi + + # 复制文件 + if cp -p "$source_file" "$target_file"; then + # 设置权限(从清单中读取) + set_file_permissions "$target_file" "$change_type" + info "✅ 应用文件: $file_path ($change_type)" + return 0 + else + error "❌ 文件应用失败: $file_path" + return 1 + fi +} + +apply_file_deletion() { + local file_path="$1" + local target_file="/$file_path" + + if [[ ! -f "$target_file" ]]; then + warn "文件不存在,无需删除: $target_file" + return 0 + fi + + # 备份文件 + local backup_file="$TEMP_DIR/backup/$file_path" + mkdir -p "$(dirname "$backup_file")" + cp -p "$target_file" "$backup_file" + + # 删除文件 + if rm -f "$target_file"; then + info "✅ 删除文件: $file_path" + + # 尝试删除空目录 + local parent_dir=$(dirname "$target_file") + while [[ "$parent_dir" != "/" ]] && rmdir "$parent_dir" 2>/dev/null; do + debug "删除空目录: $parent_dir" + parent_dir=$(dirname "$parent_dir") + done + + return 0 + else + error "❌ 文件删除失败: $file_path" + return 1 + fi +} + +set_file_permissions() { + local file_path="$1" + local change_type="$2" + + # 根据文件类型设置权限 + case "$file_path" in + *.sh|*.bash) + chmod 755 "$file_path" + ;; + *.php|*.py|*.pl) + chmod 644 "$file_path" + ;; + /etc/*|/var/www/*) + chmod 644 "$file_path" + ;; + *) + chmod 644 "$file_path" + ;; + esac + + debug "设置权限: $file_path -> $(stat -c %a "$file_path")" +} + +# 验证应用结果 +verify_application() { + local patch_dir="$1" + + info "开始验证应用结果..." + + local manifest_file="$patch_dir/MANIFEST.MF" + local verification_passed=true + + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED"|"MODIFIED") + if ! verify_file_application "$path" "$change_type" "$patch_dir"; then + verification_passed=false + fi + ;; + "DELETED") + if ! verify_file_deletion "$path"; then + verification_passed=false + fi + ;; + esac + done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") + + if $verification_passed; then + info "✅ 应用验证通过" + return 0 + else + error "❌ 应用验证失败" + return 1 + fi +} + +verify_file_application() { + local file_path="$1" + local change_type="$2" + local patch_dir="$3" + + local target_file="/$file_path" + local source_file="$patch_dir/files/$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" + 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_path="$1" + local backup_path="$2" + + local rollback_info_file="$BACKUP_DIR/rollback_info.json" + local patch_name=$(basename "$patch_path") + local timestamp=$(date +%Y%m%d_%H%M%S) + + local rollback_info=$(cat << EOF +{ + "patch_name": "$patch_name", + "apply_time": "$(date -Iseconds)", + "backup_path": "$backup_path", + "rollback_path": "$BACKUP_DIR/rollback_${timestamp}.tar.gz", + "status": "applied" +} +EOF +) + + echo "$rollback_info" > "$rollback_info_file" + info "回滚点创建完成: $rollback_info_file" +} + +# 通知函数 +send_application_notification() { + local status="$1" + local patch_path="$2" + local message="$3" + + if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then + return 0 + fi + + case "$status" in + "start") + local subject="补丁应用开始" + local color="#3498db" + ;; + "success") + local subject="补丁应用成功" + local color="#2ecc71" + ;; + "failure") + local subject="补丁应用失败" + local color="#e74c3c" + ;; + *) + local subject="补丁应用通知" + local color="#95a5a6" + ;; + esac + + # Slack通知 + if [[ -n "$SLACK_WEBHOOK" ]]; then + send_slack_notification "$subject" "$message" "$color" + fi +} + +send_slack_notification() { + local subject="$1" + local message="$2" + local color="$3" + + local payload=$(cat << EOF +{ + "attachments": [ + { + "color": "$color", + "title": "$subject", + "text": "$message", + "fields": [ + { + "title": "补丁文件", + "value": "$(basename "$PATCH_PATH")", + "short": true + }, + { + "title": "应用时间", + "value": "$(date)", + "short": true + }, + { + "title": "应用主机", + "value": "$(hostname)", + "short": true + } + ] + } + ] +} +EOF +) + + if command -v curl >/dev/null 2>&1; then + curl -s -X POST -H 'Content-type: application/json' \ + --data "$payload" "$SLACK_WEBHOOK" >/dev/null && \ + info "Slack通知发送成功" + fi +} + +# 主应用函数 +apply_patch() { + local patch_path="$1" + local dry_run="${2:-false}" + + info "开始应用补丁包: $patch_path" + send_application_notification "start" "$patch_path" "开始应用补丁" + + # 验证补丁包 + if ! verify_package "$patch_path"; then + error "补丁包验证失败" + send_application_notification "failure" "$patch_path" "补丁包验证失败" + return 1 + fi + + # 解压补丁包 + local extract_dir="$TEMP_DIR/extract" + if ! extract_package "$patch_path" "$extract_dir"; then + send_application_notification "failure" "$patch_path" "补丁包解压失败" + return 1 + fi + + # 如果是干跑模式,只验证不应用 + if [[ "$dry_run" == "true" ]]; then + info "干跑模式: 只验证不应用" + verify_application "$extract_dir" + return 0 + fi + + # 创建备份 + local backup_path=$(create_backup) + + # 应用变更 + if ! apply_file_changes "$extract_dir"; then + error "文件变更应用失败" + send_application_notification "failure" "$patch_path" "文件应用失败" + return 1 + fi + + # 验证应用结果 + if ! verify_application "$extract_dir"; then + error "应用验证失败" + send_application_notification "failure" "$patch_path" "应用验证失败" + return 1 + fi + + # 创建回滚点 + create_rollback_point "$patch_path" "$backup_path" + + info "✅ 补丁应用完成" + send_application_notification "success" "$patch_path" "补丁应用成功" + return 0 +} + +# 主函数 +main() { + local patch_path="${1:-}" + local dry_run="${2:-false}" + + if [[ -z "$patch_path" ]]; then + echo "用法: $0 <补丁包路径> [dry-run]" + echo "示例:" + echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz" + echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz dry-run" + exit 1 + fi + + # 加载配置 + load_config + + # 设置环境 + setup_environment + + # 应用补丁 + if apply_patch "$patch_path" "$dry_run"; then + info "🎉 补丁应用成功完成" + exit 0 + else + error "💥 补丁应用失败" + exit 1 + fi +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main "$@" \ No newline at end of file diff --git a/scripts/patch_tools/patch_config.sh b/scripts/patch_tools/patch_config.sh new file mode 100644 index 000000000..9646c6822 --- /dev/null +++ b/scripts/patch_tools/patch_config.sh @@ -0,0 +1,177 @@ +#!/bin/bash +# patch_config.sh - 企业级补丁配置 + +# ============================================================================== +# 基础配置 - 定义补丁的基本信息 +# ============================================================================== + +# 基础配置 +PATCH_NAME="security-hotfix-2025" +PATCH_VERSION="1.0.0" +PATCH_DESCRIPTION="紧急安全漏洞修复" +PATCH_AUTHOR="企业DevOps团队" +PATCH_EMAIL="devops@aigc-quickapp.com" + +# ============================================================================== +# 文件筛选配置 - 定义哪些文件需要被包含或排除 +# ============================================================================== + +## 包含的文件模式 +INCLUDE_PATTERNS=( + ".+/.*" # 匹配所有子目录下的文件 +) + +## 排除的文件模式 +EXCLUDE_PATTERNS=( + "*.log" + "*.tmp" + "*.bak" + "*.swp" + "*.swx" + "^.github/" + "/node_modules/*" + "/__pycache__/*" + # 排除以下一级文件夹 + "^.git/" + "^cache/" + "^temp/" + "^tmp/" + "^logs/" + "^runtime/" + "^uploads/" + "^attachment/" + "^h5/" + "^hwapp/" + ".DS_Store" + "Thumbs.db" +) + +# ============================================================================== +# 文件大小限制 - 定义补丁处理的文件大小范围 +# ============================================================================== + +# 文件大小限制 +MAX_FILE_SIZE="20MB" # 最大文件大小 +MIN_FILE_SIZE="0KB" # 最小文件大小 + +# ============================================================================== +# 时间过滤 - 定义补丁处理的文件时间范围 +# ============================================================================== + +# 时间过滤 +MODIFIED_AFTER="2000-01-01T00:00:00Z" # 仅包含修改时间在该时间之后的文件 +CREATED_AFTER="2000-01-01T00:00:00Z" # 仅包含创建时间在该时间之后的文件 + +# ============================================================================== +# 比较方法配置 - 定义补丁处理的比较方式 +# ============================================================================== + +# 比较方法配置 +# 比较内容和时间 +COMPARISON_METHOD="content" # 比较方法: content, time, both +HASH_ALGORITHM="sha256" # 哈希算法,用于文件内容比较 +TIME_PRECISION="second" # 时间精度,用于文件时间比较 +COMPARE_PERMISSIONS=false # 是否比较文件权限 +COMPARE_OWNERSHIP=false # 是否比较文件所有者 + +# ============================================================================== +# 增量配置 - 定义是否启用增量补丁 +# ============================================================================== + +# 增量配置 +INCREMENTAL_ENABLED=true # 是否启用增量补丁 +BASE_VERSION="1.0.0" # 基础版本,用于增量比较 +DETECT_RENAMES=true # 是否检测文件重命名 +BINARY_DIFF=true # 是否启用二进制差异比较 +CHUNK_SIZE="8KB" # 二进制差异比较的块大小 + + + +# ============================================================================== +# 压缩配置 - 定义补丁压缩方式和级别 +# ============================================================================== + +# 压缩配置 +COMPRESSION_ENABLED=true # 是否启用压缩 +COMPRESSION_ALGORITHM="gzip" # gzip, bzip2, xz +COMPRESSION_LEVEL=6 # 压缩级别,1-9 +PER_FILE_OPTIMIZATION=true # 是否对每个文件单独压缩 + +# ============================================================================== +# 安全配置 - 定义补丁签名和加密方式 +# ============================================================================== + +# 安全配置 +SIGNING_ENABLED=true # 是否启用签名 +SIGNING_ALGORITHM="rsa" # 签名算法,rsa, ecdsa +PRIVATE_KEY="/etc/patch/keys/private.pem" # 私钥文件路径 +PUBLIC_KEY="/etc/patch/keys/public.pem" # 公钥文件路径 + +ENCRYPTION_ENABLED=false # 是否启用加密 +ENCRYPTION_ALGORITHM="aes-256-gcm" # 加密算法,aes-256-gcm, aes-256-cbc +ENCRYPTION_KEY="/etc/patch/keys/encryption.key" # 加密密钥文件路径 +ENCRYPTION_IV="/etc/patch/keys/encryption.iv" # 加密初始化向量文件路径 + +# ============================================================================== +# 备份配置 - 定义是否启用备份和备份策略 +# ============================================================================== + +# 备份配置 +BACKUP_ENABLED=true # 是否启用备份 +BACKUP_STRATEGY="full" # 备份策略,full, incremental, differential +BACKUP_RETENTION_DAYS=30 # 备份保留天数 + +# ============================================================================== +# 回滚配置 - 定义是否启用自动回滚和回滚策略 +# ============================================================================== + +# 回滚配置 +ROLLBACK_ENABLED=true # 是否启用自动回滚 +ROLLBACK_AUTO_GENERATE=true # 是否自动生成回滚脚本 +ROLLBACK_STRATEGY="snapshot" # 回滚策略,snapshot, restore + +# ============================================================================== +# 通知配置 - 定义是否启用通知和通知渠道 +# ============================================================================== + +# 通知配置 +NOTIFICATIONS_ENABLED=false # 是否启用通知 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." # Slack Webhook URL +EMAIL_RECIPIENTS="devops@company.com,team@company.com" # 邮箱接收人列表 + +# ============================================================================== +# 性能配置 - 定义并行处理和资源限制 +# ============================================================================== + +# 性能配置 +PARALLEL_PROCESSING=false # 是否启用并行处理 +MAX_WORKERS=4 # 最大工作线程数 +MEMORY_LIMIT="2GB" # 内存限制 +IO_BUFFER_SIZE="64KB" # IO缓冲区大小 + +# ============================================================================== +# 输出配置 - 定义补丁输出格式和目录 +# ============================================================================== + +# 输出配置 +OUTPUT_FORMAT="tar.gz" # 输出格式,tar.gz, zip +OUTPUT_DIRECTORY="/opt/patches" # 输出目录 +NAMING_PATTERN="patch-{name}-{version}-{timestamp}-{git_commit}.{format}" # 文件名命名模式,包含占位符,举例:patch-app-1.0.0-20250101T000000Z-abc123.tar.gz + +# ============================================================================== +# 日志配置 - 定义日志级别、文件路径和大小 +# ============================================================================== + +# 日志配置 +LOG_LEVEL="INFO" # 日志级别,DEBUG, INFO, WARN, ERROR +LOG_FILE="/var/log/patch_system/patch.log" # 日志文件路径 +LOG_MAX_SIZE="100MB" # 日志文件最大大小 +LOG_BACKUP_COUNT=10 # 日志文件备份数量 + +# ============================================================================== +# 长路径支持 - 定义是否启用长路径支持 +# ============================================================================== + +# 长路径支持 +LONG_PATH_SUPPORT=true # 是否启用长路径支持 +TAR_OPTIONS="--force-local" # tar命令选项,--force-local 强制使用本地文件系统 diff --git a/scripts/patch_tools/patch_config.sh.example b/scripts/patch_tools/patch_config.sh.example new file mode 100644 index 000000000..6181fd08d --- /dev/null +++ b/scripts/patch_tools/patch_config.sh.example @@ -0,0 +1,202 @@ +#!/bin/bash +# patch_config.sh - 企业级补丁配置 + +# ============================================================================== +# 基础配置 - 定义补丁的基本信息 +# ============================================================================== + +# 基础配置 +PATCH_NAME="security-hotfix-2025" +PATCH_VERSION="1.0.0" +PATCH_DESCRIPTION="紧急安全漏洞修复" +PATCH_AUTHOR="企业DevOps团队" +PATCH_EMAIL="devops@aigc-quickapp.com" + +# ============================================================================== +# 文件筛选配置 - 定义哪些文件需要被包含或排除 +# ============================================================================== + +## 包含的文件模式 +INCLUDE_PATTERNS=( + ".+/.*" # 匹配所有子目录下的文件 + "^.well-known/" + "^addon/" + "^addons/" + "^app/" + "^config/" + "^extend/" + "^public/" + "^templates/" + "^vendor/" + "^web/" + "^webapi/" + "^.404.html" + "^index.php" + "^install.php" + "^install.lock" + "^.env" + "^.env.test" + "^.env.production" + "^.env.staging" + "^.env.development" + "^.env.local" + "^.htaccess" + "^.user.ini" + "^composer.json" + "^composer.lock" +) + +## 排除的文件模式 +EXCLUDE_PATTERNS=( + "*.log" + "*.tmp" + "*.bak" + "*.swp" + "*.swx" + "^.github/" + "/node_modules/*" + "/__pycache__/*" + # 排除以下一级文件夹 + "^.git/" + "^cache/" + "^temp/" + "^tmp/" + "^logs/" + "^runtime/" + "^uploads/" + "^attachment/" + "^h5/" + "^hwapp/" + ".DS_Store" + "Thumbs.db" +) + +# ============================================================================== +# 文件大小限制 - 定义补丁处理的文件大小范围 +# ============================================================================== + +# 文件大小限制 +MAX_FILE_SIZE="20MB" # 最大文件大小 +MIN_FILE_SIZE="0KB" # 最小文件大小 + +# ============================================================================== +# 时间过滤 - 定义补丁处理的文件时间范围 +# ============================================================================== + +# 时间过滤 +MODIFIED_AFTER="2000-01-01T00:00:00Z" # 仅包含修改时间在该时间之后的文件 +CREATED_AFTER="2000-01-01T00:00:00Z" # 仅包含创建时间在该时间之后的文件 + +# ============================================================================== +# 比较方法配置 - 定义补丁处理的比较方式 +# ============================================================================== + +# 比较方法配置 +# 比较内容和时间 +COMPARISON_METHOD="content" # 比较方法: content, time, both +HASH_ALGORITHM="sha256" # 哈希算法,用于文件内容比较 +TIME_PRECISION="second" # 时间精度,用于文件时间比较 +COMPARE_PERMISSIONS=false # 是否比较文件权限 +COMPARE_OWNERSHIP=false # 是否比较文件所有者 + +# ============================================================================== +# 增量配置 - 定义是否启用增量补丁 +# ============================================================================== + +# 增量配置 +INCREMENTAL_ENABLED=true # 是否启用增量补丁 +BASE_VERSION="1.0.0" # 基础版本,用于增量比较 +DETECT_RENAMES=true # 是否检测文件重命名 +BINARY_DIFF=true # 是否启用二进制差异比较 +CHUNK_SIZE="8KB" # 二进制差异比较的块大小 + + + +# ============================================================================== +# 压缩配置 - 定义补丁压缩方式和级别 +# ============================================================================== + +# 压缩配置 +COMPRESSION_ENABLED=true # 是否启用压缩 +COMPRESSION_ALGORITHM="gzip" # gzip, bzip2, xz +COMPRESSION_LEVEL=6 # 压缩级别,1-9 +PER_FILE_OPTIMIZATION=true # 是否对每个文件单独压缩 + +# ============================================================================== +# 安全配置 - 定义补丁签名和加密方式 +# ============================================================================== + +# 安全配置 +SIGNING_ENABLED=true # 是否启用签名 +SIGNING_ALGORITHM="rsa" # 签名算法,rsa, ecdsa +PRIVATE_KEY="/etc/patch/keys/private.pem" # 私钥文件路径 +PUBLIC_KEY="/etc/patch/keys/public.pem" # 公钥文件路径 + +ENCRYPTION_ENABLED=false # 是否启用加密 +ENCRYPTION_ALGORITHM="aes-256-gcm" # 加密算法,aes-256-gcm, aes-256-cbc +ENCRYPTION_KEY="/etc/patch/keys/encryption.key" # 加密密钥文件路径 +ENCRYPTION_IV="/etc/patch/keys/encryption.iv" # 加密初始化向量文件路径 + +# ============================================================================== +# 备份配置 - 定义是否启用备份和备份策略 +# ============================================================================== + +# 备份配置 +BACKUP_ENABLED=true # 是否启用备份 +BACKUP_STRATEGY="full" # 备份策略,full, incremental, differential +BACKUP_RETENTION_DAYS=30 # 备份保留天数 + +# ============================================================================== +# 回滚配置 - 定义是否启用自动回滚和回滚策略 +# ============================================================================== + +# 回滚配置 +ROLLBACK_ENABLED=true # 是否启用自动回滚 +ROLLBACK_AUTO_GENERATE=true # 是否自动生成回滚脚本 +ROLLBACK_STRATEGY="snapshot" # 回滚策略,snapshot, restore + +# ============================================================================== +# 通知配置 - 定义是否启用通知和通知渠道 +# ============================================================================== + +# 通知配置 +NOTIFICATIONS_ENABLED=false # 是否启用通知 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." # Slack Webhook URL +EMAIL_RECIPIENTS="devops@company.com,team@company.com" # 邮箱接收人列表 + +# ============================================================================== +# 性能配置 - 定义并行处理和资源限制 +# ============================================================================== + +# 性能配置 +PARALLEL_PROCESSING=false # 是否启用并行处理 +MAX_WORKERS=4 # 最大工作线程数 +MEMORY_LIMIT="2GB" # 内存限制 +IO_BUFFER_SIZE="64KB" # IO缓冲区大小 + +# ============================================================================== +# 输出配置 - 定义补丁输出格式和目录 +# ============================================================================== + +# 输出配置 +OUTPUT_FORMAT="tar.gz" # 输出格式,tar.gz, zip +OUTPUT_DIRECTORY="/opt/patches" # 输出目录 +NAMING_PATTERN="patch-{name}-{version}-{timestamp}-{git_commit}.{format}" # 文件名命名模式,包含占位符,举例:patch-app-1.0.0-20250101T000000Z-abc123.tar.gz + +# ============================================================================== +# 日志配置 - 定义日志级别、文件路径和大小 +# ============================================================================== + +# 日志配置 +LOG_LEVEL="INFO" # 日志级别,DEBUG, INFO, WARN, ERROR +LOG_FILE="/var/log/patch_system/patch.log" # 日志文件路径 +LOG_MAX_SIZE="100MB" # 日志文件最大大小 +LOG_BACKUP_COUNT=10 # 日志文件备份数量 + +# ============================================================================== +# 长路径支持 - 定义是否启用长路径支持 +# ============================================================================== + +# 长路径支持 +LONG_PATH_SUPPORT=true # 是否启用长路径支持 +TAR_OPTIONS="--force-local" # tar命令选项,--force-local 强制使用本地文件系统 diff --git a/scripts/patch_tools/patch_fnc.sh b/scripts/patch_tools/patch_fnc.sh new file mode 100644 index 000000000..a44e2a691 --- /dev/null +++ b/scripts/patch_tools/patch_fnc.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# patch_fnc.sh - 企业级增量补丁包生成脚本公共函数 +# 用于并行查找,提供给xargs使用 + + +# 文件工具函数 +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}" + + case "$algorithm" in + "md5") md5sum "$file_path" | cut -d' ' -f1 ;; + "sha1") sha1sum "$file_path" | cut -d' ' -f1 ;; + "sha256") sha256sum "$file_path" | cut -d' ' -f1 ;; + *) sha256sum "$file_path" | cut -d' ' -f1 ;; + 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 + debug "文件匹配包含模式<$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 +} \ No newline at end of file diff --git a/scripts/patch_tools/patch_generator.sh b/scripts/patch_tools/patch_generator.sh new file mode 100644 index 000000000..0bd80b858 --- /dev/null +++ b/scripts/patch_tools/patch_generator.sh @@ -0,0 +1,1052 @@ +#!/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" ;; + *) color="$NC" ;; + esac + + + # 日志级别过滤 + case "$LOG_LEVEL" in + "DEBUG") + # DEBUG级别:输出所有日志 + ;; + "INFO") + # INFO级别:只输出INFO、WARN和ERROR日志 + if [[ "$level" == "DEBUG" ]]; then + return 0 + fi + ;; + "WARN") + # WARN级别:只输出WARN和ERROR日志 + if [[ "$level" == "DEBUG" ]] || [[ "$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" +} + +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 + + # 设置默认值 + : ${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"} + + source "$CONFIG_FILE" + info "配置文件加载完成" +} + +# 环境设置 +setup_environment() { + # 创建必要目录 + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$OUTPUT_DIRECTORY" + mkdir -p "$TEMP_DIR" + + # 设置日志级别 + case "$LOG_LEVEL" in + "DEBUG") set -x ;; + "INFO") ;; + "WARN") ;; + "ERROR") ;; + esac + + 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" "find" "stat" "sha256sum" "date" "mkdir" "cp") + 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}" + + case "$algorithm" in + "md5") md5sum "$file_path" | cut -d' ' -f1 ;; + "sha1") sha1sum "$file_path" | cut -d' ' -f1 ;; + "sha256") sha256sum "$file_path" | cut -d' ' -f1 ;; + *) sha256sum "$file_path" | cut -d' ' -f1 ;; + 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 + debug "文件匹配包含模式<$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 + # 检查是否应该包含 + if should_include_file "$file_path" "$file_info"; then + local file_hash=$(get_file_hash "$file_path" "$HASH_ALGORITHM") + local relative_path="${file_path#$base_dir/}" + + 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 "开始比较文件差异" + + 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 + + case "$COMPARISON_METHOD" in + "content") + [[ "$old_hash" != "$new_hash" ]] && is_modified=true + debug "检测到修改文件: $path | 哈希值变化: <$old_hash> => <$new_hash>" + ;; + "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 + debug "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + else + [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]] && is_modified=true + debug "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + 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_hash" != "$new_hash" ]]; then + is_modified=true + debug "检测到修改文件: $path | 哈希值变化: <$old_hash> => <$new_hash>" + elif [[ "$TIME_PRECISION" == "second" ]] && [[ $old_mtime -ne $new_mtime ]]; then + is_modified=true + debug "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + elif [[ "$TIME_PRECISION" == "millisecond" ]] && [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]]; then + is_modified=true + debug "检测到修改文件: $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 --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") + debug "回滚文件: $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++)) + debug "复制文件: $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 "$@" \ No newline at end of file diff --git a/scripts/patch_tools/patch_rollback.sh b/scripts/patch_tools/patch_rollback.sh new file mode 100644 index 000000000..1708d6f44 --- /dev/null +++ b/scripts/patch_tools/patch_rollback.sh @@ -0,0 +1,356 @@ +#!/bin/bash +# patch_rollback.sh - 企业级补丁包回滚脚本 + +set -euo pipefail +shopt -s nullglob extglob + +# 脚本目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh" + +# 颜色定义 +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_rollback_$(date +%Y%m%d_%H%M%S).log"} + : ${BACKUP_DIR:="/var/backups/patch"} + : ${TEMP_DIR:="/tmp/patch_rollback_$$"} + : ${ROLLBACK_INFO_FILE:="$BACKUP_DIR/rollback_info.json"} +} + +setup_environment() { + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$TEMP_DIR" + + info "日志文件: $LOG_FILE" + info "临时目录: $TEMP_DIR" +} + +cleanup() { + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + info "临时目录已清理: $TEMP_DIR" + fi +} + +trap cleanup EXIT + +# 回滚点管理 +get_available_rollback_points() { + info "可用的回滚点:" + + if [[ ! -f "$ROLLBACK_INFO_FILE" ]]; then + warn "未找到回滚点信息文件" + return 1 + fi + + # 读取回滚点信息 + local patch_name=$(jq -r '.patch_name' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "") + local apply_time=$(jq -r '.apply_time' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "") + local backup_path=$(jq -r '.backup_path' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "") + + if [[ -n "$patch_name" && -n "$backup_path" && -f "$backup_path" ]]; then + echo "1. $patch_name (应用时间: $apply_time)" + echo " 备份文件: $backup_path" + return 0 + else + warn "回滚点信息不完整或备份文件不存在" + return 1 + fi +} + +# 验证回滚包 +verify_rollback_package() { + local rollback_path="$1" + + info "验证回滚包: $rollback_path" + + if [[ ! -f "$rollback_path" ]]; then + error "回滚包不存在: $rollback_path" + return 1 + fi + + if ! tar -tzf "$rollback_path" >/dev/null 2>&1; then + error "回滚包损坏或格式错误" + return 1 + fi + + info "✅ 回滚包验证通过" + return 0 +} + +# 回滚执行函数 +perform_rollback() { + local rollback_path="$1" + local dry_run="${2:-false}" + + info "开始执行回滚: $rollback_path" + + # 验证回滚包 + if ! verify_rollback_package "$rollback_path"; then + return 1 + fi + + # 解压回滚包 + local extract_dir="$TEMP_DIR/rollback_extract" + if ! tar -xzf "$rollback_path" -C "$extract_dir"; then + error "回滚包解压失败" + return 1 + fi + info "✅ 回滚包解压完成" + + # 如果是干跑模式,只显示将要回滚的文件 + if [[ "$dry_run" == "true" ]]; then + info "干跑模式: 显示将要回滚的文件" + find "$extract_dir" -type f | while read file; do + local relative_path="${file#$extract_dir/}" + local target_file="/$relative_path" + echo "📁 将回滚: $relative_path -> $target_file" + done + return 0 + fi + + # 执行实际回滚 + if execute_file_rollback "$extract_dir"; then + info "✅ 回滚执行完成" + + # 清理回滚点 + cleanup_rollback_point + + send_rollback_notification "success" "$rollback_path" "回滚成功" + return 0 + else + error "❌ 回滚执行失败" + send_rollback_notification "failure" "$rollback_path" "回滚失败" + return 1 + fi +} + +execute_file_rollback() { + local extract_dir="$1" + + info "开始执行文件回滚..." + + local rollback_count=0 + local error_count=0 + + # 遍历回滚包中的所有文件 + find "$extract_dir" -type f | while read backup_file; do + local relative_path="${backup_file#$extract_dir/}" + local target_file="/$relative_path" + local target_dir=$(dirname "$target_file") + + # 创建目标目录 + mkdir -p "$target_dir" + + # 备份当前文件(如果存在) + if [[ -f "$target_file" ]]; then + local current_backup="$TEMP_DIR/current_backup/$relative_path" + mkdir -p "$(dirname "$current_backup")" + cp -p "$target_file" "$current_backup" + debug "备份当前文件: $target_file" + fi + + # 恢复文件 + if cp -p "$backup_file" "$target_file"; then + info "✅ 回滚文件: $relative_path" + ((rollback_count++)) + else + error "❌ 回滚失败: $relative_path" + ((error_count++)) + fi + done + + info "回滚完成: $rollback_count 个文件成功, $error_count 个文件失败" + + if [[ $error_count -gt 0 ]]; then + return 1 + fi + return 0 +} + +# 清理回滚点 +cleanup_rollback_point() { + if [[ -f "$ROLLBACK_INFO_FILE" ]]; then + mv "$ROLLBACK_INFO_FILE" "$ROLLBACK_INFO_FILE.bak" + info "回滚点已清理: $ROLLBACK_INFO_FILE -> $ROLLBACK_INFO_FILE.bak" + fi +} + +# 通知函数 +send_rollback_notification() { + local status="$1" + local rollback_path="$2" + local message="$3" + + if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then + return 0 + fi + + case "$status" in + "success") + local subject="补丁回滚成功" + local color="#2ecc71" + ;; + "failure") + local subject="补丁回滚失败" + local color="#e74c3c" + ;; + *) + local subject="补丁回滚通知" + local color="#95a5a6" + ;; + esac + + # Slack通知 + if [[ -n "$SLACK_WEBHOOK" ]]; then + send_slack_rollback_notification "$subject" "$message" "$color" "$rollback_path" + fi +} + +send_slack_rollback_notification() { + local subject="$1" + local message="$2" + local color="$3" + local rollback_path="$4" + + local payload=$(cat << EOF +{ + "attachments": [ + { + "color": "$color", + "title": "$subject", + "text": "$message", + "fields": [ + { + "title": "回滚包", + "value": "$(basename "$rollback_path")", + "short": true + }, + { + "title": "回滚时间", + "value": "$(date)", + "short": true + }, + { + "title": "回滚主机", + "value": "$(hostname)", + "short": true + } + ] + } + ] +} +EOF +) + + if command -v curl >/dev/null 2>&1; then + curl -s -X POST -H 'Content-type: application/json' \ + --data "$payload" "$SLACK_WEBHOOK" >/dev/null && \ + info "Slack通知发送成功" + fi +} + +# 主回滚函数 +rollback_patch() { + local rollback_path="${1:-}" + local dry_run="${2:-false}" + + # 如果没有指定回滚包,使用最近的 + if [[ -z "$rollback_path" ]]; then + info "未指定回滚包,查找最近的回滚点..." + if get_available_rollback_points; then + if [[ -f "$ROLLBACK_INFO_FILE" ]]; then + rollback_path=$(jq -r '.backup_path' "$ROLLBACK_INFO_FILE") + info "使用回滚包: $rollback_path" + else + error "未找到可用的回滚点" + return 1 + fi + else + error "无法获取回滚点信息" + return 1 + fi + fi + + # 执行回滚 + if perform_rollback "$rollback_path" "$dry_run"; then + info "🎉 回滚操作成功完成" + return 0 + else + error "💥 回滚操作失败" + return 1 + fi +} + +# 主函数 +main() { + local rollback_path="${1:-}" + local dry_run="${2:-false}" + + # 加载配置 + load_config + + # 设置环境 + setup_environment + + # 显示使用信息 + if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then + echo "用法: $0 [回滚包路径] [dry-run]" + echo "示例:" + echo " $0 # 使用最近的回滚点" + echo " $0 /var/backups/patch/backup_xxx.tar.gz # 指定回滚包" + echo " $0 /path/to/backup.tar.gz dry-run # 干跑模式" + exit 0 + fi + + # 执行回滚 + if rollback_patch "$rollback_path" "$dry_run"; then + exit 0 + else + exit 1 + fi +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main "$@" \ No newline at end of file diff --git a/scripts/patch_tools/patch_verifier.sh b/scripts/patch_tools/patch_verifier.sh new file mode 100644 index 000000000..36e4d27a3 --- /dev/null +++ b/scripts/patch_tools/patch_verifier.sh @@ -0,0 +1,538 @@ +#!/bin/bash +# patch_verifier.sh - 企业级补丁包验证脚本 + +set -euo pipefail +shopt -s nullglob extglob + +# 脚本目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh" + +# 颜色定义 +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_verify_$(date +%Y%m%d_%H%M%S).log"} + : ${TEMP_DIR:="/tmp/patch_verify_$$"} +} + +setup_environment() { + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$TEMP_DIR" + + info "日志文件: $LOG_FILE" + info "临时目录: $TEMP_DIR" +} + +cleanup() { + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + info "临时目录已清理: $TEMP_DIR" + fi +} + +trap cleanup EXIT + +# 验证函数 +verify_package_integrity() { + local package_path="$1" + local verify_type="$2" # pre-apply, post-apply, standalone + + info "开始验证补丁包: $package_path (类型: $verify_type)" + + local overall_result=true + + # 1. 基础验证 + if ! verify_basic_integrity "$package_path"; then + overall_result=false + fi + + # 2. 安全验证 + if ! verify_security "$package_path"; then + overall_result=false + fi + + # 3. 内容验证 + if ! verify_content "$package_path" "$verify_type"; then + overall_result=false + fi + + # 4. 系统状态验证 + if [[ "$verify_type" == "post-apply" ]]; then + if ! verify_system_state "$package_path"; then + overall_result=false + fi + fi + + # 生成验证报告 + generate_verification_report "$package_path" "$overall_result" "$verify_type" + + if $overall_result; then + info "✅ 补丁包验证通过" + return 0 + else + error "❌ 补丁包验证失败" + return 1 + fi +} + +verify_basic_integrity() { + local package_path="$1" + + info "执行基础完整性验证..." + local result=true + + # 文件存在性检查 + if [[ ! -f "$package_path" ]]; then + error "❌ 补丁包不存在: $package_path" + result=false + fi + + # 文件大小检查 + local size=$(stat -c%s "$package_path" 2>/dev/null || echo "0") + if [[ $size -eq 0 ]]; then + error "❌ 补丁包为空" + result=false + else + info "✅ 文件大小: $(numfmt --to=iec-i --suffix=B $size)" + fi + + # 压缩包完整性检查 + if ! tar -tzf "$package_path" >/dev/null 2>&1; then + error "❌ 压缩包损坏或格式错误" + result=false + else + info "✅ 压缩包完整性验证通过" + fi + + $result +} + +verify_security() { + local package_path="$1" + + info "执行安全验证..." + local result=true + + # 校验和验证 + if [[ -f "${package_path}.sha256" ]]; then + if sha256sum -c "${package_path}.sha256" >/dev/null 2>&1; then + info "✅ 校验和验证通过" + else + error "❌ 校验和验证失败" + result=false + fi + else + warn "⚠️ 未找到校验和文件" + fi + + # 签名验证 + if [[ -f "${package_path}.sig" ]]; then + if command -v gpg >/dev/null 2>&1; then + if gpg --verify "${package_path}.sig" "$package_path" >/dev/null 2>&1; then + info "✅ 签名验证通过" + else + error "❌ 签名验证失败" + result=false + fi + else + warn "⚠️ GPG未安装,跳过签名验证" + fi + else + warn "⚠️ 未找到签名文件" + fi + + $result +} + +verify_content() { + local package_path="$1" + local verify_type="$2" + + info "执行内容验证..." + local result=true + + # 解压补丁包 + local extract_dir="$TEMP_DIR/extract" + if ! tar -xzf "$package_path" -C "$extract_dir"; then + error "❌ 补丁包解压失败" + return false + fi + + # 检查清单文件 + local manifest_file="$extract_dir/MANIFEST.MF" + if [[ ! -f "$manifest_file" ]]; then + error "❌ 清单文件不存在" + result=false + else + info "✅ 清单文件存在" + + # 验证清单格式 + if ! validate_manifest_format "$manifest_file"; then + result=false + fi + + # 验证文件完整性 + if ! validate_file_integrity "$extract_dir" "$manifest_file" "$verify_type"; then + result=false + fi + fi + + $result +} + +validate_manifest_format() { + local manifest_file="$1" + local result=true + + # 检查必需字段 + local required_fields=("名称" "版本" "生成时间") + for field in "${required_fields[@]}"; do + if ! grep -q "^$field:" "$manifest_file"; then + error "❌ 清单缺少必需字段: $field" + result=false + fi + done + + # 检查变更记录格式 + local change_count=$(grep -c -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file" || true) + if [[ $change_count -eq 0 ]]; then + warn "⚠️ 清单中没有变更记录" + else + info "✅ 变更记录数量: $change_count" + fi + + $result +} + +validate_file_integrity() { + local extract_dir="$1" + local manifest_file="$2" + local verify_type="$3" + + info "验证文件完整性..." + local result=true + local verified_count=0 + local error_count=0 + + # 处理清单中的每个变更记录 + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED"|"MODIFIED") + if ! verify_patch_file "$extract_dir" "$path" "$change_type" "$verify_type"; then + error_count=$((error_count + 1)) + result=false + else + verified_count=$((verified_count + 1)) + fi + ;; + "DELETED") + if ! verify_deleted_file "$path" "$verify_type"; then + error_count=$((error_count + 1)) + result=false + else + verified_count=$((verified_count + 1)) + fi + ;; + esac + done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") + + info "文件验证完成: $verified_count 个文件成功, $error_count 个文件失败" + $result +} + +verify_patch_file() { + local extract_dir="$1" + local file_path="$2" + local change_type="$3" + local verify_type="$4" + + local patch_file="$extract_dir/files/$file_path" + local target_file="/$file_path" + + # 检查补丁包中的文件是否存在 + if [[ ! -f "$patch_file" ]]; then + error "❌ 补丁包中文件不存在: $file_path" + return 1 + fi + + # 对于应用后验证,检查目标文件 + if [[ "$verify_type" == "post-apply" ]]; then + if [[ ! -f "$target_file" ]]; then + error "❌ 目标文件不存在: $target_file" + return 1 + fi + + # 比较文件内容 + local patch_hash=$(sha256sum "$patch_file" | cut -d' ' -f1) + local target_hash=$(sha256sum "$target_file" | cut -d' ' -f1) + + if [[ "$patch_hash" != "$target_hash" ]]; then + error "❌ 文件内容不匹配: $file_path" + return 1 + fi + + info "✅ 文件验证通过: $file_path" + else + info "✅ 补丁文件存在: $file_path" + fi + + return 0 +} + +verify_deleted_file() { + local file_path="$1" + local verify_type="$2" + + local target_file="/$file_path" + + # 对于应用后验证,检查文件是否已删除 + if [[ "$verify_type" == "post-apply" ]]; then + if [[ -f "$target_file" ]]; then + error "❌ 文件未成功删除: $target_file" + return 1 + fi + info "✅ 文件删除验证通过: $file_path" + else + info "✅ 删除操作记录存在: $file_path" + fi + + return 0 +} + +verify_system_state() { + local package_path="$1" + + info "验证系统状态..." + local result=true + + # 检查关键服务状态 + if ! verify_services; then + result=false + fi + + # 检查磁盘空间 + if ! verify_disk_space; then + result=false + fi + + # 检查系统负载 + if ! verify_system_load; then + result=false + fi + + $result +} + +verify_services() { + local result=true + + # 检查Web服务 + if systemctl is-active --quiet nginx || systemctl is-active --quiet apache2; then + info "✅ Web服务运行正常" + else + warn "⚠️ Web服务未运行" + fi + + # 检查数据库服务 + if systemctl is-active --quiet mysql || systemctl is-active --quiet postgresql; then + info "✅ 数据库服务运行正常" + else + warn "⚠️ 数据库服务未运行" + fi + + $result +} + +verify_disk_space() { + local result=true + local threshold=90 # 90% 使用率阈值 + + # 检查根分区使用率 + local usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') + if [[ $usage -gt $threshold ]]; then + error "❌ 磁盘空间不足: / 分区使用率 $usage%" + result=false + else + info "✅ 磁盘空间正常: / 分区使用率 $usage%" + fi + + $result +} + +verify_system_load() { + local result=true + local load_threshold=10.0 # 负载阈值 + + # 获取系统负载 + local load=$(awk '{print $1}' /proc/loadavg) + local cores=$(nproc) + + if (( $(echo "$load > $cores" | bc -l) )); then + warn "⚠️ 系统负载较高: $load (CPU核心数: $cores)" + else + info "✅ 系统负载正常: $load (CPU核心数: $cores)" + fi + + $result +} + +# 报告生成函数 +generate_verification_report() { + local package_path="$1" + local overall_result="$2" + local verify_type="$3" + + local report_file="$TEMP_DIR/verification_report_$(date +%Y%m%d_%H%M%S).txt" + + cat > "$report_file" << EOF +补丁包验证报告 +======================================== +补丁包: $(basename "$package_path") +验证类型: $verify_type +验证时间: $(date) +验证主机: $(hostname) +总体结果: $(if $overall_result; then echo "通过"; else echo "失败"; fi) + +详细验证结果: +EOF + + # 添加详细验证信息 + { + echo "1. 基础完整性验证: $(if verify_basic_integrity "$package_path"; then echo "通过"; else echo "失败"; fi)" + echo "2. 安全验证: $(if verify_security "$package_path"; then echo "通过"; else echo "失败"; fi)" + echo "3. 内容验证: $(if verify_content "$package_path" "$verify_type"; then echo "通过"; else echo "失败"; fi)" + if [[ "$verify_type" == "post-apply" ]]; then + echo "4. 系统状态验证: $(if verify_system_state "$package_path"; then echo "通过"; else echo "失败"; fi)" + fi + } >> "$report_file" + + info "验证报告生成完成: $report_file" + + # 显示报告摘要 + echo + echo "验证报告摘要:" + cat "$report_file" + echo +} + +# 批量验证函数 +batch_verify() { + local patch_dir="${1:-$OUTPUT_DIRECTORY}" + local verify_type="${2:-standalone}" + + info "开始批量验证补丁包目录: $patch_dir" + + local total_count=0 + local success_count=0 + local fail_count=0 + + # 查找所有补丁包文件 + for patch_file in "$patch_dir"/*.tar.gz; do + if [[ -f "$patch_file" ]]; then + ((total_count++)) + info "验证补丁包: $(basename "$patch_file")" + + if verify_package_integrity "$patch_file" "$verify_type"; then + ((success_count++)) + else + ((fail_count++)) + fi + + echo "----------------------------------------" + fi + done + + info "批量验证完成: $success_count/$total_count 成功, $fail_count/$total_count 失败" + + if [[ $fail_count -eq 0 ]]; then + return 0 + else + return 1 + fi +} + +# 主验证函数 +main_verify() { + local patch_path="${1:-}" + local verify_type="${2:-standalone}" + local batch_mode="${3:-false}" + + # 加载配置 + load_config + + # 设置环境 + setup_environment + + # 批量验证模式 + if [[ "$batch_mode" == "true" ]] || [[ -d "$patch_path" ]]; then + batch_verify "$patch_path" "$verify_type" + return $? + fi + + # 单个文件验证 + if [[ -z "$patch_path" ]]; then + echo "用法: $0 <补丁包路径|目录> [verify-type] [batch]" + echo "验证类型:" + echo " standalone - 独立验证(默认)" + echo " pre-apply - 应用前验证" + echo " post-apply - 应用后验证" + echo "示例:" + echo " $0 /opt/patches/patch.tar.gz" + echo " $0 /opt/patches/patch.tar.gz pre-apply" + echo " $0 /opt/patches/ batch" + exit 1 + fi + + # 执行验证 + if verify_package_integrity "$patch_path" "$verify_type"; then + info "🎉 补丁包验证成功" + exit 0 + else + error "💥 补丁包验证失败" + exit 1 + fi +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main_verify "$@" \ No newline at end of file diff --git a/scripts/patch_tools/patch_workflow.sh b/scripts/patch_tools/patch_workflow.sh new file mode 100644 index 000000000..7049ea95a --- /dev/null +++ b/scripts/patch_tools/patch_workflow.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# patch_workflow.sh - 完整补丁管理工作流 + +set -euo pipefail + +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_DIR="/opt/patch-management" + +# 颜色定义 +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 完整工作流 +full_patch_workflow() { + local source_dir="$1" + local target_dir="$2" + local patch_name="$3" + + log "开始完整补丁管理工作流" + echo "========================================" + + # 1. 生成补丁包 + log "步骤1: 生成补丁包" + if ! "$SCRIPT_DIR/patch_generator.sh" "$source_dir" "$target_dir" "$patch_name"; then + error "补丁包生成失败" + return 1 + fi + + # 获取生成的补丁包路径 + local patch_file=$(find "/opt/patches" -name "*${patch_name}*" -type f | head -1) + if [[ -z "$patch_file" ]]; then + error "未找到补丁包文件" + return 1 + fi + + # 2. 验证补丁包 + log "步骤2: 验证补丁包" + if ! "$SCRIPT_DIR/patch_verifier.sh" "$patch_file" "pre-apply"; then + error "补丁包验证失败" + return 1 + fi + + # 3. 应用补丁包(干跑模式) + log "步骤3: 干跑模式应用补丁" + if ! "$SCRIPT_DIR/patch_applier.sh" "$patch_file" "dry-run"; then + error "干跑模式应用失败" + return 1 + fi + + # 4. 实际应用补丁包 + read -p "是否继续实际应用补丁? (y/N): " confirm + if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then + log "步骤4: 实际应用补丁" + if ! "$SCRIPT_DIR/patch_applier.sh" "$patch_file"; then + error "补丁应用失败" + return 1 + fi + else + log "操作取消" + return 0 + fi + + # 5. 应用后验证 + log "步骤5: 应用后验证" + if ! "$SCRIPT_DIR/patch_verifier.sh" "$patch_file" "post-apply"; then + error "应用后验证失败" + return 1 + fi + + log "🎉 完整补丁管理工作流完成" + return 0 +} + +# 回滚工作流 +rollback_workflow() { + local rollback_file="${1:-}" + + log "开始回滚工作流" + echo "========================================" + + # 1. 干跑模式回滚 + log "步骤1: 干跑模式回滚" + if ! "$SCRIPT_DIR/patch_rollback.sh" "$rollback_file" "dry-run"; then + error "干跑模式回滚失败" + return 1 + fi + + # 2. 实际回滚 + read -p "是否继续实际回滚? (y/N): " confirm + if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then + log "步骤2: 实际回滚" + if ! "$SCRIPT_DIR/patch_rollback.sh" "$rollback_file"; then + error "回滚失败" + return 1 + fi + else + log "操作取消" + return 0 + fi + + log "✅ 回滚工作流完成" + return 0 +} + +# 主函数 +main() { + case "${1:-}" in + "generate") + shift + full_patch_workflow "$@" + ;; + "apply") + shift + "$SCRIPT_DIR/patch_applier.sh" "$@" + ;; + "rollback") + shift + rollback_workflow "$@" + ;; + "verify") + shift + "$SCRIPT_DIR/patch_verifier.sh" "$@" + ;; + "batch-verify") + shift + "$SCRIPT_DIR/patch_verifier.sh" "$1" "standalone" "batch" + ;; + *) + echo "用法: $0 [args]" + echo "命令:" + echo " generate <源目录> <目标目录> <补丁名称> # 完整工作流" + echo " apply <补丁包路径> [dry-run] # 应用补丁" + echo " rollback [回滚包路径] # 回滚补丁" + echo " verify <补丁包路径> [验证类型] # 验证补丁" + echo " batch-verify <目录> # 批量验证" + echo "" + echo "示例:" + echo " $0 generate /old/version /new/version security-hotfix" + echo " $0 apply /opt/patches/patch.tar.gz dry-run" + echo " $0 rollback /var/backups/patch/backup.tar.gz" + exit 1 + ;; + esac +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main "$@" \ No newline at end of file diff --git a/scripts/patch_tools/shop_src_patch_config.sh b/scripts/patch_tools/shop_src_patch_config.sh new file mode 100644 index 000000000..5c420b64f --- /dev/null +++ b/scripts/patch_tools/shop_src_patch_config.sh @@ -0,0 +1,210 @@ +#!/bin/bash +# shop_src_patch_config.sh - 针对Shop项目的补丁配置 + +# ============================================================================== +# 基础配置 - 定义补丁的基本信息 +# ============================================================================== + +# 基础配置 +PATCH_NAME="shop-src-feat-fix-2025" +PATCH_VERSION="1.0.0" +PATCH_DESCRIPTION="Shop项目功能修复补丁" +PATCH_AUTHOR="企业DevOps团队" +PATCH_EMAIL="devops@aigc-quickapp.com" + +# ============================================================================== +# 文件筛选配置 - 定义哪些文件需要被包含或排除 +# ============================================================================== + +## 包含的文件模式 +INCLUDE_PATTERNS=( + "*.php" + "*.js" + "*.css" + "*.html" + "*.conf" + "*.json" + "*.yml" + "*.yaml" + "*.py" + ".well-known/*" + "addon/*" + "addons/*" + "app/*" + "config/*" + "extend/*" + "h5/*" + "hwapp/*" + "public/*" + "templates/*" + "web/*" + "webapi/*" + ".404.html" + "index.php" + "install.php" + "install.lock" + ".env" + ".env.test" + ".env.production" + ".env.staging" + ".env.development" + ".env.local" + ".htaccess" + ".user.ini" + "composer.json" + "composer.lock" +) + +## 排除的文件模式 +EXCLUDE_PATTERNS=( + "*.log" + "*.tmp" + "*.bak" + "*.swp" + ".git/*" + "node_modules/*" + "__pycache__/*" + '/cache/', # 排除缓存目录 + '/temp/', # 排除临时目录 + '/tmp/', # 排除临时目录 + '/logs/', # 排除日志目录 + '/runtime/', # 排除运行时目录 + '/uploads/', # 排除上传目录 + '/attachment/', # 排除附件目录 + "*.min.js" + "*.min.css" + "test/*" + "temp/*" + ".DS_Store" + "Thumbs.db" +) + +# ============================================================================== +# 文件大小限制 - 定义补丁处理的文件大小范围 +# ============================================================================== + +# 文件大小限制 +MAX_FILE_SIZE="20MB" # 最大文件大小 +MIN_FILE_SIZE="1KB" # 最小文件大小 + +# ============================================================================== +# 时间过滤 - 定义补丁处理的文件时间范围 +# ============================================================================== + +# 时间过滤 +MODIFIED_AFTER="2025-01-01T00:00:00Z" # 仅包含修改时间在该时间之后的文件 +CREATED_AFTER="2025-01-01T00:00:00Z" # 仅包含创建时间在该时间之后的文件 + +# ============================================================================== +# 比较方法配置 - 定义补丁处理的比较方式 +# ============================================================================== + +# 比较方法配置 +# 比较内容和时间 +COMPARISON_METHOD="both" # content, time, both +HASH_ALGORITHM="sha256" # 哈希算法,用于文件内容比较 +TIME_PRECISION="second" # 时间精度,用于文件时间比较 +COMPARE_PERMISSIONS=true # 是否比较文件权限 +COMPARE_OWNERSHIP=false # 是否比较文件所有者 + +# ============================================================================== +# 增量配置 - 定义是否启用增量补丁 +# ============================================================================== + +# 增量配置 +INCREMENTAL_ENABLED=true # 是否启用增量补丁 +BASE_VERSION="1.0.0" # 基础版本,用于增量比较 +DETECT_RENAMES=true # 是否检测文件重命名 +BINARY_DIFF=true # 是否启用二进制差异比较 +CHUNK_SIZE="8KB" # 二进制差异比较的块大小 + + + +# ============================================================================== +# 压缩配置 - 定义补丁压缩方式和级别 +# ============================================================================== + +# 压缩配置 +COMPRESSION_ENABLED=true # 是否启用压缩 +COMPRESSION_ALGORITHM="gzip" # gzip, bzip2, xz +COMPRESSION_LEVEL=6 # 压缩级别,1-9 +PER_FILE_OPTIMIZATION=true # 是否对每个文件单独压缩 + +# ============================================================================== +# 安全配置 - 定义补丁签名和加密方式 +# ============================================================================== + +# 安全配置 +SIGNING_ENABLED=true # 是否启用签名 +SIGNING_ALGORITHM="rsa" # 签名算法,rsa, ecdsa +PRIVATE_KEY="/etc/patch/keys/private.pem" # 私钥文件路径 +PUBLIC_KEY="/etc/patch/keys/public.pem" # 公钥文件路径 + +ENCRYPTION_ENABLED=false # 是否启用加密 +ENCRYPTION_ALGORITHM="aes-256-gcm" # 加密算法,aes-256-gcm, aes-256-cbc +ENCRYPTION_KEY="/etc/patch/keys/encryption.key" # 加密密钥文件路径 +ENCRYPTION_IV="/etc/patch/keys/encryption.iv" # 加密初始化向量文件路径 + +# ============================================================================== +# 备份配置 - 定义是否启用备份和备份策略 +# ============================================================================== + +# 备份配置 +BACKUP_ENABLED=true # 是否启用备份 +BACKUP_STRATEGY="full" # 备份策略,full, incremental, differential +BACKUP_RETENTION_DAYS=30 # 备份保留天数 + +# ============================================================================== +# 回滚配置 - 定义是否启用自动回滚和回滚策略 +# ============================================================================== + +# 回滚配置 +ROLLBACK_ENABLED=true # 是否启用自动回滚 +ROLLBACK_AUTO_GENERATE=true # 是否自动生成回滚脚本 +ROLLBACK_STRATEGY="snapshot" # 回滚策略,snapshot, restore + +# ============================================================================== +# 通知配置 - 定义是否启用通知和通知渠道 +# ============================================================================== + +# 通知配置 +NOTIFICATIONS_ENABLED=false # 是否启用通知 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." # Slack Webhook URL +EMAIL_RECIPIENTS="devops@company.com,team@company.com" # 邮箱接收人列表 + +# ============================================================================== +# 性能配置 - 定义并行处理和资源限制 +# ============================================================================== + +# 性能配置 +PARALLEL_PROCESSING=true # 是否启用并行处理 +MAX_WORKERS=4 # 最大工作线程数 +MEMORY_LIMIT="2GB" # 内存限制 +IO_BUFFER_SIZE="64KB" # IO缓冲区大小 + +# ============================================================================== +# 输出配置 - 定义补丁输出格式和目录 +# ============================================================================== + +# 输出配置 +OUTPUT_FORMAT="tar.gz" # 输出格式,tar.gz, zip +OUTPUT_DIRECTORY="/opt/patches" # 输出目录 +NAMING_PATTERN="patch-{name}-{version}-{timestamp}-{git_commit}.{format}" # 文件名命名模式,包含占位符,举例:patch-app-1.0.0-20250101T000000Z-abc123.tar.gz + +# ============================================================================== +# 日志配置 - 定义日志级别、文件路径和大小 +# ============================================================================== + +# 日志配置 +LOG_LEVEL="INFO" # 日志级别,DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_FILE="/var/log/patch_system/patch.log" # 日志文件路径 +LOG_MAX_SIZE="100MB" # 日志文件最大大小 +LOG_BACKUP_COUNT=10 # 日志文件备份数量 + +# ============================================================================== +# 长路径支持 - 定义是否启用长路径支持 +# ============================================================================== + +# 长路径支持 +LONG_PATH_SUPPORT=true # 是否启用长路径支持 +TAR_OPTIONS="--force-local" # tar命令选项,--force-local 强制使用本地文件系统