前言

本期分享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 "$@"

预留

预留

工作原理说明

  1. 监控系统:

    monitor.sh 使用 inotifywait 持续监控指定目录

    当检测到文件变化时,创建当天的标记文件

    每个项目都有独立的标记文件

  2. 备份系统:

    backup.sh 在凌晨2点运行

    检查每个项目的标记文件

    只备份当天有变化(有标记文件)的项目

    每个项目最多保留3个备份版本

  3. 日志系统:

    所有操作都有日志记录

    方便追踪问题和监控系统状态

准备工作

安装必要的软件包

1
2
3
4
5
6
# 安装 inotify-tools(用于文件监控)
apt-get update
apt-get install inotify-tools

# 安装 zip(用于文件压缩)
apt-get install zip

创建目录结构

1
2
3
# 创建脚本和配置文件目录
mkdir -p /root/data/shell_script/backup
mkdir -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
# 执行 easyimage 备份
backup_directory "/root/data/docker_data/easyimage" "/mnt/alist/Google/backup/easyimage" "easyimage"

设置脚本权限

1
2
3
# 设置可执行权限
chmod +x /root/data/shell_script/backup/monitor.sh
chmod +x /root/data/shell_script/backup/backup.sh

运行起来后备份脚本日志:

备份日志

在凌晨两点准时上传前一天的备份

云盘备份结果

配置自动启动

配置监控脚本开机自启

1
2
# 编辑 crontab
crontab -e

添加以下内容:

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.log
#停止监控
pkill -f monitor.sh

添加或修改备份

如需添加或修改备份项目:

  • 编辑 monitor_config.conf 添加或修改备份目录

  • 编辑 backup.sh 添加或修改备份目录

  • 重启 monitor.sh