chore: 针对备份及还原处理
This commit is contained in:
@@ -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
|
||||
|
||||
21
scripts/backup_restore/.env
Normal file
21
scripts/backup_restore/.env
Normal file
@@ -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"
|
||||
59
scripts/backup_restore/backup_config.json
Normal file
59
scripts/backup_restore/backup_config.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
75
scripts/backup_restore/backup_config.php
Normal file
75
scripts/backup_restore/backup_config.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 备份还原工具默认配置文件
|
||||
* 可通过--config参数指定此文件路径使用
|
||||
*/
|
||||
return [
|
||||
// 备份文件保存目录
|
||||
'backup_dir' => '/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
|
||||
|
||||
];
|
||||
278
scripts/backup_restore/backup_server.sh
Normal file
278
scripts/backup_restore/backup_server.sh
Normal file
@@ -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 "$@"
|
||||
111
scripts/backup_restore/install_backup_system.sh
Normal file
111
scripts/backup_restore/install_backup_system.sh
Normal file
@@ -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 "$@"
|
||||
10
scripts/backup_restore/readme.md
Normal file
10
scripts/backup_restore/readme.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 备份还原脚本
|
||||
|
||||
|
||||
## 功能
|
||||
|
||||
- 备份服务器源码
|
||||
- 还原服务器源码
|
||||
- 支持长文件路径
|
||||
- 支持根据配置文件备份
|
||||
- 支持根据配置文件还原
|
||||
275
scripts/backup_restore/restore_server.sh
Normal file
275
scripts/backup_restore/restore_server.sh
Normal file
@@ -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 "$@"
|
||||
82
scripts/backup_restore/verify_backup.sh
Normal file
82
scripts/backup_restore/verify_backup.sh
Normal file
@@ -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 "$@"
|
||||
@@ -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)) {
|
||||
|
||||
16
scripts/patch_tools/docker-compose.yml
Normal file
16
scripts/patch_tools/docker-compose.yml
Normal file
@@ -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
|
||||
223
scripts/patch_tools/install_patch_system.sh
Normal file
223
scripts/patch_tools/install_patch_system.sh
Normal file
@@ -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 "$@"
|
||||
587
scripts/patch_tools/patch_applier.sh
Normal file
587
scripts/patch_tools/patch_applier.sh
Normal file
@@ -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 "$@"
|
||||
177
scripts/patch_tools/patch_config.sh
Normal file
177
scripts/patch_tools/patch_config.sh
Normal file
@@ -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 强制使用本地文件系统
|
||||
202
scripts/patch_tools/patch_config.sh.example
Normal file
202
scripts/patch_tools/patch_config.sh.example
Normal file
@@ -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 强制使用本地文件系统
|
||||
126
scripts/patch_tools/patch_fnc.sh
Normal file
126
scripts/patch_tools/patch_fnc.sh
Normal file
@@ -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
|
||||
}
|
||||
1052
scripts/patch_tools/patch_generator.sh
Normal file
1052
scripts/patch_tools/patch_generator.sh
Normal file
File diff suppressed because it is too large
Load Diff
356
scripts/patch_tools/patch_rollback.sh
Normal file
356
scripts/patch_tools/patch_rollback.sh
Normal file
@@ -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 "$@"
|
||||
538
scripts/patch_tools/patch_verifier.sh
Normal file
538
scripts/patch_tools/patch_verifier.sh
Normal file
@@ -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 "$@"
|
||||
153
scripts/patch_tools/patch_workflow.sh
Normal file
153
scripts/patch_tools/patch_workflow.sh
Normal file
@@ -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 <command> [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 "$@"
|
||||
210
scripts/patch_tools/shop_src_patch_config.sh
Normal file
210
scripts/patch_tools/shop_src_patch_config.sh
Normal file
@@ -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 强制使用本地文件系统
|
||||
Reference in New Issue
Block a user