#!/bin/bash # 数据备份脚本 # 支持 SQLite 数据库和配置文件的备份 # # 使用方法: # ./scripts/backup/backup.sh # 执行一次备份 # ./scripts/backup/backup.sh --restore # 从最新备份恢复 # ./scripts/backup/backup.sh --list # 列出所有备份 # ./scripts/backup/backup.sh --verify # 验证备份完整性 # # 自动备份 (crontab): # 0 2 * * * /path/to/scripts/backup/backup.sh # 每天凌晨2点 set -e # 配置 BACKUP_DIR="${BACKUP_DIR:-./backups}" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_NAME="user-management_${TIMESTAMP}" DB_PATH="${DB_PATH:-./data/user_management.db}" CONFIG_PATH="${CONFIG_PATH:-./configs/config.yaml}" RETENTION_DAYS="${RETENTION_DAYS:-30}" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # 创建备份目录 mkdir -p "${BACKUP_DIR}" # 备份数据库 backup_database() { local db_file="$1" local backup_file="$2" if [ -f "${db_file}" ]; then log_info "Backing up database: ${db_file}" # 使用 SQLite 的 .backup 命令进行一致性备份 sqlite3 "${db_file}" ".backup '${backup_file}'" log_success "Database backed up to: ${backup_file}" return 0 else log_warning "Database file not found: ${db_file}" return 1 fi } # 备份配置文件 backup_config() { local config_file="$1" local backup_file="$2" if [ -f "${config_file}" ]; then log_info "Backing up config: ${config_file}" cp "${config_file}" "${backup_file}" log_success "Config backed up to: ${backup_file}" return 0 else log_warning "Config file not found: ${config_file}" return 1 fi } # 验证备份完整性 verify_backup() { local backup_file="$1" local file_type="$2" log_info "Verifying backup: ${backup_file}" if [ ! -f "${backup_file}" ]; then log_error "Backup file not found: ${backup_file}" return 1 fi case "${file_type}" in "sqlite") # 验证 SQLite 数据库完整性 if sqlite3 "${backup_file}" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then log_success "SQLite backup is valid" return 0 else log_error "SQLite backup is corrupted" return 1 fi ;; "config") # 验证 YAML 格式 if grep -q "^server:" "${backup_file}"; then log_success "Config backup is valid" return 0 else log_error "Config backup is invalid" return 1 fi ;; esac } # 执行完整备份 do_backup() { log_info "Starting backup..." local backup_subdir="${BACKUP_DIR}/${BACKUP_NAME}" mkdir -p "${backup_subdir}" local db_backup="${backup_subdir}/database.db" local config_backup="${backup_subdir}/config.yaml" local metadata_file="${backup_subdir}/metadata.json" # 备份数据库 backup_database "${DB_PATH}" "${db_backup}" || true # 备份配置 backup_config "${CONFIG_PATH}" "${config_backup}" || true # 创建元数据 cat > "${metadata_file}" << EOF { "timestamp": "${TIMESTAMP}", "backup_name": "${BACKUP_NAME}", "db_path": "${DB_PATH}", "config_path": "${CONFIG_PATH}", "created_at": "$(date -Iseconds)" } EOF # 创建压缩包 local archive_file="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" tar -czf "${archive_file}" -C "${BACKUP_DIR}" "${BACKUP_NAME}" # 计算校验和 local checksum_file="${BACKUP_DIR}/${BACKUP_NAME}.sha256" sha256sum "${archive_file}" > "${checksum_file}" # 清理未压缩的备份目录 rm -rf "${backup_subdir}" log_success "Backup completed: ${archive_file}" log_success "Checksum: $(cat ${checksum_file})" # 清理过期备份 cleanup_old_backups } # 清理过期备份 cleanup_old_backups() { log_info "Cleaning up backups older than ${RETENTION_DAYS} days..." find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true find "${BACKUP_DIR}" -name "*.sha256" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true log_success "Cleanup completed" } # 列出所有备份 list_backups() { log_info "Available backups in ${BACKUP_DIR}:" if [ ! -d "${BACKUP_DIR}" ] || [ -z "$(ls -A ${BACKUP_DIR}/*.tar.gz 2>/dev/null)" ]; then log_warning "No backups found" return fi printf "\n%-40s %15s %20s\n" "BACKUP FILE" "SIZE" "CREATED" printf "%s\n" "------------------------------------------------------------------------" for archive in "${BACKUP_DIR}"/*.tar.gz; do if [ -f "${archive}" ]; then local size=$(du -h "${archive}" | cut -f1) local date=$(date -r "${archive}" "+%Y-%m-%d %H:%M:%S") printf "%-40s %15s %20s\n" "$(basename ${archive})" "${size}" "${date}" fi done } # 恢复备份 do_restore() { local latest_archive=$(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null | head -1) if [ -z "${latest_archive}" ]; then log_error "No backup found to restore" exit 1 fi log_warning "This will overwrite current data with backup: ${latest_archive}" read -p "Are you sure? (yes/no): " confirm if [ "${confirm}" != "yes" ]; then log_info "Restore cancelled" exit 0 fi log_info "Restoring from: ${latest_archive}" # 验证备份 if [ -f "${latest_archive}.sha256" ]; then if ! sha256sum --check "${latest_archive}.sha256"; then log_error "Backup checksum verification failed!" exit 1 fi log_success "Checksum verified" fi # 解压到临时目录 local temp_dir="${BACKUP_DIR}/.restore_temp" rm -rf "${temp_dir}" mkdir -p "${temp_dir}" tar -xzf "${latest_archive}" -C "${temp_dir}" # 查找解压的目录 local restored_subdir=$(find "${temp_dir}" -mindepth 1 -maxdepth 1 -type d | head -1) # 恢复数据库 local db_backup="${restored_subdir}/database.db" if [ -f "${db_backup}" ]; then verify_backup "${db_backup}" "sqlite" log_info "Restoring database..." cp "${db_backup}" "${DB_PATH}" log_success "Database restored" fi # 恢复配置 local config_backup="${restored_subdir}/config.yaml" if [ -f "${config_backup}" ]; then verify_backup "${config_backup}" "config" log_info "Restoring config..." cp "${config_backup}" "${CONFIG_PATH}" log_success "Config restored" fi # 清理临时目录 rm -rf "${temp_dir}" log_success "Restore completed successfully!" } # 验证备份 do_verify() { local latest_archive=$(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null | head -1) if [ -z "${latest_archive}" ]; then log_error "No backup found to verify" exit 1 fi log_info "Verifying latest backup: ${latest_archive}" # 验证校验和 if [ -f "${latest_archive}.sha256" ]; then if sha256sum --check "${latest_archive}.sha256"; then log_success "Checksum verified" else log_error "Checksum verification failed" exit 1 fi fi # 验证压缩包完整性 if tar -tzf "${latest_archive}" > /dev/null 2>&1; then log_success "Archive integrity verified" else log_error "Archive is corrupted" exit 1 fi # 解压并验证内容 local temp_dir="${BACKUP_DIR}/.verify_temp" rm -rf "${temp_dir}" mkdir -p "${temp_dir}" tar -xzf "${latest_archive}" -C "${temp_dir}" local restored_subdir=$(find "${temp_dir}" -mindepth 1 -maxdepth 1 -type d | head -1) local db_backup="${restored_subdir}/database.db" if [ -f "${db_backup}" ]; then verify_backup "${db_backup}" "sqlite" fi rm -rf "${temp_dir}" log_success "Backup verification completed successfully!" } # 显示帮助 show_help() { echo "Usage: $0 [COMMAND]" echo "" echo "Commands:" echo " (no args) Execute a backup" echo " --restore Restore from the latest backup" echo " --list List all backups" echo " --verify Verify the latest backup" echo " --help Show this help message" echo "" echo "Environment variables:" echo " BACKUP_DIR Backup directory (default: ./backups)" echo " DB_PATH Database path (default: ./data/user_management.db)" echo " CONFIG_PATH Config path (default: ./configs/config.yaml)" echo " RETENTION_DAYS Backup retention days (default: 30)" } # 主逻辑 case "${1:-}" in --restore) do_restore ;; --list) list_backups ;; --verify) do_verify ;; --help) show_help ;; "") do_backup ;; *) log_error "Unknown command: $1" show_help exit 1 ;; esac