-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathautosnap.sh
executable file
·266 lines (229 loc) · 7.09 KB
/
autosnap.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
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
#!/usr/bin/env bash
# 设置常量
readonly TIMEZONE="Asia/Shanghai"
readonly MOUNT_DEVICE=$(df / | awk 'END{print $1}') # 获取根分区设备
readonly MOUNT_POINT="/.snapshots/"
readonly SNAPSHOT_DIR="/.snapshots/autosnap"
readonly SUBVOLUME_NAME="@.snapshots"
readonly BOOT_FILE="/boot/loader/entries/arch.conf"
# 获取当前挂载的子卷
readonly CURRENT_SUBVOLUME=${SNAPSHOT_DIR}$(mount | grep " / " | sed -n -e 's/.*subvol=\/\?\([^,)]*\).*/\1/p' | sed -n -e 's/.*\(\/.*\/.*\)/\1/p')
# 设置颜色代码
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly NC='\033[0m' # No Color
# 获取当前日期和时间
readonly NOW=$(TZ=${TIMEZONE} date +'%Y-%m-%d %H-%M-%S')
readonly CURRENT_DATE=$(echo "${NOW}" | cut -d' ' -f1)
readonly CURRENT_TIME=$(echo "${NOW}" | cut -d' ' -f2)
# 实用函数
log_success() { printf "${GREEN}%s${NC}\n" "$*"; }
log_warning() { printf "${YELLOW}%s${NC}\n" "$*"; }
log_error() { printf "${RED}%s${NC}\n" "$*"; }
require_confirm() {
printf "${YELLOW}%s${NC} [y/N] " "${1}"
read -r confirm
if [[ "${confirm}" != "y" ]]; then
return 1
fi
}
check_command() {
if ! command -v "${1}" &>/dev/null; then
log_error "=> ${1} is not installed."
exit 1
fi
}
check_dependencies() {
# 检查依赖
check_command "btrfs"
check_command "df"
check_command "wc"
check_command "awk"
check_command "sed"
check_command "cut"
check_command "find"
check_command "grep"
check_command "sort"
check_command "head"
check_command "tail"
check_command "tree"
check_command "date"
check_command "dirname"
check_command "basename"
check_command "mountpoint"
}
init() {
check_dependencies
# 挂载 @.snapshots 子卷
sudo mount -o "subvol=${SUBVOLUME_NAME}" "${MOUNT_DEVICE}" "${MOUNT_POINT}"
}
quit() {
# 清理 SNAPSHOT_DIR 中的空文件夹
sudo find "$SNAPSHOT_DIR" -maxdepth 1 -mindepth 1 ! -name ".*" -type d -empty -exec echo "Clean: {}" \; -delete
# 卸载 @.snapshots 子卷
while mountpoint -q "${MOUNT_POINT}"; do
sudo umount "${MOUNT_POINT}"
#sleep 1
done
exit "$1"
}
create_snapshot() {
local default_path="${SNAPSHOT_DIR}/${CURRENT_DATE}/@${CURRENT_TIME}"
local snapshot_path=${2:-${default_path}}
local dir=$(dirname "${snapshot_path}")
if [[ ! -d "${dir}" ]]; then
sudo mkdir -p "${dir}"
fi
sudo btrfs subvolume snapshot "${1}" "${snapshot_path}"
log_success "=> Created snapshot: ${snapshot_path}"
}
list_snapshots() {
if [[ "${1:-}" == "all" ]]; then
# 列表列出所有快照
find "${SNAPSHOT_DIR}" -maxdepth 2 -mindepth 2 ! -name ".*"
else
# 默认以树形结构列出所有快照
tree -L 2 "${SNAPSHOT_DIR}"
fi
}
clean_snapshots() {
case "${1:-}" in
"all")
### 清理除当前挂载中的快照外的所有快照
if ! require_confirm "=> Clean all snapshots except current subvolume. Are you sure?"; then
return
fi
find "${SNAPSHOT_DIR}" -maxdepth 2 -mindepth 2 ! -name ".*" ! -path "${CURRENT_SUBVOLUME}" -exec sudo btrfs subvolume delete {} \;
;;
"yesterday")
### 清理前一天的快照目录
local previous_date
previous_date=$(TZ=${TIMEZONE} date -d "yesterday" +'%Y-%m-%d')
# 检查前一天的日期目录是否存在
if [[ -d "${SNAPSHOT_DIR}/${previous_date}" ]]; then
# 如果快照数量小于等于1,则不进行清理
local dir_count=$(find "${SNAPSHOT_DIR}/${previous_date}" -maxdepth 1 -mindepth 1 ! -name ".*" ! -path "${CURRENT_SUBVOLUME}" | wc -l)
if ((dir_count <= 1)); then
return
fi
# 保留时间最早的一个快照,删除其余所有快照
earliest_snapshot=$(find "${SNAPSHOT_DIR}/${previous_date}" -maxdepth 1 -mindepth 1 ! -name ".*" ! -path "${CURRENT_SUBVOLUME}" | sort | head -n 1)
for snapshot in "${SNAPSHOT_DIR}/${previous_date}"/@*; do
if [[ "${snapshot}" != "${earliest_snapshot}" ]]; then
sudo btrfs subvolume delete "${snapshot}"
fi
done
fi
;;
*)
### 清理一周前的所有快照
local one_week_ago
one_week_ago=$(TZ=${TIMEZONE} date -d "1 week ago" +'%Y-%m-%d')
for dir in "${SNAPSHOT_DIR}"/*; do
if [[ -d "${dir}" ]]; then
dir_date=$(basename "${dir}")
if [[ "${dir_date}" < "${one_week_ago}" ]]; then
find "${dir}" -maxdepth 1 -mindepth 1 ! -name ".*" ! -path "${CURRENT_SUBVOLUME}" -exec sudo btrfs subvolume delete {} \;
fi
fi
done
;;
esac
}
check_snapshot_exists() {
# 检查指定快照是否存在
if [[ ! -d "${SNAPSHOT_DIR}/${1}" ]]; then
log_error "=> Snapshot ${SNAPSHOT_DIR}/$1 does not exist."
return 1
fi
return 0
}
mount_snapshot() {
# 如果不带参数,直接退出,保持 @.snapshots 子卷挂载
if [[ -z "${1}" ]]; then
exit 0
fi
# 挂载指定快照
if ! check_snapshot_exists "${1}"; then
return 1
fi
# 更新 systemd-boot 配置
local boot_file_backup="${BOOT_FILE}.$(TZ=${TIMEZONE} date +'%Y%m%d%H%M%S').bak"
sudo cp -f "${BOOT_FILE}" "${boot_file_backup}"
sudo awk -i inplace -v snap="subvol=${SUBVOLUME_NAME}/${SNAPSHOT_DIR##*/}/$1" '{gsub(/subvol=[^ ]*/, snap); print}' ${BOOT_FILE}
sudo cat "${BOOT_FILE}"
log_success "=> Systemd-boot config updated. Check the above config before reboot."
}
restore_snapshot() {
if [[ -z "${1}" ]]; then
log_error "=> Please specify a snapshot to restore."
return 1
fi
# 恢复到指定快照
if ! check_snapshot_exists "${1}"; then
return 1
fi
# 备份当前
log_success "=> Backup current subvolume..."
create_snapshot /
# 备份要恢复的快照
local snapshot_path="${SNAPSHOT_DIR}/${1}"
local backup_path="${SNAPSHOT_DIR}/${1}.bak"
if [[ ! -f "${backup_path}" ]]; then
log_success "=> Backup snapshot ${1}..."
create_snapshot "${snapshot_path}" "${backup_path}"
else
log_warning "=> Snapshot ${1}.bak already exists, skip backup."
fi
# 恢复快照
log_success "=> Mount snapshot ${1}..."
mount_snapshot "${1}"
}
auto_snap() {
# 如果今天没有快照,则创建一个
if [[ ! -d "${SNAPSHOT_DIR}/${CURRENT_DATE}" ]]; then
create_snapshot /
return
fi
# 检查今天最近的快照时间
local latest_snapshot=$(find "${SNAPSHOT_DIR}/${CURRENT_DATE}" -maxdepth 1 -mindepth 1 ! -name ".*" | sort | tail -n 1)
if [[ -n "${latest_snapshot}" ]]; then
# 计算时间差
latest_time=$(basename "${latest_snapshot}" | cut -d'@' -f2)
latest_time=$(TZ=${TIMEZONE} date -d "${latest_time//-/:}" +'%s')
current_time=$(TZ=${TIMEZONE} date -d "${CURRENT_TIME//-/:}" +'%s')
time_diff=$((current_time - latest_time))
# 如果最近的快照是在1小时内创建的,则跳过
if [[ ${time_diff} -lt 3600 ]]; then
log_success "=> Last snapshot was taken less than 1 hour ago, skipping."
return
fi
fi
create_snapshot /
}
main() {
init
case "${1:-}" in
"list")
list_snapshots "${2:-}"
;;
"clean")
clean_snapshots "${2:-}"
;;
"mount")
mount_snapshot "${2:-}"
;;
"restore")
restore_snapshot "${2:-}"
;;
"now")
create_snapshot /
;;
*)
auto_snap
;;
esac
quit $?
}
main "$@"