前言 本期分享vps重要数据如何备份,以个人为例,我备份的内容是图床和书签导航站,这两个会在使用过程中不断添加数据。那既然是备份,自然不可能再备份到vps本机,所以我使用alist+rclone(放弃alist挂载googledrive的方案,原因:个人配置后一周左右就掉盘;推荐使用rclone直接配置挂载goolgedrive,这个时间更长,点击查看挂载教程 )rclone把谷歌云盘挂载到了vps上,这样谷歌云盘就相当于vps的本地盘,方便互传文件。起初是想使用rsync直接对目录同步备份,但是尝试下来发现特别慢,因为小文件太多了,而且不知道为啥会自动断开,故采用先压缩成zip包再上传到云盘。如何自动化压缩并上传呢,这就需要shell脚本来实现了。shell压缩脚本也大致分两种备份方案,一个就是固定时间压缩上传备份,比如每天或每周的固定时间备份,另一个就是根据要备份的目录是否发生变化再判断是否备份。我使用的是后者,使用两个脚来实现备份,监控脚本会实时监控备份目录是否发生变化,发生变化后会生成标志文件,备份脚本会在每天的凌晨两点去找昨天的标志文件,发现标志文件就开始压缩和上传,备份成功后会删掉标志文件。简单说就是在目录发生变化的第二天凌晨两点会自动备份。这种其实也有丢数据的风险,比如当天目录发生了变化,恰巧你的vps在第二天凌晨两点前又出问题了,比如晚上9点机房失火了,那就会丢失当天的数据。最后,备份方案的选择需要根据自身数据的特性来选择,如果经常使用且很重要还是想办法实时备份。我的数据变动的不频繁,每个月偶尔会上传图片和更新书签,所以此备份方案比较适合。
版本1-推荐-已验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 #!/bin/bash # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # 全局变量 SCRIPT_PATH="$(readlink -f "$0")" SCRIPT_NAME="$(basename "$SCRIPT_PATH")" SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" CONFIG_DIR="$SCRIPT_DIR/.backup_config" CONFIG_FILE="$CONFIG_DIR/config.json" LOG_FILE="$CONFIG_DIR/backup.log" TASK_FILE="$CONFIG_DIR/tasks.json" MARK_DIR="$CONFIG_DIR/marks" PID_DIR="$CONFIG_DIR/pids" LOCK_FILE="$CONFIG_DIR/backup_main.lock" # 初始化函数 init() { # 检查依赖 if ! check_dependencies; then echo -e "${RED}依赖检查失败,请解决依赖问题后重试${NC}" exit 1 fi # 创建必要的目录 mkdir -p "$CONFIG_DIR" "$MARK_DIR" "$PID_DIR" # 初始化配置文件(添加默认值) if [ ! -f "$CONFIG_FILE" ]; then cat > "$CONFIG_FILE" <<EOF { "backup_time": "02:00", "backup_retention_count": 3, "log_retention_days": 30 } EOF fi # 初始化任务文件 if [ ! -f "$TASK_FILE" ]; then echo "{}" > "$TASK_FILE" fi # 初始化日志文件 if [ ! -f "$LOG_FILE" ]; then touch "$LOG_FILE" fi # 设置定时任务 update_cron_job } # 检查依赖 check_dependencies() { local missing_deps=() local deps_info=( "inotify-tools:用于监控目录变化,实现实时备份触发(必需)" "zip:用于压缩备份文件,节省存储空间(必需)" "jq:用于处理JSON格式的配置文件,提供更好的配置管理(可选,但推荐安装)" ) # 检查每个依赖 for dep_info in "${deps_info[@]}"; do local dep_name=${dep_info%%:*} local dep_desc=${dep_info#*:} # 特殊处理 inotify-tools if [ "$dep_name" = "inotify-tools" ]; then if ! command -v inotifywait >/dev/null 2>&1; then missing_deps+=("$dep_name") echo -e "${YELLOW}缺少依赖: $dep_name${NC}" echo " 用途: $dep_desc" fi else if ! command -v "$dep_name" >/dev/null 2>&1; then missing_deps+=("$dep_name") echo -e "${YELLOW}缺少依赖: $dep_name${NC}" echo " 用途: $dep_desc" fi fi done # 如果有缺失的依赖 if [ ${#missing_deps[@]} -gt 0 ]; then echo -e "\n检测到缺少以下依赖:" printf -- "- %s\n" "${missing_deps[@]}" read -p "是否要安装这些依赖? (y/n) " choice if [ "$choice" = "y" ]; then echo -e "\n正在安装依赖..." # 使用apt-get安装依赖 if ! apt-get update > /dev/null 2>&1; then echo -e "${RED}更新软件包列表失败${NC}" return 1 fi local install_success=true for dep in "${missing_deps[@]}"; do echo -e "\n正在安装 $dep..." if ! apt-get install -y "$dep" > /dev/null 2>&1; then echo -e "${RED}安装 $dep 失败${NC}" install_success=false else echo -e "${GREEN}$dep 安装成功${NC}" fi done if [ "$install_success" = "false" ]; then echo -e "\n${RED}部分依赖安装失败,请检查错误信息并手动安装后重试${NC}" return 1 fi echo -e "\n${GREEN}所有依赖安装完成${NC}" # 如果是inotify-tools,需要重新加载系统服务 if [[ " ${missing_deps[@]} " =~ " inotify-tools " ]]; then echo -e "\n${YELLOW}正在重新加载系统服务...${NC}" if ! systemctl daemon-reload > /dev/null 2>&1; then echo -e "${RED}重新加载系统服务失败${NC}" echo -e "${YELLOW}请尝试重启系统或手动运行: sudo systemctl daemon-reload${NC}" return 1 fi # 等待服务生效 echo -e "${YELLOW}等待服务生效...${NC}" sleep 2 # 检查 inotify 系统限制 local max_watches=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null) if [ -n "$max_watches" ] && [ "$max_watches" -lt 524288 ]; then echo -e "${YELLOW}警告: inotify 监视限制较低 ($max_watches)${NC}" echo -e "${YELLOW}建议增加限制,运行以下命令:${NC}" echo "echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf" echo "sudo sysctl -p" fi fi # 重新检查依赖是否真的安装成功 for dep in "${missing_deps[@]}"; do if [ "$dep" = "inotify-tools" ]; then if ! command -v inotifywait >/dev/null 2>&1; then echo -e "${RED}警告: $dep 安装后仍然无法使用${NC}" echo -e "${YELLOW}请尝试以下操作:${NC}" echo "1. 重启系统" echo "2. 或手动运行: sudo systemctl daemon-reload" echo "3. 或检查 /proc/sys/fs/inotify/max_user_watches 的值" echo "4. 或运行: sudo modprobe inotify" return 1 fi else if ! command -v "$dep" >/dev/null 2>&1; then echo -e "${RED}警告: $dep 安装后仍然无法使用${NC}" return 1 fi fi done else echo -e "\n${RED}缺少必要依赖,无法继续运行${NC}" return 1 fi fi return 0 } # 设置定时任务 setup_cron() { # 检查是否已经设置了定时任务 if ! crontab -l 2>/dev/null | grep -q "$SCRIPT_PATH --schedule"; then (crontab -l 2>/dev/null; echo "0 2 * * * $SCRIPT_PATH --schedule") | crontab - log "已设置定时任务" "INFO" fi } # 日志函数 log() { local message="$1" local level="${2:-INFO}" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE" # echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} [$level] $message" # 不再输出到终端 } # 显示菜单 show_menu() { clear echo -e "${GREEN}=== 备份管理系统 ===${NC}" echo "1. 添加备份任务" echo "2. 批量添加任务" echo "3. 查看所有任务" echo "4. 暂停/恢复任务" echo "5. 删除任务" echo "6. 查看日志" echo "7. 查看状态" echo "8. 立即备份" echo "9. 系统设置" echo "0. 退出" echo -e "${YELLOW}请选择操作 (0-9):${NC}" } # 添加备份任务 add_backup_task() { while true; do echo -e "${GREEN}添加新的备份任务${NC}" # 使用 read -e 启用目录补全 read -e -p "请输入要备份的源目录: " source_dir # 检查源目录 if [ ! -d "$source_dir" ]; then log "错误: 源目录不存在" "ERROR" return 1 fi # 获取源目录的最后一层目录名作为默认任务名称 default_task_name=$(basename "$source_dir") # 获取任务名称 read -p "请输入备份任务名称 (直接回车使用默认名称 '$default_task_name'): " task_name if [ -z "$task_name" ]; then task_name="$default_task_name" fi # 获取多个目标目录 target_dirs=() while true; do read -e -p "请输入备份目标目录 (输入空行结束): " target_dir if [ -z "$target_dir" ]; then break fi # 检查目标目录 if [ ! -d "$target_dir" ]; then read -p "目标目录不存在,是否创建? (y/n): " create_dir if [ "$create_dir" = "y" ]; then mkdir -p "$target_dir" else continue fi fi target_dirs+=("$target_dir") done if [ ${#target_dirs[@]} -eq 0 ]; then log "错误: 未指定任何目标目录" "ERROR" return 1 fi # 生成任务ID task_id=$(date +%s) # 添加到任务文件 local target_dirs_json=$(printf '%s\n' "${target_dirs[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source_dir" \ --arg name "$task_name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" # 启动监控进程 start_monitoring "$task_id" "$source_dir" "${target_dirs[@]}" & # 保存PID echo $! > "$PID_DIR/${task_id}.pid" log "添加新的备份任务: $task_name ($source_dir -> ${target_dirs[*]})" "INFO" echo -e "${GREEN}备份任务已添加${NC}" # 询问是否继续添加 read -p "是否继续添加备份任务?(y/n,回车默认否): " add_more if [ "$add_more" != "y" ] && [ "$add_more" != "Y" ]; then break fi done } # 批量添加任务 batch_add_tasks() { echo -e "${GREEN}批量添加备份任务${NC}" read -e -p "请输入批量任务文件路径: " batch_file if [ ! -f "$batch_file" ]; then echo -e "${RED}文件不存在${NC}" read -p "按回车键继续..." return fi local success_count=0 local fail_count=0 local success_msgs=() local fail_msgs=() # 判断文件格式 first_line=$(head -n 1 "$batch_file") if [[ "$first_line" =~ ^\[|^\{ ]]; then # JSON格式 tasks=$(jq -c '.[]' "$batch_file" 2>/dev/null) if [ -z "$tasks" ]; then echo -e "${RED}JSON格式解析失败${NC}" read -p "按回车键继续..." return fi echo "$tasks" | while read -r task; do name=$(echo "$task" | jq -r '.name // empty') source=$(echo "$task" | jq -r '.source // empty') targets=($(echo "$task" | jq -r '.targets[]? // empty')) # 去除 \r name=$(echo "$name" | tr -d '\r') source=$(echo "$source" | tr -d '\r') for i in "${!targets[@]}"; do targets[$i]=$(echo "${targets[$i]}" | tr -d '\r') done if [ -z "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录为空") ((fail_count++)) continue fi if [ ! -d "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录不存在") ((fail_count++)) continue fi # 检查目标目录 valid_targets=() target_error="" for target_dir in "${targets[@]}"; do if [ -z "$target_dir" ]; then target_error="目标目录为空" continue fi if [ ! -d "$target_dir" ]; then mkdir -p "$target_dir" 2>/dev/null if [ ! -d "$target_dir" ]; then target_error="目标目录无法创建: $target_dir" break fi fi valid_targets+=("$target_dir") done if [ -n "$target_error" ] || [ ${#valid_targets[@]} -eq 0 ]; then fail_msgs+=("失败: $name ($source) $target_error") ((fail_count++)) continue fi # 生成任务ID task_id=$(date +%s%N | cut -c1-13) local target_dirs_json=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source" \ --arg name "$name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source" "${valid_targets[@]}" & echo $! > "$PID_DIR/${task_id}.pid" log "批量添加任务: $name ($source -> ${valid_targets[*]})" "INFO" success_msgs+=("成功: $name ($source)") ((success_count++)) sleep 0.1 done else # 文本/CSV/TSV格式 while IFS= read -r line || [ -n "$line" ]; do # 跳过空行和注释 [[ -z "$line" || "$line" =~ ^# ]] && continue # 自动识别分隔符 if [[ "$line" == *,* ]]; then IFS=',' read -ra arr <<< "$line" elif [[ "$line" == *$'\t'* ]]; then IFS=$'\t' read -ra arr <<< "$line" else read -ra arr <<< "$line" fi # 至少要有源目录和目标目录 if [ ${#arr[@]} -lt 2 ]; then continue fi name="${arr[0]}" source="${arr[1]}" targets=("${arr[@]:2}") # 去除 \r name=$(echo "$name" | tr -d '\r') source=$(echo "$source" | tr -d '\r') for i in "${!targets[@]}"; do targets[$i]=$(echo "${targets[$i]}" | tr -d '\r') done # 如果任务名为空,自动用源目录最后一层 if [ -z "$name" ]; then name=$(basename "$source") fi if [ -z "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录为空") ((fail_count++)) continue fi if [ ! -d "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录不存在") ((fail_count++)) continue fi # 检查目标目录 valid_targets=() target_error="" for target_dir in "${targets[@]}"; do if [ -z "$target_dir" ]; then target_error="目标目录为空" continue fi if [ ! -d "$target_dir" ]; then mkdir -p "$target_dir" 2>/dev/null if [ ! -d "$target_dir" ]; then target_error="目标目录无法创建: $target_dir" break fi fi valid_targets+=("$target_dir") done if [ -n "$target_error" ] || [ ${#valid_targets[@]} -eq 0 ]; then fail_msgs+=("失败: $name ($source) $target_error") ((fail_count++)) continue fi # 生成任务ID task_id=$(date +%s%N | cut -c1-13) local target_dirs_json=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source" \ --arg name "$name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source" "${valid_targets[@]}" & echo $! > "$PID_DIR/${task_id}.pid" log "批量添加任务: $name ($source -> ${valid_targets[*]})" "INFO" success_msgs+=("成功: $name ($source)") ((success_count++)) sleep 0.1 done < "$batch_file" fi echo -e "\n${GREEN}批量添加任务结果:${NC}" echo -e "成功: $success_count 条" echo -e "失败: $fail_count 条" for msg in "${success_msgs[@]}"; do echo -e "${GREEN}$msg${NC}" done for msg in "${fail_msgs[@]}"; do echo -e "${RED}$msg${NC}" done read -p "按回车键返回主菜单..." } # 启动监控 start_monitoring() { local task_id=$1 local source_dir=$2 local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") # 检查是否已有监控 pids=($(pgrep -f "inotifywait -m -r -e modify,create,delete,move $source_dir")) if [ "${#pids[@]}" -gt 0 ]; then log "已存在监控进程(PID: ${pids[*]}),不重复启动" "INFO" return fi # 检查目录 if [ ! -d "$source_dir" ]; then log "错误: 监控目录不存在: $source_dir" "ERROR" return 1 fi # 启动新的监控进程 inotifywait -m -r -e modify,create,delete,move "$source_dir" 2>> "$LOG_FILE" | { read -r _ # 丢弃第一行,防止刚启动时误报 while read -r directory events filename; do local today=$(date +%Y-%m-%d) local mark_file="$MARK_DIR/${task_name}_${today}.mark" if [ ! -f "$mark_file" ]; then touch "$mark_file" log "检测到 $source_dir 变化,创建标记文件: $mark_file" "INFO" fi done } & echo $! > "$PID_DIR/${task_id}.pid" } # 列出所有任务 list_tasks() { echo -e "${GREEN}当前备份任务列表:${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 使用jq格式化输出 jq -r 'to_entries[] | "ID: \(.key)\n名称: \(.value.name)\n源目录: \(.value.source)\n目标目录: \(.value.targets[])\n状态: \(.value.status)\n创建时间: \(.value.created)\n"' "$TASK_FILE" | \ while IFS= read -r line; do if [[ $line =~ ^名称:\ (.+)$ ]]; then task_name="${BASH_REMATCH[1]}" echo -e "${RED}任务名称: $task_name${NC}" elif [[ $line =~ ^ID:\ (.+)$ ]]; then task_id="${BASH_REMATCH[1]}" echo -e "任务ID: $task_id" elif [[ $line =~ ^源目录:\ (.+)$ ]]; then echo -e "源目录: ${BASH_REMATCH[1]}" elif [[ $line =~ ^目标目录:\ (.+)$ ]]; then echo -e "目标目录: ${BASH_REMATCH[1]}" elif [[ $line =~ ^状态:\ (.+)$ ]]; then status="${BASH_REMATCH[1]}" if [ "$status" = "active" ]; then echo -e "状态: ${GREEN}运行中${NC}" else echo -e "状态: ${RED}已停止${NC}" fi elif [[ $line =~ ^创建时间:\ (.+)$ ]]; then echo -e "创建时间: $(date -d @${BASH_REMATCH[1]} '+%Y-%m-%d %H:%M:%S')" echo -e "${YELLOW}----------------------------------------${NC}" fi done } # 暂停/恢复任务 pause_resume_task() { while true; do clear echo -e "${YELLOW}暂停/恢复备份任务${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 显示当前任务 list_tasks echo -e "\n${GREEN}操作选项:${NC}" echo "1. 输入任务ID(支持多选,空格分隔)暂停/恢复任务" echo "2. 全部暂停/恢复" echo "3. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入要操作的任务ID(可多个,空格分隔): " ids for task_id in $ids; do # 检查任务是否存在 if ! jq -e --arg id "$task_id" 'has($id)' "$TASK_FILE" > /dev/null; then echo -e "${RED}错误: 任务 $task_id 不存在${NC}" continue fi # 获取当前状态 status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ "$status" = "active" ]; then # 暂停任务 # 停止监控进程 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 pkill -f "inotifywait -m -r -e modify,create,delete,move $source_dir" jq --arg id "$task_id" '.[$id].status = "paused"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" log "已暂停任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已暂停${NC}" elif [ "$status" = "paused" ]; then # 恢复任务 jq --arg id "$task_id" '.[$id].status = "active"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" # 启动监控进程 start_monitoring "$task_id" "$source_dir" & echo $! > "$PID_DIR/${task_id}.pid" log "已恢复任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已恢复${NC}" else echo -e "${YELLOW}任务 $task_id 当前状态为 $status,无法操作${NC}" fi done read -p "按回车键继续..." ;; 2) # 全部暂停/恢复 for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ "$status" = "active" ]; then # 暂停 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi pkill -f "inotifywait -m -r -e modify,create,delete,move $source_dir" jq --arg id "$task_id" '.[$id].status = "paused"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" log "已暂停任务 $task_id" "INFO" elif [ "$status" = "paused" ]; then jq --arg id "$task_id" '.[$id].status = "active"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source_dir" & echo $! > "$PID_DIR/${task_id}.pid" log "已恢复任务 $task_id" "INFO" fi done echo -e "${GREEN}全部任务已切换状态(active<->paused)${NC}" read -p "按回车键继续..." ;; 3) return ;; *) echo -e "${RED}无效的选择${NC}" read -p "按回车键继续..." ;; esac done } # 删除任务 delete_task() { while true; do clear echo -e "${YELLOW}删除备份任务${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 显示当前任务 list_tasks echo -e "\n${GREEN}操作选项:${NC}" echo "1. 输入任务ID(支持多选,空格分隔)删除任务" echo "2. 全部删除" echo "3. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入要删除的任务ID(可多个,空格分隔): " ids for task_id in $ids; do # 检查任务是否存在 if ! jq -e --arg id "$task_id" 'has($id)' "$TASK_FILE" > /dev/null; then echo -e "${RED}错误: 任务 $task_id 不存在${NC}" continue fi # 获取任务名称和源目录 local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") # 停止监控进程 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 ps aux | grep inotifywait | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 # 强制查杀所有与该任务源目录相关的 bash 监控子进程 ps aux | grep bash | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 jq --arg id "$task_id" 'del(.[$id])' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" rm -f "$MARK_DIR/"*_${task_name}_${task_id}.mark log "已删除任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已删除${NC}" done # 检查是否还有其他任务 if [ $(jq 'length' "$TASK_FILE") -eq 0 ]; then (crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule") | crontab - log "已删除定时任务(无活动任务)" "INFO" fi read -p "按回车键继续..." ;; 2) # 全部删除 for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 ps aux | grep inotifywait | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 # 强制查杀所有与该任务源目录相关的 bash 监控子进程 ps aux | grep bash | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 jq --arg id "$task_id" 'del(.[$id])' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" rm -f "$MARK_DIR/"*_${task_name}_${task_id}.mark log "已删除任务 $task_id" "INFO" done (crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule") | crontab - log "已删除定时任务(无活动任务)" "INFO" # 删除所有标记文件 rm -f "$MARK_DIR"/*.mark echo -e "${GREEN}所有任务已全部删除${NC}" read -p "按回车键继续..." ;; 3) return ;; *) echo -e "${RED}无效的选择${NC}" read -p "按回车键继续..." ;; esac done } # 查看日志 view_logs() { echo -e "${GREEN}备份日志:${NC}" tail -n 50 "$LOG_FILE" } # 查看状态 check_status() { echo -e "${GREEN}系统状态:${NC}" echo "配置文件: $CONFIG_FILE" echo "日志文件: $LOG_FILE" echo "任务文件: $TASK_FILE" echo "标记目录: $MARK_DIR" echo "PID目录: $PID_DIR" echo list_tasks } # 系统设置 system_settings() { while true; do clear echo -e "${GREEN}=== 系统设置 ===${NC}" echo "1. 设置备份时间" echo "2. 设置备份保留数量" echo "3. 设置日志保留天数" echo "4. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入备份时间 (格式: HH:MM,24小时制,默认 02:00): " backup_time if [ -z "$backup_time" ]; then backup_time="02:00" fi # 验证时间格式 if ! [[ $backup_time =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]]; then echo -e "${RED}时间格式错误,请使用 HH:MM 格式${NC}" read -p "按回车键继续..." continue fi # 获取当前设置的时间 current_time=$(jq -r '.backup_time // "02:00"' "$CONFIG_FILE") # 只在时间发生变化时更新 if [ "$backup_time" != "$current_time" ]; then jq --arg time "$backup_time" '.backup_time = $time' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" # 更新定时任务 update_cron_job echo -e "${GREEN}备份时间已设置为 $backup_time${NC}" else echo -e "${YELLOW}备份时间未发生变化${NC}" fi ;; 2) read -p "请输入备份保留数量 (默认 3): " count if [ -z "$count" ]; then count=3 fi jq --arg count "$count" '.backup_retention_count = $count' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" echo -e "${GREEN}备份保留数量已设置为 $count${NC}" ;; 3) read -p "请输入日志保留天数 (默认 30): " days if [ -z "$days" ]; then days=30 fi jq --arg days "$days" '.log_retention_days = $days' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" echo -e "${GREEN}日志保留天数已设置为 $days${NC}" ;; 4) return ;; *) echo -e "${RED}无效的选择${NC}" ;; esac read -p "按回车键继续..." done } # 更新定时任务 update_cron_job() { local backup_time=$(jq -r '.backup_time // "02:00"' "$CONFIG_FILE") local hour=${backup_time%%:*} local minute=${backup_time#*:} # 删除旧的定时任务 crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule" | crontab - # 添加新的定时任务 (crontab -l 2>/dev/null; echo "$minute $hour * * * $SCRIPT_PATH --schedule") | crontab - log "已更新定时任务为每天 $backup_time 执行" "INFO" } # 检查并补全所有监控进程 check_and_restore_monitors() { for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") if [ "$status" != "active" ]; then continue fi source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") # 检查有多少 inotifywait 正在监控该目录 local count=$(pgrep -f "inotifywait -m -r -e modify,create,delete,move $source_dir" | wc -l) if [ "$count" -eq 0 ]; then start_monitoring "$task_id" "$source_dir" log "自动补全监控进程: $task_id ($source_dir)" "INFO" fi done } # 执行备份 perform_backup() { local task_id=$1 local source_dir=$2 local is_manual=$3 shift 3 local target_dirs=("$@") local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") # 日期处理 if [ "$is_manual" = "true" ]; then local date_part=$(date +"%Y-%m-%d") local mark_file="$MARK_DIR/${task_name}_${date_part}.mark" else local date_part=$(date -d "yesterday" +"%Y-%m-%d") local mark_file="$MARK_DIR/${task_name}_${date_part}.mark" fi # 检查标记文件 if [ ! -f "$mark_file" ]; then log "未检测到 $task_name 的变化,跳过备份" "INFO" return 0 fi log "开始备份 $task_name..." "INFO" # 检查源目录 if [ ! -d "$source_dir" ]; then log "错误: 源目录 $source_dir 不存在" "ERROR" return 1 fi if [ ! -r "$source_dir" ]; then log "错误: 源目录 $source_dir 没有读取权限" "ERROR" return 1 fi # 检查源目录大小 local source_size=$(du -sb "$source_dir" 2>/dev/null | cut -f1) if [ -z "$source_size" ]; then log "错误: 无法获取源目录大小" "ERROR" return 1 fi log "源目录大小: $(numfmt --to=iec-i --suffix=B $source_size)" "INFO" # 检查 /tmp 目录空间 local tmp_space=$(df -B1 /tmp | awk 'NR==2 {print $4}') if [ -z "$tmp_space" ]; then log "错误: 无法获取 /tmp 目录剩余空间" "ERROR" return 1 fi log "/tmp 目录剩余空间: $(numfmt --to=iec-i --suffix=B $tmp_space)" "INFO" # 检查空间是否足够(源目录大小的1.5倍) local required_space=$((source_size * 15 / 10)) if [ "$tmp_space" -lt "$required_space" ]; then log "错误: /tmp 目录空间不足,需要 $(numfmt --to=iec-i --suffix=B $required_space)" "ERROR" return 1 fi # 生成备份文件名 local zip_file="/tmp/${date_part}_${task_name}.zip" # 压缩 log "正在压缩 $source_dir 到 $zip_file..." "INFO" zip -r -v "$zip_file" "$source_dir" > /tmp/zip_log.txt 2>&1 local zip_status=$? if [ $zip_status -ne 0 ]; then log "压缩失败: $task_name (状态码: $zip_status)" "ERROR" log "压缩错误详情: $(cat /tmp/zip_log.txt)" "ERROR" rm -f /tmp/zip_log.txt return 1 fi log "压缩完成: $zip_file" "INFO" log "压缩文件大小: $(numfmt --to=iec-i --suffix=B $(stat -c %s "$zip_file"))" "INFO" rm -f /tmp/zip_log.txt # 处理每个目标目录 for backup_dir in "${target_dirs[@]}"; do # 跳过空目标 if [ -z "$backup_dir" ]; then continue fi # 检查目标目录 if [ ! -d "$backup_dir" ] || [ ! -w "$backup_dir" ]; then log "错误: 备份目录 $backup_dir 不可写或不存在" "ERROR" continue fi # 检查目标目录空间 local backup_space=$(df -B1 "$backup_dir" | awk 'NR==2 {print $4}') if [ -z "$backup_space" ]; then log "错误: 无法获取备份目录剩余空间" "ERROR" continue fi log "备份目录剩余空间: $(numfmt --to=iec-i --suffix=B $backup_space)" "INFO" if [ "$backup_space" -lt "$(stat -c %s "$zip_file")" ]; then log "错误: 备份目录空间不足" "ERROR" continue fi # 检查同名文件 if [ -f "$backup_dir/$(basename $zip_file)" ]; then log "警告: 备份目录中已存在同名文件,重命名新备份" "WARN" zip_file="/tmp/${date_part}_${task_name}_$(date +"%H%M%S").zip" zip -r -v "$zip_file" "$source_dir" > /tmp/zip_log.txt 2>&1 if [ $? -ne 0 ]; then log "重压缩失败: $task_name" "ERROR" log "压缩错误详情: $(cat /tmp/zip_log.txt)" "ERROR" rm -f /tmp/zip_log.txt continue fi rm -f /tmp/zip_log.txt fi # 移动文件 log "正在移动 $zip_file 到 $backup_dir..." "INFO" if mv "$zip_file" "$backup_dir/"; then log "文件移动成功" "INFO" else log "移动文件失败: $backup_dir" "ERROR" continue fi # 维护备份数量(修正匹配规则) cd "$backup_dir" || continue BACKUP_COUNT=$(ls -1 *"${task_name}.zip" 2>/dev/null | wc -l) log "当前备份数量: $BACKUP_COUNT" "INFO" if [ "$BACKUP_COUNT" -gt 3 ]; then log "正在删除旧备份文件..." "INFO" ls -1t *"${task_name}.zip" | tail -n +4 | xargs rm -f if [ $? -eq 0 ]; then log "旧备份删除成功" "INFO" else log "删除旧备份失败" "ERROR" fi fi done # 删除标记文件并校验 if rm -f "$mark_file"; then log "已删除 $task_name 的标记文件 ($mark_file)" "INFO" else log "标记文件删除失败 ($mark_file)" "ERROR" fi log "备份完成: $task_name" "INFO" log "----------------------------------------" "INFO" } # 定时任务处理 handle_scheduled_backup() { # 处理所有活动任务 for task_id in $(jq -r 'to_entries[] | select(.value.status == "active") | .key' "$TASK_FILE"); do local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") local target_dirs=($(jq -r --arg id "$task_id" '.[$id].targets[]' "$TASK_FILE")) if [ "$source_dir" != "null" ] && [ ${#target_dirs[@]} -gt 0 ]; then perform_backup "$task_id" "$source_dir" false "${target_dirs[@]}" fi done } # 立即备份 backup_now() { echo -e "${YELLOW}立即执行备份${NC}" list_tasks read -p "请输入要备份的任务ID: " task_id source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") target_dirs=($(jq -r --arg id "$task_id" '.[$id].targets[]' "$TASK_FILE")) if [ "$source_dir" = "null" ] || [ ${#target_dirs[@]} -eq 0 ]; then log "错误: 任务不存在" "ERROR" return 1 fi perform_backup "$task_id" "$source_dir" true "${target_dirs[@]}" } # 主循环 main() { # 主进程加锁 if [ -f "$LOCK_FILE" ]; then oldpid=$(cat "$LOCK_FILE") if ps -p "$oldpid" > /dev/null 2>&1; then echo -e "${RED}已有主进程在运行,PID: $oldpid,禁止重复启动!${NC}" exit 1 else rm -f "$LOCK_FILE" fi fi echo $$ > "$LOCK_FILE" trap 'rm -f "$LOCK_FILE"; exit' EXIT # 检查是否是定时任务调用 if [ "$1" = "--schedule" ]; then mkdir -p "$CONFIG_DIR" "$MARK_DIR" "$PID_DIR" touch "$LOG_FILE" handle_scheduled_backup # 关键:定时任务应调用自动备份 exit 0 fi # 正常运行时执行完整的初始化 init # 在主菜单前调用自检 check_and_restore_monitors while true; do show_menu read -p "请选择: " choice case $choice in 1) add_backup_task ;; 2) batch_add_tasks ;; 3) list_tasks ;; 4) pause_resume_task ;; 5) delete_task ;; 6) view_logs ;; 7) check_status ;; 8) backup_now ;; 9) system_settings ;; 0) exit 0 ;; *) echo -e "${RED}无效的选择${NC}" ;; esac read -p "按回车键继续..." done } # 启动主程序 main "$@"
版本二-未测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 #!/bin/bash # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # 全局变量 SCRIPT_PATH="$(readlink -f "$0")" SCRIPT_NAME="$(basename "$SCRIPT_PATH")" SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" CONFIG_DIR="$SCRIPT_DIR/.backup_config" CONFIG_FILE="$CONFIG_DIR/config.json" LOG_FILE="$CONFIG_DIR/backup.log" TASK_FILE="$CONFIG_DIR/tasks.json" MARK_DIR="$CONFIG_DIR/marks" PID_DIR="$CONFIG_DIR/pids" LOCK_FILE="$CONFIG_DIR/backup_main.lock" # 初始化函数 init() { # 检查依赖 if ! check_dependencies; then echo -e "${RED}依赖检查失败,请解决依赖问题后重试${NC}" exit 1 fi # 创建必要的目录 mkdir -p "$CONFIG_DIR" "$MARK_DIR" "$PID_DIR" # 初始化配置文件(添加默认值) if [ ! -f "$CONFIG_FILE" ]; then cat > "$CONFIG_FILE" <<EOF { "backup_time": "02:00", "backup_retention_count": 3, "log_retention_days": 30 } EOF fi # 初始化任务文件 if [ ! -f "$TASK_FILE" ]; then echo "{}" > "$TASK_FILE" fi # 初始化日志文件 if [ ! -f "$LOG_FILE" ]; then touch "$LOG_FILE" fi # 设置定时任务 update_cron_job } # 检查依赖 check_dependencies() { local missing_deps=() local deps_info=( "inotify-tools:用于监控目录变化,实现实时备份触发(必需)" "zip:用于压缩备份文件,节省存储空间(必需)" "jq:用于处理JSON格式的配置文件,提供更好的配置管理(可选,但推荐安装)" ) # 检查每个依赖 for dep_info in "${deps_info[@]}"; do local dep_name=${dep_info%%:*} local dep_desc=${dep_info#*:} # 特殊处理 inotify-tools if [ "$dep_name" = "inotify-tools" ]; then if ! command -v inotifywait >/dev/null 2>&1; then missing_deps+=("$dep_name") echo -e "${YELLOW}缺少依赖: $dep_name${NC}" echo " 用途: $dep_desc" fi else if ! command -v "$dep_name" >/dev/null 2>&1; then missing_deps+=("$dep_name") echo -e "${YELLOW}缺少依赖: $dep_name${NC}" echo " 用途: $dep_desc" fi fi done # 如果有缺失的依赖 if [ ${#missing_deps[@]} -gt 0 ]; then echo -e "\n检测到缺少以下依赖:" printf -- "- %s\n" "${missing_deps[@]}" read -p "是否要安装这些依赖? (y/n) " choice if [ "$choice" = "y" ]; then echo -e "\n正在安装依赖..." # 使用apt-get安装依赖 if ! apt-get update > /dev/null 2>&1; then echo -e "${RED}更新软件包列表失败${NC}" return 1 fi local install_success=true for dep in "${missing_deps[@]}"; do echo -e "\n正在安装 $dep..." if ! apt-get install -y "$dep" > /dev/null 2>&1; then echo -e "${RED}安装 $dep 失败${NC}" install_success=false else echo -e "${GREEN}$dep 安装成功${NC}" fi done if [ "$install_success" = "false" ]; then echo -e "\n${RED}部分依赖安装失败,请检查错误信息并手动安装后重试${NC}" return 1 fi echo -e "\n${GREEN}所有依赖安装完成${NC}" # 如果是inotify-tools,需要重新加载系统服务 if [[ " ${missing_deps[@]} " =~ " inotify-tools " ]]; then echo -e "\n${YELLOW}正在重新加载系统服务...${NC}" if ! systemctl daemon-reload > /dev/null 2>&1; then echo -e "${RED}重新加载系统服务失败${NC}" echo -e "${YELLOW}请尝试重启系统或手动运行: sudo systemctl daemon-reload${NC}" return 1 fi # 等待服务生效 echo -e "${YELLOW}等待服务生效...${NC}" sleep 2 # 检查 inotify 系统限制 local max_watches=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null) if [ -n "$max_watches" ] && [ "$max_watches" -lt 524288 ]; then echo -e "${YELLOW}警告: inotify 监视限制较低 ($max_watches)${NC}" echo -e "${YELLOW}建议增加限制,运行以下命令:${NC}" echo "echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf" echo "sudo sysctl -p" fi fi # 重新检查依赖是否真的安装成功 for dep in "${missing_deps[@]}"; do if [ "$dep" = "inotify-tools" ]; then if ! command -v inotifywait >/dev/null 2>&1; then echo -e "${RED}警告: $dep 安装后仍然无法使用${NC}" echo -e "${YELLOW}请尝试以下操作:${NC}" echo "1. 重启系统" echo "2. 或手动运行: sudo systemctl daemon-reload" echo "3. 或检查 /proc/sys/fs/inotify/max_user_watches 的值" echo "4. 或运行: sudo modprobe inotify" return 1 fi else if ! command -v "$dep" >/dev/null 2>&1; then echo -e "${RED}警告: $dep 安装后仍然无法使用${NC}" return 1 fi fi done else echo -e "\n${RED}缺少必要依赖,无法继续运行${NC}" return 1 fi fi return 0 } # 设置定时任务 setup_cron() { # 检查是否已经设置了定时任务 if ! crontab -l 2>/dev/null | grep -q "$SCRIPT_PATH --schedule"; then (crontab -l 2>/dev/null; echo "0 2 * * * $SCRIPT_PATH --schedule") | crontab - log "已设置定时任务" "INFO" fi } # 日志函数 log() { local message="$1" local level="${2:-INFO}" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE" # echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} [$level] $message" # 不再输出到终端 } # 显示菜单 show_menu() { clear echo -e "${GREEN}=== 备份管理系统 ===${NC}" echo "1. 添加备份任务" echo "2. 批量添加任务" echo "3. 查看所有任务" echo "4. 暂停/恢复任务" echo "5. 删除任务" echo "6. 查看日志" echo "7. 查看状态" echo "8. 立即备份" echo "9. 系统设置" echo "0. 退出" echo -e "${YELLOW}请选择操作 (0-9):${NC}" } # 添加备份任务 add_backup_task() { while true; do echo -e "${GREEN}添加新的备份任务${NC}" # 使用 read -e 启用目录补全 read -e -p "请输入要备份的源目录: " source_dir # 检查源目录 if [ ! -d "$source_dir" ]; then log "错误: 源目录不存在" "ERROR" return 1 fi # 获取源目录的最后一层目录名作为默认任务名称 default_task_name=$(basename "$source_dir") # 获取任务名称 read -p "请输入备份任务名称 (直接回车使用默认名称 '$default_task_name'): " task_name if [ -z "$task_name" ]; then task_name="$default_task_name" fi # 获取多个目标目录 target_dirs=() while true; do read -e -p "请输入备份目标目录 (输入空行结束): " target_dir if [ -z "$target_dir" ]; then break fi # 检查目标目录 if [ ! -d "$target_dir" ]; then read -p "目标目录不存在,是否创建? (y/n): " create_dir if [ "$create_dir" = "y" ]; then mkdir -p "$target_dir" else continue fi fi target_dirs+=("$target_dir") done if [ ${#target_dirs[@]} -eq 0 ]; then log "错误: 未指定任何目标目录" "ERROR" return 1 fi # 生成任务ID task_id=$(date +%s) # 添加到任务文件 local target_dirs_json=$(printf '%s\n' "${target_dirs[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source_dir" \ --arg name "$task_name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" # 启动监控进程 start_monitoring "$task_id" "$source_dir" "${target_dirs[@]}" & # 保存PID echo $! > "$PID_DIR/${task_id}.pid" log "添加新的备份任务: $task_name ($source_dir -> ${target_dirs[*]})" "INFO" echo -e "${GREEN}备份任务已添加${NC}" # 询问是否继续添加 read -p "是否继续添加备份任务?(y/n,回车默认否): " add_more if [ "$add_more" != "y" ] && [ "$add_more" != "Y" ]; then break fi done } # 批量添加任务 batch_add_tasks() { echo -e "${GREEN}批量添加备份任务${NC}" read -e -p "请输入批量任务文件路径: " batch_file if [ ! -f "$batch_file" ]; then echo -e "${RED}文件不存在${NC}" read -p "按回车键继续..." return fi local success_count=0 local fail_count=0 local success_msgs=() local fail_msgs=() # 判断文件格式 first_line=$(head -n 1 "$batch_file") if [[ "$first_line" =~ ^\[|^\{ ]]; then # JSON格式 tasks=$(jq -c '.[]' "$batch_file" 2>/dev/null) if [ -z "$tasks" ]; then echo -e "${RED}JSON格式解析失败${NC}" read -p "按回车键继续..." return fi echo "$tasks" | while read -r task; do name=$(echo "$task" | jq -r '.name // empty') source=$(echo "$task" | jq -r '.source // empty') targets=($(echo "$task" | jq -r '.targets[]? // empty')) # 去除 \r name=$(echo "$name" | tr -d '\r') source=$(echo "$source" | tr -d '\r') for i in "${!targets[@]}"; do targets[$i]=$(echo "${targets[$i]}" | tr -d '\r') done if [ -z "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录为空") ((fail_count++)) continue fi if [ ! -d "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录不存在") ((fail_count++)) continue fi # 检查目标目录 valid_targets=() target_error="" for target_dir in "${targets[@]}"; do if [ -z "$target_dir" ]; then target_error="目标目录为空" continue fi if [ ! -d "$target_dir" ]; then mkdir -p "$target_dir" 2>/dev/null if [ ! -d "$target_dir" ]; then target_error="目标目录无法创建: $target_dir" break fi fi valid_targets+=("$target_dir") done if [ -n "$target_error" ] || [ ${#valid_targets[@]} -eq 0 ]; then fail_msgs+=("失败: $name ($source) $target_error") ((fail_count++)) continue fi # 生成任务ID task_id=$(date +%s%N | cut -c1-13) local target_dirs_json=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source" \ --arg name "$name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source" "${valid_targets[@]}" & echo $! > "$PID_DIR/${task_id}.pid" log "批量添加任务: $name ($source -> ${valid_targets[*]})" "INFO" success_msgs+=("成功: $name ($source)") ((success_count++)) sleep 0.1 done else # 文本/CSV/TSV格式 while IFS= read -r line || [ -n "$line" ]; do # 跳过空行和注释 [[ -z "$line" || "$line" =~ ^# ]] && continue # 自动识别分隔符 if [[ "$line" == *,* ]]; then IFS=',' read -ra arr <<< "$line" elif [[ "$line" == *$'\t'* ]]; then IFS=$'\t' read -ra arr <<< "$line" else read -ra arr <<< "$line" fi # 至少要有源目录和目标目录 if [ ${#arr[@]} -lt 2 ]; then continue fi name="${arr[0]}" source="${arr[1]}" targets=("${arr[@]:2}") # 去除 \r name=$(echo "$name" | tr -d '\r') source=$(echo "$source" | tr -d '\r') for i in "${!targets[@]}"; do targets[$i]=$(echo "${targets[$i]}" | tr -d '\r') done # 如果任务名为空,自动用源目录最后一层 if [ -z "$name" ]; then name=$(basename "$source") fi if [ -z "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录为空") ((fail_count++)) continue fi if [ ! -d "$source" ]; then fail_msgs+=("失败: $name ($source) 源目录不存在") ((fail_count++)) continue fi # 检查目标目录 valid_targets=() target_error="" for target_dir in "${targets[@]}"; do if [ -z "$target_dir" ]; then target_error="目标目录为空" continue fi if [ ! -d "$target_dir" ]; then mkdir -p "$target_dir" 2>/dev/null if [ ! -d "$target_dir" ]; then target_error="目标目录无法创建: $target_dir" break fi fi valid_targets+=("$target_dir") done if [ -n "$target_error" ] || [ ${#valid_targets[@]} -eq 0 ]; then fail_msgs+=("失败: $name ($source) $target_error") ((fail_count++)) continue fi # 生成任务ID task_id=$(date +%s%N | cut -c1-13) local target_dirs_json=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) jq --arg id "$task_id" \ --arg source "$source" \ --arg name "$name" \ --argjson targets "$target_dirs_json" \ '. + {($id): {"name": $name, "source": $source, "targets": $targets, "status": "active", "created": "'$(date +%s)'"}}' \ "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source" "${valid_targets[@]}" & echo $! > "$PID_DIR/${task_id}.pid" log "批量添加任务: $name ($source -> ${valid_targets[*]})" "INFO" success_msgs+=("成功: $name ($source)") ((success_count++)) sleep 0.1 done < "$batch_file" fi echo -e "\n${GREEN}批量添加任务结果:${NC}" echo -e "成功: $success_count 条" echo -e "失败: $fail_count 条" for msg in "${success_msgs[@]}"; do echo -e "${GREEN}$msg${NC}" done for msg in "${fail_msgs[@]}"; do echo -e "${RED}$msg${NC}" done read -p "按回车键返回主菜单..." } # 启动监控 start_monitoring() { local task_id=$1 local source_dir=$2 local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") # 检查是否已有监控 pids=($(pgrep -f "inotifywait -m -r -e modify,create,delete,move $source_dir")) if [ "${#pids[@]}" -gt 0 ]; then log "已存在监控进程(PID: ${pids[*]}),不重复启动" "INFO" return fi # 检查目录 if [ ! -d "$source_dir" ]; then log "错误: 监控目录不存在: $source_dir" "ERROR" return 1 fi # 启动新的监控进程 inotifywait -m -r -e modify,create,delete,move "$source_dir" 2>> "$LOG_FILE" | { read -r _ # 丢弃第一行,防止刚启动时误报 while read -r directory events filename; do local today=$(date +%Y-%m-%d) local mark_file="$MARK_DIR/${task_name}_${today}.mark" if [ ! -f "$mark_file" ]; then touch "$mark_file" log "检测到 $source_dir 变化,创建标记文件: $mark_file" "INFO" fi done } & echo $! > "$PID_DIR/${task_id}.pid" } # 列出所有任务 list_tasks() { echo -e "${GREEN}当前备份任务列表:${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 使用jq格式化输出 jq -r 'to_entries[] | "ID: \(.key)\n名称: \(.value.name)\n源目录: \(.value.source)\n目标目录: \(.value.targets[])\n状态: \(.value.status)\n创建时间: \(.value.created)\n"' "$TASK_FILE" | \ while IFS= read -r line; do if [[ $line =~ ^名称:\ (.+)$ ]]; then task_name="${BASH_REMATCH[1]}" echo -e "${RED}任务名称: $task_name${NC}" elif [[ $line =~ ^ID:\ (.+)$ ]]; then task_id="${BASH_REMATCH[1]}" echo -e "任务ID: $task_id" elif [[ $line =~ ^源目录:\ (.+)$ ]]; then echo -e "源目录: ${BASH_REMATCH[1]}" elif [[ $line =~ ^目标目录:\ (.+)$ ]]; then echo -e "目标目录: ${BASH_REMATCH[1]}" elif [[ $line =~ ^状态:\ (.+)$ ]]; then status="${BASH_REMATCH[1]}" if [ "$status" = "active" ]; then echo -e "状态: ${GREEN}运行中${NC}" else echo -e "状态: ${RED}已停止${NC}" fi elif [[ $line =~ ^创建时间:\ (.+)$ ]]; then echo -e "创建时间: $(date -d @${BASH_REMATCH[1]} '+%Y-%m-%d %H:%M:%S')" echo -e "${YELLOW}----------------------------------------${NC}" fi done } # 暂停/恢复任务 pause_resume_task() { while true; do clear echo -e "${YELLOW}暂停/恢复备份任务${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 显示当前任务 list_tasks echo -e "\n${GREEN}操作选项:${NC}" echo "1. 输入任务ID(支持多选,空格分隔)暂停/恢复任务" echo "2. 全部暂停/恢复" echo "3. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入要操作的任务ID(可多个,空格分隔): " ids for task_id in $ids; do # 检查任务是否存在 if ! jq -e --arg id "$task_id" 'has($id)' "$TASK_FILE" > /dev/null; then echo -e "${RED}错误: 任务 $task_id 不存在${NC}" continue fi # 获取当前状态 status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ "$status" = "active" ]; then # 暂停任务 # 停止监控进程 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 pkill -f "inotifywait -m -r -e modify,create,delete,move $source_dir" jq --arg id "$task_id" '.[$id].status = "paused"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" log "已暂停任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已暂停${NC}" elif [ "$status" = "paused" ]; then # 恢复任务 jq --arg id "$task_id" '.[$id].status = "active"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" # 启动监控进程 start_monitoring "$task_id" "$source_dir" & echo $! > "$PID_DIR/${task_id}.pid" log "已恢复任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已恢复${NC}" else echo -e "${YELLOW}任务 $task_id 当前状态为 $status,无法操作${NC}" fi done read -p "按回车键继续..." ;; 2) # 全部暂停/恢复 for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ "$status" = "active" ]; then # 暂停 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi pkill -f "inotifywait -m -r -e modify,create,delete,move $source_dir" jq --arg id "$task_id" '.[$id].status = "paused"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" log "已暂停任务 $task_id" "INFO" elif [ "$status" = "paused" ]; then jq --arg id "$task_id" '.[$id].status = "active"' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" start_monitoring "$task_id" "$source_dir" & echo $! > "$PID_DIR/${task_id}.pid" log "已恢复任务 $task_id" "INFO" fi done echo -e "${GREEN}全部任务已切换状态(active<->paused)${NC}" read -p "按回车键继续..." ;; 3) return ;; *) echo -e "${RED}无效的选择${NC}" read -p "按回车键继续..." ;; esac done } # 删除任务 delete_task() { while true; do clear echo -e "${YELLOW}删除备份任务${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 显示当前任务 list_tasks echo -e "\n${GREEN}操作选项:${NC}" echo "1. 输入任务ID(支持多选,空格分隔)删除任务" echo "2. 全部删除" echo "3. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入要删除的任务ID(可多个,空格分隔): " ids for task_id in $ids; do # 检查任务是否存在 if ! jq -e --arg id "$task_id" 'has($id)' "$TASK_FILE" > /dev/null; then echo -e "${RED}错误: 任务 $task_id 不存在${NC}" continue fi # 获取任务名称和源目录 local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") # 停止监控进程 if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 ps aux | grep inotifywait | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 # 强制查杀所有与该任务源目录相关的 bash 监控子进程 ps aux | grep bash | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 jq --arg id "$task_id" 'del(.[$id])' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" rm -f "$MARK_DIR/"*_${task_name}_${task_id}.mark log "已删除任务 $task_id" "INFO" echo -e "${GREEN}任务 $task_id 已删除${NC}" done # 检查是否还有其他任务 if [ $(jq 'length' "$TASK_FILE") -eq 0 ]; then (crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule") | crontab - log "已删除定时任务(无活动任务)" "INFO" fi read -p "按回车键继续..." ;; 2) # 全部删除 for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") if [ -f "$PID_DIR/${task_id}.pid" ]; then local pid=$(cat "$PID_DIR/${task_id}.pid") if ps -p "$pid" > /dev/null 2>&1; then kill "$pid" 2>/dev/null for i in {1..5}; do if ! ps -p "$pid" > /dev/null 2>&1; then break; fi sleep 1 done if ps -p "$pid" > /dev/null 2>&1; then kill -9 "$pid" 2>/dev/null fi fi rm "$PID_DIR/${task_id}.pid" fi # 强制查杀所有与该任务源目录相关的 inotifywait 进程 ps aux | grep inotifywait | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 # 强制查杀所有与该任务源目录相关的 bash 监控子进程 ps aux | grep bash | grep "$source_dir" | grep -v grep | awk '{print $2}' | xargs -r kill -9 jq --arg id "$task_id" 'del(.[$id])' "$TASK_FILE" > tmp_tasks.json && mv tmp_tasks.json "$TASK_FILE" rm -f "$MARK_DIR/"*_${task_name}_${task_id}.mark log "已删除任务 $task_id" "INFO" done (crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule") | crontab - log "已删除定时任务(无活动任务)" "INFO" # 删除所有标记文件 rm -f "$MARK_DIR"/*.mark echo -e "${GREEN}所有任务已全部删除${NC}" read -p "按回车键继续..." ;; 3) return ;; *) echo -e "${RED}无效的选择${NC}" read -p "按回车键继续..." ;; esac done } # 查看日志 view_logs() { echo -e "${GREEN}备份日志:${NC}" tail -n 50 "$LOG_FILE" } # 查看状态 check_status() { echo -e "${GREEN}=== 系统状态 ===${NC}" echo -e "${YELLOW}----------------------------------------${NC}" # 1. 系统配置信息 echo -e "${BLUE}系统配置:${NC}" echo "配置文件: $CONFIG_FILE" echo "日志文件: $LOG_FILE" echo "任务文件: $TASK_FILE" echo "标记目录: $MARK_DIR" echo "PID目录: $PID_DIR" # 2. 系统运行状态 echo -e "\n${BLUE}运行状态:${NC}" echo "当前时间: $(date '+%Y-%m-%d %H:%M:%S')" if command -v uptime >/dev/null 2>&1; then echo "系统运行时间: $(uptime -p)" else echo "系统运行时间: 无法获取" fi # 3. 任务统计 echo -e "\n${BLUE}任务统计:${NC}" if [ -f "$TASK_FILE" ]; then local total_tasks=$(jq 'length' "$TASK_FILE" 2>/dev/null || echo "0") local active_tasks=$(jq 'to_entries | map(select(.value.status == "active")) | length' "$TASK_FILE" 2>/dev/null || echo "0") local paused_tasks=$(jq 'to_entries | map(select(.value.status == "paused")) | length' "$TASK_FILE" 2>/dev/null || echo "0") echo "总任务数: $total_tasks" echo "运行中任务: $active_tasks" echo "已暂停任务: $paused_tasks" else echo "任务文件不存在" fi # 4. 磁盘空间信息 echo -e "\n${BLUE}磁盘空间:${NC}" if command -v df >/dev/null 2>&1; then df -h 2>/dev/null | grep -v "tmpfs" | grep -v "udev" || echo "无法获取磁盘信息" else echo "无法获取磁盘信息" fi # 5. 最近备份记录 echo -e "\n${BLUE}最近备份记录:${NC}" if [ -f "$LOG_FILE" ]; then tail -n 5 "$LOG_FILE" 2>/dev/null | grep "备份完成" || echo "暂无备份记录" else echo "日志文件不存在" fi echo -e "\n${YELLOW}----------------------------------------${NC}" echo -e "${GREEN}提示: 输入 3 查看详细任务列表${NC}" } # 系统设置 system_settings() { while true; do clear echo -e "${GREEN}=== 系统设置 ===${NC}" echo "1. 设置备份时间" echo "2. 设置备份保留数量" echo "3. 设置日志保留天数" echo "4. 返回主菜单" read -p "请选择: " choice case $choice in 1) read -p "请输入备份时间 (格式: HH:MM,24小时制,默认 02:00): " backup_time if [ -z "$backup_time" ]; then backup_time="02:00" fi # 验证时间格式 if ! [[ $backup_time =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]]; then echo -e "${RED}时间格式错误,请使用 HH:MM 格式${NC}" read -p "按回车键继续..." continue fi # 获取当前设置的时间 current_time=$(jq -r '.backup_time // "02:00"' "$CONFIG_FILE") # 只在时间发生变化时更新 if [ "$backup_time" != "$current_time" ]; then jq --arg time "$backup_time" '.backup_time = $time' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" # 更新定时任务 update_cron_job echo -e "${GREEN}备份时间已设置为 $backup_time${NC}" else echo -e "${YELLOW}备份时间未发生变化${NC}" fi ;; 2) read -p "请输入备份保留数量 (默认 3): " count if [ -z "$count" ]; then count=3 fi jq --arg count "$count" '.backup_retention_count = $count' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" echo -e "${GREEN}备份保留数量已设置为 $count${NC}" ;; 3) read -p "请输入日志保留天数 (默认 30): " days if [ -z "$days" ]; then days=30 fi jq --arg days "$days" '.log_retention_days = $days' "$CONFIG_FILE" > tmp_config.json && mv tmp_config.json "$CONFIG_FILE" echo -e "${GREEN}日志保留天数已设置为 $days${NC}" ;; 4) return ;; *) echo -e "${RED}无效的选择${NC}" ;; esac read -p "按回车键继续..." done } # 更新定时任务 update_cron_job() { local backup_time=$(jq -r '.backup_time // "02:00"' "$CONFIG_FILE") local hour=${backup_time%%:*} local minute=${backup_time#*:} # 删除旧的定时任务 crontab -l 2>/dev/null | grep -v "$SCRIPT_PATH --schedule" | crontab - # 添加新的定时任务 (crontab -l 2>/dev/null; echo "$minute $hour * * * $SCRIPT_PATH --schedule") | crontab - log "已更新定时任务为每天 $backup_time 执行" "INFO" } # 检查并补全所有监控进程 check_and_restore_monitors() { for task_id in $(jq -r 'keys[]' "$TASK_FILE"); do status=$(jq -r --arg id "$task_id" '.[$id].status' "$TASK_FILE") if [ "$status" != "active" ]; then continue fi source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") # 检查有多少 inotifywait 正在监控该目录 local count=$(pgrep -f "inotifywait -m -r -e modify,create,delete,move $source_dir" | wc -l) if [ "$count" -eq 0 ]; then start_monitoring "$task_id" "$source_dir" log "自动补全监控进程: $task_id ($source_dir)" "INFO" fi done } # 执行备份 perform_backup() { local task_id=$1 local source_dir=$2 local is_manual=$3 shift 3 local target_dirs=("$@") local task_name=$(jq -r --arg id "$task_id" '.[$id].name // "backup"' "$TASK_FILE") # 日期处理 if [ "$is_manual" = "true" ]; then local date_part=$(date +"%Y-%m-%d") local mark_file="$MARK_DIR/${task_name}_${date_part}.mark" else local date_part=$(date -d "yesterday" +"%Y-%m-%d") local mark_file="$MARK_DIR/${task_name}_${date_part}.mark" fi # 检查标记文件 if [ ! -f "$mark_file" ]; then log "未检测到 $task_name 的变化,跳过备份" "INFO" return 0 fi log "开始备份 $task_name..." "INFO" # 检查源目录 if [ ! -d "$source_dir" ]; then log "错误: 源目录 $source_dir 不存在" "ERROR" return 1 fi if [ ! -r "$source_dir" ]; then log "错误: 源目录 $source_dir 没有读取权限" "ERROR" return 1 fi # 检查源目录大小 local source_size=$(du -sb "$source_dir" 2>/dev/null | cut -f1) if [ -z "$source_size" ]; then log "错误: 无法获取源目录大小" "ERROR" return 1 fi log "源目录大小: $(numfmt --to=iec-i --suffix=B $source_size)" "INFO" # 检查 /tmp 目录空间 local tmp_space=$(df -B1 /tmp | awk 'NR==2 {print $4}') if [ -z "$tmp_space" ]; then log "错误: 无法获取 /tmp 目录剩余空间" "ERROR" return 1 fi log "/tmp 目录剩余空间: $(numfmt --to=iec-i --suffix=B $tmp_space)" "INFO" # 检查空间是否足够(源目录大小的1.5倍) local required_space=$((source_size * 15 / 10)) if [ "$tmp_space" -lt "$required_space" ]; then log "错误: /tmp 目录空间不足,需要 $(numfmt --to=iec-i --suffix=B $required_space)" "ERROR" return 1 fi # 生成备份文件名 local zip_file="/tmp/${date_part}_${task_name}.zip" # 压缩 log "正在压缩 $source_dir 到 $zip_file..." "INFO" zip -r -v "$zip_file" "$source_dir" > /tmp/zip_log.txt 2>&1 local zip_status=$? if [ $zip_status -ne 0 ]; then log "压缩失败: $task_name (状态码: $zip_status)" "ERROR" log "压缩错误详情: $(cat /tmp/zip_log.txt)" "ERROR" rm -f /tmp/zip_log.txt return 1 fi log "压缩完成: $zip_file" "INFO" log "压缩文件大小: $(numfmt --to=iec-i --suffix=B $(stat -c %s "$zip_file"))" "INFO" rm -f /tmp/zip_log.txt # 处理每个目标目录 for backup_dir in "${target_dirs[@]}"; do # 跳过空目标 if [ -z "$backup_dir" ]; then continue fi # 检查目标目录 if [ ! -d "$backup_dir" ] || [ ! -w "$backup_dir" ]; then log "错误: 备份目录 $backup_dir 不可写或不存在" "ERROR" continue fi # 检查目标目录空间 local backup_space=$(df -B1 "$backup_dir" | awk 'NR==2 {print $4}') if [ -z "$backup_space" ]; then log "错误: 无法获取备份目录剩余空间" "ERROR" continue fi log "备份目录剩余空间: $(numfmt --to=iec-i --suffix=B $backup_space)" "INFO" if [ "$backup_space" -lt "$(stat -c %s "$zip_file")" ]; then log "错误: 备份目录空间不足" "ERROR" continue fi # 检查同名文件 if [ -f "$backup_dir/$(basename $zip_file)" ]; then log "警告: 备份目录中已存在同名文件,重命名新备份" "WARN" zip_file="/tmp/${date_part}_${task_name}_$(date +"%H%M%S").zip" zip -r -v "$zip_file" "$source_dir" > /tmp/zip_log.txt 2>&1 if [ $? -ne 0 ]; then log "重压缩失败: $task_name" "ERROR" log "压缩错误详情: $(cat /tmp/zip_log.txt)" "ERROR" rm -f /tmp/zip_log.txt continue fi rm -f /tmp/zip_log.txt fi # 移动文件 log "正在移动 $zip_file 到 $backup_dir..." "INFO" if mv "$zip_file" "$backup_dir/"; then log "文件移动成功" "INFO" else log "移动文件失败: $backup_dir" "ERROR" continue fi # 维护备份数量(修正匹配规则) cd "$backup_dir" || continue BACKUP_COUNT=$(ls -1 *"${task_name}.zip" 2>/dev/null | wc -l) log "当前备份数量: $BACKUP_COUNT" "INFO" if [ "$BACKUP_COUNT" -gt 3 ]; then log "正在删除旧备份文件..." "INFO" ls -1t *"${task_name}.zip" | tail -n +4 | xargs rm -f if [ $? -eq 0 ]; then log "旧备份删除成功" "INFO" else log "删除旧备份失败" "ERROR" fi fi done # 删除标记文件并校验 if rm -f "$mark_file"; then log "已删除 $task_name 的标记文件 ($mark_file)" "INFO" else log "标记文件删除失败 ($mark_file)" "ERROR" fi log "备份完成: $task_name" "INFO" log "----------------------------------------" "INFO" } # 定时任务处理 handle_scheduled_backup() { # 处理所有活动任务 for task_id in $(jq -r 'to_entries[] | select(.value.status == "active") | .key' "$TASK_FILE"); do local source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") local target_dirs=($(jq -r --arg id "$task_id" '.[$id].targets[]' "$TASK_FILE")) if [ "$source_dir" != "null" ] && [ ${#target_dirs[@]} -gt 0 ]; then perform_backup "$task_id" "$source_dir" false "${target_dirs[@]}" fi done } # 立即备份 backup_now() { echo -e "${YELLOW}立即执行备份${NC}" list_tasks read -p "请输入要备份的任务ID: " task_id source_dir=$(jq -r --arg id "$task_id" '.[$id].source' "$TASK_FILE") target_dirs=($(jq -r --arg id "$task_id" '.[$id].targets[]' "$TASK_FILE")) if [ "$source_dir" = "null" ] || [ ${#target_dirs[@]} -eq 0 ]; then log "错误: 任务不存在" "ERROR" return 1 fi perform_backup "$task_id" "$source_dir" true "${target_dirs[@]}" } # 主循环 main() { # 主进程加锁 if [ -f "$LOCK_FILE" ]; then oldpid=$(cat "$LOCK_FILE") if ps -p "$oldpid" > /dev/null 2>&1; then echo -e "${RED}已有主进程在运行,PID: $oldpid,禁止重复启动!${NC}" exit 1 else rm -f "$LOCK_FILE" fi fi echo $$ > "$LOCK_FILE" trap 'rm -f "$LOCK_FILE"; exit' EXIT # 检查是否是定时任务调用 if [ "$1" = "--schedule" ]; then mkdir -p "$CONFIG_DIR" "$MARK_DIR" "$PID_DIR" touch "$LOG_FILE" handle_scheduled_backup # 关键:定时任务应调用自动备份 exit 0 fi # 正常运行时执行完整的初始化 init # 在主菜单前调用自检 check_and_restore_monitors while true; do show_menu read -p "请选择: " choice case $choice in 1) add_backup_task ;; 2) batch_add_tasks ;; 3) list_tasks ;; 4) pause_resume_task ;; 5) delete_task ;; 6) view_logs ;; 7) check_status ;; 8) backup_now ;; 9) system_settings ;; 0) exit 0 ;; *) echo -e "${RED}无效的选择${NC}" ;; esac read -p "按回车键继续..." done } # 启动主程序 main "$@"
工作原理说明
监控系统:
monitor.sh 使用 inotifywait 持续监控指定目录
当检测到文件变化时,创建当天的标记文件
每个项目都有独立的标记文件
备份系统:
backup.sh 在凌晨2点运行
检查每个项目的标记文件
只备份当天有变化(有标记文件)的项目
每个项目最多保留3个备份版本
日志系统:
所有操作都有日志记录
方便追踪问题和监控系统状态
准备工作 安装必要的软件包 1 2 3 4 5 6 apt-get update apt-get install inotify-tools apt-get install zip
创建目录结构 1 2 3 mkdir -p /root/data/shell_script/backupmkdir -p /root/data/shell_script/backup/marks
部署脚本文件 创建监控配置文件 1 2 vim /root/data/shell_script/backup/monitor_config.conf
写入以下内容(可自己根据需求修改备份目录):
1 2 3 4 5 6 7 8 9 # 监控配置MARK_ DIR="/root/data/shell_ script/backup/marks" SLEEP_ INTERVAL=86400 # 监控目录WATCH_ DIRS=( ["easyimage"]="/root/data/docker_ data/easyimage" ["onenav"]="/root/data/docker_ data/onenav" )
部署监控脚本 1 2 vim /root/data/shell_script/backup/monitor.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 # !/bin/bash # 颜色配置 GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' # 无颜色 # 将脚本转入后台运行 if [ "$1" != "background" ]; then echo -e "${YELLOW}正在后台启动监控脚本...${NC}" # 检查锁文件,防止启动多个进程 if [ -f "${LOCK_FILE}" ]; then pid=$(cat "${LOCK_FILE}") if ps -p "${pid}" > /dev/null 2>&1; then echo -e "${RED}错误: 监控脚本已经在运行,PID: ${pid}。退出...${NC}" exit 1 fi fi $0 background > /dev/null 2>&1 & echo -e "${GREEN}监控脚本成功启动! (PID: $!)${NC}" echo "你可以查看日志: /root/data/shell_script/backup/monitor.log" exit 0 fi # 配置部分 CONFIG_FILE="/root/data/shell_script/backup/monitor_config.conf" LOG_FILE="/root/data/shell_script/backup/monitor.log" LOCK_FILE="/var/run/backup_monitor.pid" # 默认配置 MARK_DIR="/root/data/shell_script/backup/marks" SLEEP_INTERVAL=86400 # 24小时 # 监控目录配置 declare -A WATCH_DIRS=( ["easyimage"]="/root/data/docker_data/easyimage" ["onenav"]="/root/data/docker_data/onenav" ) # 日志函数 log() { local level=$1 shift local message=$@ echo "[$(date +'%Y-%m-%d %H:%M:%S')] [${level}] ${message}" >> "${LOG_FILE}" } # 加载配置文件 load_config() { if [ -f "${CONFIG_FILE}" ]; then log "信息" "加载配置文件: ${CONFIG_FILE}" source "${CONFIG_FILE}" else log "警告" "配置文件未找到,使用默认配置" # 创建默认配置文件 cat > "${CONFIG_FILE}" << EOF # 监控配置 MARK_DIR="/root/data/shell_script/backup/marks" SLEEP_INTERVAL=86400 # 监控目录 WATCH_DIRS=( ["easyimage"]="/root/data/docker_data/easyimage" ["onenav"]="/root/data/docker_data/onenav" ) EOF fi } # 检查是否已运行 check_running() { if [ -f "${LOCK_FILE}" ]; then pid=$(cat "${LOCK_FILE}") if ps -p "${pid}" > /dev/null 2>&1; then log "错误" "监控脚本已经在运行,PID: ${pid}" echo -e "${RED}错误: 监控脚本已经在运行,PID: ${pid}。退出...${NC}" exit 1 else log "警告" "发现过期的锁文件,正在移除" rm -f "${LOCK_FILE}" fi fi echo $$ > "${LOCK_FILE}" } # 清理函数 cleanup() { log "信息" "清理并退出" rm -f "${LOCK_FILE}" pkill -P $$ # 终止所有子进程 exit 0 } # 监控单个目录 monitor_directory() { local project_name=$1 local dir=$2 # 检查目录是否存在 if [ ! -d "${dir}" ]; then log "错误" "项目 ${project_name} 的目录 ${dir} 不存在" return 1 fi log "信息" "开始监控项目 ${project_name},目录: ${dir}" # 启动单个 inotifywait 进程 inotifywait -m -r -e modify,create,delete,move "${dir}" 2>> "${LOG_FILE}" | while read -r directory events filename; do local TODAY=$(date +"%Y-%m-%d") local MARK_FILE="${MARK_DIR}/${project_name}_${TODAY}.mark" if [ ! -f "${MARK_FILE}" ]; then touch "${MARK_FILE}" log "信息" "检测到 ${project_name} 的变化,创建标记文件: ${MARK_FILE}" fi done & } # 主函数 main() { # 设置退出时的清理 trap cleanup SIGTERM SIGINT # 检查必要的工具 if ! command -v inotifywait &> /dev/null; then log "错误" "inotify-tools 未安装,请先安装它。" echo -e "${RED}错误: inotify-tools 未安装,请先安装它。${NC}" exit 1 fi # 创建必要的目录 mkdir -p "${MARK_DIR}" # 加载配置 load_config # 检查是否已运行 check_running log "信息" "监控脚本已启动" # 启动所有目录的监控 for project in "${!WATCH_DIRS[@]}"; do monitor_directory "${project}" "${WATCH_DIRS[${project}]}" log "信息" "已启动监控进程,项目: ${project}, 目录: ${WATCH_DIRS[${project}]}" # 记录启动的项目 done # 保持主进程运行 wait } # 运行主函数 main
监控日志:
部署备份脚本 1 2 vim /root/data/shell_script/backup/backup.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 # !/bin/bash # 日志文件定义 LOG_FILE="/root/data/shell_script/backup/backup.log" # 重定向标准输出和错误输出到日志文件 exec >>"${LOG_FILE}" 2>&1 # 打印时间戳以标记任务开始 echo "==================== $(date +"%Y-%m-%d %H:%M:%S") ====================" # 定义备份函数 backup_directory() { local SOURCE_DIR=$1 local BACKUP_DIR=$2 local PROJECT_NAME=$3 local DATE=$(date -d "-1 day" +"%Y-%m-%d") local ZIP_FILE="/tmp/${DATE}_${PROJECT_NAME}.zip" local MARK_FILE="/root/data/shell_script/backup/marks/${PROJECT_NAME}_${DATE}.mark" # 检查是否存在此项目的标记文件 if [ ! -f "${MARK_FILE}" ]; then echo "No changes detected for ${PROJECT_NAME} today, skipping backup." return 0 fi echo "Starting backup for ${PROJECT_NAME}..." # 检查源目录是否存在 if [ ! -d "${SOURCE_DIR}" ]; then echo "Error: Source directory ${SOURCE_DIR} does not exist. Skipping." return 1 fi # 检查备份目录是否可写 if [ ! -d "${BACKUP_DIR}" ] || [ ! -w "${BACKUP_DIR}" ]; then echo "Error: Backup directory ${BACKUP_DIR} is not writable or does not exist. Skipping." return 1 fi # 检查是否存在同名文件并处理 if [ -f "${BACKUP_DIR}/$(basename ${ZIP_FILE})" ]; then echo "Warning: A backup file with the same name already exists in ${BACKUP_DIR}. Renaming the new backup." ZIP_FILE="/tmp/${DATE}_${PROJECT_NAME}_$(date +"%H%M%S").zip" echo "New backup file name: ${ZIP_FILE}" fi # 压缩文件 echo "Compressing ${SOURCE_DIR} to ${ZIP_FILE}..." zip -r "${ZIP_FILE}" "${SOURCE_DIR}" > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "Error: Compression failed for ${PROJECT_NAME}. Skipping." return 1 fi echo "Compression completed: ${ZIP_FILE}" # 移动到目标备份目录 echo "Moving ${ZIP_FILE} to ${BACKUP_DIR}..." rclone copy "${ZIP_FILE}" "${BACKUP_DIR}" if [ $? -ne 0 ]; then echo "Error: Failed to move file to ${BACKUP_DIR}. Skipping." return 1 fi echo "File moved successfully." rm -f "${ZIP_FILE}" # 维护备份文件数量(最多保留3份) echo "Maintaining backup files in ${BACKUP_DIR}..." cd "${BACKUP_DIR}" || return 1 BACKUP_COUNT=$(ls -1 *_${PROJECT_NAME}.zip 2>/dev/null | wc -l) if [ "${BACKUP_COUNT}" -gt 3 ]; then echo "Deleting old backup files..." ls -1t *_${PROJECT_NAME}.zip | tail -n +4 | xargs rm -f if [ $? -eq 0 ]; then echo "Old backups deleted successfully." else echo "Error: Failed to delete old backups." fi else echo "No old backups to delete. Current count: ${BACKUP_COUNT}" fi # 备份完成后删除标记文件 rm -f "${MARK_FILE}" echo "Removed mark file for ${PROJECT_NAME}" echo "Backup completed for ${PROJECT_NAME}" echo "----------------------------------------" } # 执行 easyimage 备份 backup_directory "/root/data/docker_data/easyimage" "/mnt/Google/backup/easyimage" "easyimage" # 执行 onenav 备份 backup_directory "/root/data/docker_data/onenav" "/mnt/Google/backup/onenav" "onenav" # 记录任务完成时间 echo "All backups completed at $(date +"%Y-%m-%d %H:%M:%S")."
backup.sh如何自定义使用:
只需要修改backup_directory
函数的参数,一共三个参数,以easyimage
备份为例。源目录(/root/data/docker_data/easyimage),目标目录(/mnt/alist/Google/backup/easyimage),和备份项目名称(easyimage)。
1 2 backup_directory "/root/data/docker_data/easyimage" "/mnt/alist/Google/backup/easyimage" "easyimage"
设置脚本权限 1 2 3 chmod +x /root/data/shell_script/backup/monitor.shchmod +x /root/data/shell_script/backup/backup.sh
运行起来后备份脚本日志:
在凌晨两点准时上传前一天的备份
配置自动启动 配置监控脚本开机自启
添加以下内容:
1 @reboot /root/data/shell_script/backup/monitor.sh
配置备份脚本定时执行 在同一个 crontab 中添加:
1 0 2 * * * /root/data/shell_script/backup/backup.sh
说明:0 2 * 表示每天凌晨2点执行,这个时间点系统负载通常较低。
日志文件 系统会自动创建两个日志文件:
监控日志:/root/data/shell_script/backup/monitor.log
备份日志:/root/data/shell_script/backup/backup.log
验证部署 启动监控 1 2 /root/data/shell_script/backup/monitor.sh
检查监控状态 1 2 3 4 5 6 ps aux | grep monitor.sh tail -f /root/data/shell_script/backup/monitor.logpkill -f monitor.sh
添加或修改备份 如需添加或修改备份项目: