├── config.template
├── install.sh
├── README.md
├── keensnap.sh
└── keensnap-init
/config.template:
--------------------------------------------------------------------------------
1 | LOG_FILE="/opt/var/log/keensnap.log"
2 | PATH_SNAPD="/opt/root/KeenSnap/keensnap-init"
3 | SCHEDULE_NAME=""
4 | SELECTED_DRIVE=""
5 | BOT_TOKEN=""
6 | CHAT_ID=""
7 | BACKUP_STARTUP_CONFIG=false
8 | BACKUP_FIRMWARE=false
9 | BACKUP_ENTWARE=false
10 | BACKUP_WG_PRIVATE_KEY=false
11 | DELETE_ARCHIVE_AFTER_BACKUP=true
12 | SEND_BACKUP_TG=true
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | REPO="keensnap"
4 | SCRIPT="keensnap.sh"
5 | SNAPD="keensnap-init"
6 | CONFIG="config.template"
7 | TMP_DIR="/tmp"
8 | OPT_DIR="/opt"
9 | KEENSNAP_DIR="/opt/root/KeenSnap"
10 |
11 | if ! opkg list-installed | grep -q "^curl"; then
12 | opkg update && opkg install curl
13 | fi
14 |
15 | curl -L -s "https://raw.githubusercontent.com/spatiumstas/$REPO/main/$SCRIPT" --output $TMP_DIR/$SCRIPT
16 | mkdir -p "$KEENSNAP_DIR"
17 | mv "$TMP_DIR/$SCRIPT" "$KEENSNAP_DIR/$SCRIPT"
18 | cd $OPT_DIR/bin && ln -sf $KEENSNAP_DIR/$SCRIPT $OPT_DIR/bin/$REPO
19 | curl -L -s "https://raw.githubusercontent.com/spatiumstas/$REPO/main/$SNAPD" --output $TMP_DIR/$SNAPD
20 | mv "$TMP_DIR/$SNAPD" "$KEENSNAP_DIR/$SNAPD"
21 | chmod -R +x "$KEENSNAP_DIR"
22 | $KEENSNAP_DIR/$SCRIPT
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Бэкап конфигурации KeeneticOS
2 |
3 |
4 | ## Работа сервиса
5 | - Выбор объектов бэкапа состоит из: `Startup-Config`, `Entware`, `Firmware` и `WireGuard Private-Keys`
6 | - Полученный архив с копией устройства можно сохранить/отправить в Telegram и/или смонтированный раздел (внешний накопитель/WebDav).
7 | - При срабатывании расписания запускается хук `/opt/etc/ndm/schedule.d/99-keensnap.sh`
8 | - Просмотр логов: `cat /opt/var/log/keensnap.log` или журнале KeeneticOS. Они также сохраняются в каждом созданном архиве.
9 |
10 | ## Установка:
11 |
12 | 1. В `SSH` ввести команду
13 | ```shell
14 | opkg update && opkg install curl && curl -L -s "https://raw.githubusercontent.com/spatiumstas/keensnap/main/install.sh" > /tmp/install.sh && sh /tmp/install.sh
15 | ```
16 |
17 | 2. В скрипте выбрать настройку
18 |
19 | - Ручной запуска скрипта через `keensnap` или `/opt/root/KeenSnap/keensnap.sh`
20 |
21 | # Настройка
22 | 1. Иметь настроенное расписание, созданное через веб-интерфейс [KeeneticOS](https://support.keenetic.ru/giga/kn-1010/ru/22348-disabling-all-leds-on-schedule.html). Вешать его на что-либо необязательно.
23 | 2. После запуска скрипта выбрать `Настроить конфигурацию`. В предложенном списке выбрать нужное расписание для частоты бэкапа. При первом запуске создастся файл конфигурации, в дальнейшем в нём записываются все настройки. Также скрипт спросит, где сохранять архив с копией устройства.
24 | 3. Перейти в `Параметры бэкапа` и выбрать нужные параметры.
25 | 4. В разделе `Подключить Telegram` можно указать данные, необходимые для отправки архива.
26 |
27 | ## Подключение Telegram
28 |
29 | 1. Получить и скопировать `ID` своего аккаунта или чата через [UserInfoBot](https://t.me/userinfobot)
30 | 2. Создать своего бота через [BotFather](https://t.me/BotFather) и скопировать его `token`
31 |
32 |
33 |
34 | 3. Вставить в скрипт
35 |
36 |
37 |
--------------------------------------------------------------------------------
/keensnap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | trap cleanup INT TERM EXIT
3 | [ -f /opt/root/KeenSnap/config.sh ] && source /opt/root/KeenSnap/config.sh
4 | export LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
5 | RED='\033[1;31m'
6 | GREEN='\033[1;32m'
7 | CYAN='\033[0;36m'
8 | NC='\033[0m'
9 | USERNAME="spatiumstas"
10 | REPO="keensnap"
11 | SCRIPT="keensnap.sh"
12 | TMP_DIR="/tmp"
13 | OPT_DIR="/opt"
14 |
15 | KEENSNAP_DIR="/opt/root/KeenSnap"
16 | SNAPD="keensnap-init"
17 | CONFIG_FILE="/opt/root/KeenSnap/config.sh"
18 | PATH_SCHEDULE="/opt/etc/ndm/schedule.d/99-keensnap.sh"
19 | CONFIG_TEMPLATE="config.template"
20 | SCRIPT_VERSION=$(grep -oP 'SCRIPT_VERSION="\K[^"]+' $KEENSNAP_DIR/$SNAPD)
21 |
22 | print_menu() {
23 | printf "\033c"
24 | printf "${CYAN}"
25 | cat <<'EOF'
26 | _ __ ____
27 | | |/ /___ ___ _ __ / ___| _ __ __ _ _ __
28 | | ' // _ \/ _ \ '_ \\___ \| '_ \ / _` | '_ \
29 | | . \ __/ __/ | | |___) | | | | (_| | |_) |
30 | |_|\_\___|\___|_| |_|____/|_| |_|\__,_| .__/
31 | |_|
32 | EOF
33 | if [ ! -f $KEENSNAP_DIR/$SNAPD ]; then
34 | printf "${RED}Конфигурация не настроена${NC}\n\n"
35 | else
36 | printf "${RED}Версия скрипта: ${NC}%s\n\n" "$SCRIPT_VERSION by ${USERNAME}"
37 | fi
38 | echo "1. Настроить конфигурацию"
39 | echo "2. Параметры бэкапа"
40 | echo "3. Подключить Telegram"
41 | echo "4. Ручной бэкап"
42 | echo ""
43 | echo "77. Удалить скрипт"
44 | echo "99. Обновить скрипт"
45 | echo "00. Выход"
46 | echo ""
47 | }
48 |
49 | main_menu() {
50 | print_menu
51 | read -p "Выберите действие: " choice branch
52 | echo ""
53 | choice=$(echo "$choice" | tr -d '\032' | tr -d '[A-Z]')
54 |
55 | if [ -z "$choice" ]; then
56 | main_menu
57 | else
58 | case "$choice" in
59 | 1) setup_schedule ;;
60 | 2) select_backup_options ;;
61 | 3) setup_telegram ;;
62 | 4) manual_backup ;;
63 | 77) remove_script ;;
64 | 99) script_update "main" ;;
65 | 999) script_update "dev" ;;
66 | 00) exit ;;
67 | *)
68 | echo "Неверный выбор. Попробуйте снова."
69 | sleep 1
70 | main_menu
71 | ;;
72 | esac
73 | fi
74 | }
75 |
76 | print_message() {
77 | message="$1"
78 | color="${2:-$NC}"
79 | border=$(printf '%0.s-' $(seq 1 $((${#message} + 2))))
80 | printf "${color}\n+${border}+\n| ${message} |\n+${border}+\n${NC}\n"
81 | }
82 |
83 | exit_function() {
84 | echo ""
85 | read -n 1 -s -r -p "Для возврата нажмите любую клавишу..."
86 | pkill -P $$ 2>/dev/null
87 | exec "$KEENSNAP_DIR/$SCRIPT"
88 | }
89 |
90 | create_schedule_init() {
91 | cat <<'EOL' >"$PATH_SCHEDULE"
92 | #!/bin/sh
93 | source /opt/root/KeenSnap/config.sh
94 |
95 | if [ "$1" = "start" ] && [ "$schedule" = "$SCHEDULE_NAME" ]; then
96 | $PATH_SNAPD start "$schedule" &
97 | fi
98 | exit 0
99 |
100 | EOL
101 | chmod +x "$PATH_SCHEDULE"
102 | }
103 |
104 | select_schedule() {
105 | message=$1
106 | schedules=""
107 | descs=""
108 | index=1
109 | schedule_output=$(ndmc -c show sc schedule)
110 |
111 | while IFS= read -r line; do
112 | if echo "$line" | grep -q "^\s*name:" && ! echo "$line" | grep -q "config"; then
113 | if [ -n "$current_schedule" ]; then
114 | if [ -n "$current_desc" ]; then
115 | echo "$index. $current_schedule ($current_desc)"
116 | else
117 | echo "$index. $current_schedule"
118 | fi
119 | schedules="$schedules $index:$current_schedule"
120 | descs="$descs $index:$current_desc"
121 | index=$((index + 1))
122 | fi
123 | current_schedule=$(echo "$line" | cut -d ':' -f2- | sed 's/^ *//g')
124 | current_desc=""
125 | fi
126 |
127 | if echo "$line" | grep -q "^\s*description:"; then
128 | current_desc=$(echo "$line" | cut -d ':' -f2- | sed 's/^ *//g')
129 | fi
130 | done </dev/null; then
166 | echo "$key=$defval" >>"$CONFIG_FILE"
167 | fi
168 | }
169 |
170 | setup_config() {
171 | mkdir -p "$KEENSNAP_DIR"
172 | if [ ! -f "$CONFIG_FILE" ]; then
173 | print_message "Создаю конфигурационный файл..." "$CYAN"
174 | cat <<'EOL' >"$CONFIG_FILE"
175 | LOG_FILE="/opt/var/log/keensnap.log"
176 | PATH_SNAPD="/opt/root/KeenSnap/keensnap-init"
177 | SCHEDULE_NAME=""
178 | SELECTED_DRIVE=""
179 | BOT_TOKEN=""
180 | CHAT_ID=""
181 | BACKUP_STARTUP_CONFIG=false
182 | BACKUP_FIRMWARE=false
183 | BACKUP_ENTWARE=false
184 | BACKUP_WG_PRIVATE_KEY=false
185 | DELETE_ARCHIVE_AFTER_BACKUP=true
186 | SEND_BACKUP_TG=true
187 | EOL
188 | else
189 | update_config_value "LOG_FILE" '"/opt/var/log/keensnap.log"'
190 | update_config_value "PATH_SNAPD" '"/opt/root/KeenSnap/keensnap-init"'
191 | update_config_value "SCHEDULE_NAME" '""'
192 | update_config_value "SELECTED_DRIVE" '""'
193 | update_config_value "BOT_TOKEN" '""'
194 | update_config_value "CHAT_ID" '""'
195 | update_config_value "BACKUP_STARTUP_CONFIG" "false"
196 | update_config_value "BACKUP_FIRMWARE" "false"
197 | update_config_value "BACKUP_ENTWARE" "false"
198 | update_config_value "BACKUP_WG_PRIVATE_KEY" "false"
199 | update_config_value "DELETE_ARCHIVE_AFTER_BACKUP" "true"
200 | update_config_value "SEND_BACKUP_TG" "true"
201 | fi
202 | dos2unix "$CONFIG_FILE" >/dev/null 2>&1
203 | create_schedule_init
204 | }
205 |
206 | setup_schedule() {
207 | setup_config
208 | if [ ! -f "$KEENSNAP_DIR/$SNAPD" ]; then
209 | curl -L -s "https://raw.githubusercontent.com/$USERNAME/$REPO/main/$SNAPD" --output "$KEENSNAP_DIR/$SNAPD"
210 | chmod +x "$KEENSNAP_DIR/$SNAPD"
211 | fi
212 |
213 | if ! select_schedule "Выберите номер расписания:"; then
214 | exit_function
215 | fi
216 |
217 | print_message "Вы выбрали: $SCHEDULE_SELECTED" "$CYAN"
218 |
219 | sed -i "s|^SCHEDULE_NAME=.*|SCHEDULE_NAME=\"$SCHEDULE_SELECTED\"|" "$CONFIG_FILE"
220 | identify_external_drive "Выберите накопитель для бэкапа:"
221 | sed -i "s|^SELECTED_DRIVE=.*|SELECTED_DRIVE=\"$selected_drive\"|" "$CONFIG_FILE"
222 | print_message "Вы выбрали: $selected_drive" "$CYAN"
223 |
224 | dos2unix "$CONFIG_FILE"
225 | print_message "Конфигурация сохранена в $CONFIG_FILE" "$GREEN"
226 | exit_function
227 | }
228 |
229 | get_options() {
230 | i=1
231 | for option in $options; do
232 | value=$(grep "^$option=" "$CONFIG_FILE" | cut -d '=' -f2)
233 | echo "$i) $option=${value:-false}"
234 | i=$((i + 1))
235 | done
236 | }
237 | select_backup_options() {
238 | check_config
239 | echo "Текущие параметры:"
240 |
241 | options="BACKUP_STARTUP_CONFIG BACKUP_FIRMWARE BACKUP_ENTWARE BACKUP_WG_PRIVATE_KEY DELETE_ARCHIVE_AFTER_BACKUP SEND_BACKUP_TG"
242 | get_options
243 | echo ""
244 | read -p "Выберите, какие параметры изменить, разделяя их пробелом: " user_choice
245 |
246 | for choice in $user_choice; do
247 | if [ "$choice" -ge 1 ] && [ "$choice" -le $(echo "$options" | wc -w) ]; then
248 | selected_option=$(echo "$options" | cut -d' ' -f"$choice")
249 | current_value=$(grep "^$selected_option=" "$CONFIG_FILE" | cut -d '=' -f2)
250 |
251 | if [ "$current_value" = "true" ]; then
252 | sed -i "s/^$selected_option=.*/$selected_option=false/" "$CONFIG_FILE"
253 | else
254 | sed -i "s/^$selected_option=.*/$selected_option=true/" "$CONFIG_FILE"
255 | fi
256 | else
257 | echo "Неверный выбор: $choice."
258 | exit_function
259 | fi
260 | done
261 |
262 | print_message "Настройки обновлены" "$GREEN"
263 | echo "Новые параметры:"
264 | get_options
265 | exit_function
266 | }
267 |
268 | setup_telegram() {
269 | check_config
270 | read -p "Введите токен бота Telegram: " BOT_TOKEN
271 | BOT_TOKEN=$(echo "$BOT_TOKEN" | sed 's/^[ \t]*//;s/[ \t]*$//')
272 | read -p "Введите ID пользователя/чата Telegram: " CHAT_ID
273 | CHAT_ID=$(echo "$CHAT_ID" | sed 's/^[ \t]*//;s/[ \t]*$//')
274 | sed -i "s|^BOT_TOKEN=.*|BOT_TOKEN=\"$BOT_TOKEN\"|" "$CONFIG_FILE"
275 | sed -i "s|^CHAT_ID=.*|CHAT_ID=\"$CHAT_ID\"|" "$CONFIG_FILE"
276 |
277 | dos2unix "$CONFIG_FILE"
278 | print_message "Конфигурация сохранена в $CONFIG_FILE" "$GREEN"
279 | exit_function
280 | }
281 |
282 | check_config() {
283 | if [ ! -f "$CONFIG_FILE" ]; then
284 | print_message "Не выполнена начальная конфигурация" "$RED"
285 | exit_function
286 | fi
287 | }
288 |
289 | manual_backup() {
290 | $KEENSNAP_DIR/$SNAPD start manual
291 | exit_function
292 | }
293 |
294 | identify_external_drive() {
295 | local message=$1
296 | local message2=$2
297 | local special_message=$3
298 | labels=""
299 | uuids=""
300 | index=1
301 | media_found=0
302 | media_output=$(ndmc -c show media)
303 | current_manufacturer=""
304 |
305 | if [ -z "$media_output" ]; then
306 | print_message "Не удалось получить список накопителей" "$RED"
307 | return 1
308 | fi
309 |
310 | echo "0. Встроенное хранилище (может не хватить места) $message2"
311 |
312 | while IFS= read -r line; do
313 | case "$line" in
314 | *"name: Media"*)
315 | media_found=1
316 | current_manufacturer=""
317 | ;;
318 | *"manufacturer:"*)
319 | if [ "$media_found" = "1" ]; then
320 | current_manufacturer=$(echo "$line" | cut -d ':' -f2- | sed 's/^ *//g')
321 | fi
322 | ;;
323 | *"uuid:"*)
324 | if [ "$media_found" = "1" ]; then
325 | uuid=$(echo "$line" | cut -d ':' -f2- | sed 's/^ *//g')
326 | read -r label_line
327 | read -r fstype_line
328 | read -r state_line
329 | read -r total_line
330 | read -r free_line
331 |
332 | label=$(echo "$label_line" | cut -d ':' -f2- | sed 's/^ *//g')
333 | fstype=$(echo "$fstype_line" | cut -d ':' -f2- | sed 's/^ *//g')
334 | free_bytes=$(echo "$free_line" | cut -d ':' -f2- | sed 's/^ *//g')
335 |
336 | if [ "$fstype" = "swap" ]; then
337 | uuid=""
338 | continue
339 | fi
340 |
341 | free_mb=$((free_bytes / 1024 / 1024))
342 | free_gb=$((free_mb / 1024))
343 |
344 | if [ "$free_mb" -lt 1024 ]; then
345 | free_display="$free_mb"
346 | unit="MB"
347 | else
348 | free_display="$free_gb"
349 | unit="GB"
350 | fi
351 |
352 | if [ -n "$label" ]; then
353 | display_name="$label"
354 | elif [ -n "$current_manufacturer" ]; then
355 | display_name="$current_manufacturer"
356 | else
357 | display_name="Unknown"
358 | fi
359 |
360 | echo "$index. $display_name ($fstype, ${free_display}${unit})"
361 | labels="$labels \"$display_name\""
362 | uuids="$uuids $uuid"
363 | index=$((index + 1))
364 | uuid=""
365 | fi
366 | ;;
367 | esac
368 | done </dev/null | grep -v "^$selected_drive$" | grep -v '/\\.' | grep -vE '/[А-Яа-яЁё]' | sort)
390 | set -- $folders_list
391 | folder_count=$#
392 | if [ $folder_count -eq 0 ]; then
393 | printf "${RED}Директория пустая ${NC}\n"
394 | else
395 | i=1
396 | for folder in "$@"; do
397 | fname="${folder##*/}"
398 | echo "$i. $fname"
399 | i=$((i + 1))
400 | done
401 | fi
402 | read -p "Выберите папку, 0 — выбрать текущую, 00 — уровень назад: " folder_choice
403 | if [ -z "$folder_choice" ] || [ "$folder_choice" = "0" ]; then
404 | break
405 | elif echo "$folder_choice" | grep -Eq '^[0-9]+$' && [ "$folder_choice" -ge 1 ] && [ "$folder_choice" -le "$folder_count" ]; then
406 | eval "selected_drive=\"\${$folder_choice}\""
407 | elif [ "$folder_choice" = "00" ] && [ "$selected_drive" != "/tmp/mnt/$uuid" ]; then
408 | selected_drive=$(dirname "$selected_drive")
409 | else
410 | echo "Неверный выбор. Попробуйте снова."
411 | fi
412 | done
413 | fi
414 | }
415 |
416 | remove_script() {
417 | echo "Удаляю все файлы и выхожу из скрипта..."
418 | rm -rf "$KEENSNAP_DIR" 2>/dev/null
419 | rm -f "$PATH_SCHEDULE" 2>/dev/null
420 | rm -f "$OPT_DIR/bin/$REPO" 2>/dev/null
421 |
422 | print_message "Успешно удалено" "$GREEN"
423 | cleanup
424 | }
425 |
426 | packages_checker() {
427 | if ! opkg list-installed | grep -q "^curl"; then
428 | opkg update && opkg install curl
429 | echo ""
430 | fi
431 | }
432 |
433 | script_update() {
434 | BRANCH="$1"
435 | packages_checker
436 | curl -L -s "https://raw.githubusercontent.com/$USERNAME/$REPO/$BRANCH/$SCRIPT" --output $TMP_DIR/$SCRIPT
437 | curl -L -s "https://raw.githubusercontent.com/$USERNAME/$REPO/$BRANCH/$SNAPD" --output $KEENSNAP_DIR/$SNAPD
438 | chmod +x $KEENSNAP_DIR/$SNAPD
439 |
440 | if [ -f "$TMP_DIR/$SCRIPT" ]; then
441 | mv "$TMP_DIR/$SCRIPT" "$KEENSNAP_DIR/$SCRIPT"
442 | chmod +x $KEENSNAP_DIR/$SCRIPT
443 | if [ ! -f "$OPT_DIR/bin/$REPO" ]; then
444 | cd $OPT_DIR/bin
445 | ln -s "$KEENSNAP_DIR/$SCRIPT" "$OPT_DIR/bin/$REPO"
446 | fi
447 | if [ "$BRANCH" = "dev" ]; then
448 | print_message "Скрипт успешно обновлён на $BRANCH ветку..." "$GREEN"
449 | else
450 | print_message "Скрипт успешно обновлён" "$GREEN"
451 | fi
452 | sleep 1
453 | $KEENSNAP_DIR/$SCRIPT update_config
454 | else
455 | print_message "Ошибка при скачивании скрипта" "$RED"
456 | fi
457 | }
458 |
459 | cleanup() {
460 | pkill -P $$ 2>/dev/null
461 | exit 0
462 | }
463 |
464 | if [ "$1" = "script_update" ]; then
465 | script_update "main"
466 | else
467 | main_menu
468 | fi
469 |
--------------------------------------------------------------------------------
/keensnap-init:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | source /opt/root/KeenSnap/config.sh
4 | export LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
5 | SCRIPT_VERSION="v1.0.9"
6 | PATH_SNAPD="/opt/etc/ndm/schedule.d/99-keensnap.sh"
7 | REMOTE_VERSION=$(curl -s "https://api.github.com/repos/spatiumstas/keensnap/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")')
8 | date="backup$(date +%Y-%m-%d_%H-%M)"
9 | items=""
10 | statuses=""
11 | (
12 | case "$1" in
13 | start)
14 | if [ -n "$2" ]; then
15 | SCHEDULE="$2"
16 | echo "Запуск KeenSnap (расписание: $SCHEDULE)"
17 |
18 | log() {
19 | local message="$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*"
20 | echo "$message"
21 | logger -p notice -t KeenSnap "$*"
22 | }
23 |
24 | error() {
25 | local message="$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*"
26 | echo "$message"
27 | logger -p err -t KeenSnap "$*"
28 | }
29 |
30 | success() {
31 | local message="$(date '+%Y-%m-%d %H:%M:%S') [SUCCESS] $*"
32 | echo "$message"
33 | logger -p notice -t KeenSnap "$*"
34 | }
35 |
36 | get_device_info() {
37 | version_output=$(ndmc -c show version 2>/dev/null)
38 | DEVICE=$(echo "$version_output" | grep "device" | awk -F": " '{print $2}')
39 | FW_VERSION=$(echo "$version_output" | grep "release" | awk -F": " '{print $2}')
40 | DEVICE_ID=$(echo "$version_output" | grep "hw_id" | awk -F": " '{print $2}')
41 |
42 | [ -z "$DEVICE" ] && DEVICE="unknown"
43 | [ -z "$FW_VERSION" ] && FW_VERSION="unknown"
44 | [ -z "$DEVICE_ID" ] && DEVICE_ID="unknown"
45 | }
46 |
47 | get_architecture() {
48 | arch=$(opkg print-architecture | grep -oE 'mips-3|mipsel-3|aarch64-3' | head -n 1)
49 |
50 | case "$arch" in
51 | "mips-3") echo "mips" ;;
52 | "mipsel-3") echo "mipsel" ;;
53 | "aarch64-3") echo "aarch64" ;;
54 | *) echo "unknown_arch" ;;
55 | esac
56 | }
57 |
58 | package_check() {
59 | package="$1"
60 | if ! opkg list-installed | grep -q "^$package"; then
61 | opkg update && opkg install "$package"
62 | fi
63 | }
64 |
65 | clean_log() {
66 | local log_file="$1"
67 | local max_size=524288
68 |
69 | if [ ! -f $log_file ]; then
70 | touch $log_file
71 | fi
72 |
73 | local current_size=$(wc -c <"$log_file")
74 | if [ $current_size -gt $max_size ]; then
75 | sed -i '1,100d' "$log_file"
76 | log "Лог-файл был обрезан на первые 100 строк."
77 | fi
78 | }
79 |
80 | send_to_telegram() {
81 | if [ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ]; then
82 | log "Токен бота или ID чата не заданы. Отправка в Telegram пропущена."
83 | return 1
84 | fi
85 |
86 | local chat_id="${CHAT_ID%%_*}"
87 | local topic_id="${CHAT_ID#*_}"
88 | if [ "$chat_id" = "$CHAT_ID" ]; then
89 | topic_id=""
90 | fi
91 |
92 | local caption="$1"
93 | local file_path="$2"
94 | local escaped_caption
95 | escaped_caption=$(echo "$caption" | sed 's/[][*_`]/\\&/g')
96 |
97 | local response
98 | local response_body
99 |
100 | if [ -z "$file_path" ] || [ ! -f "$file_path" ]; then
101 | local payload
102 | if [ -n "$topic_id" ]; then
103 | payload=$(printf '{"chat_id":%s,"message_thread_id":%s,"parse_mode":"Markdown","text":"%s"}' \
104 | "$chat_id" "$topic_id" "$escaped_caption")
105 | else
106 | payload=$(printf '{"chat_id":%s,"parse_mode":"Markdown","text":"%s"}' \
107 | "$chat_id" "$escaped_caption")
108 | fi
109 | response=$(curl -s -o /tmp/telegram_response -w "%{http_code}" -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
110 | -H "Content-Type: application/json" \
111 | -d "$payload")
112 | response_body=$(cat /tmp/telegram_response)
113 | rm -f /tmp/telegram_response
114 | else
115 | local max_size=$((49 * 1024 * 1024))
116 | local new_archive="${file_path%.tar.gz}.tar"
117 | tar -cf "$new_archive" -C "$(dirname "$file_path")" "$(basename "$file_path")"
118 |
119 | local archive_size
120 | archive_size=$(wc -c <"$new_archive")
121 |
122 | if [ "$archive_size" -le "$max_size" ]; then
123 | log "Отправка архива: $new_archive"
124 | response=$(curl -s -o /tmp/telegram_response -w "%{http_code}" -F "chat_id=$chat_id" \
125 | -F "document=@$new_archive" \
126 | -F "caption=$escaped_caption" \
127 | -F "parse_mode=Markdown" \
128 | ${topic_id:+-F "message_thread_id=$topic_id"} \
129 | "https://api.telegram.org/bot$BOT_TOKEN/sendDocument")
130 | response_body=$(cat /tmp/telegram_response)
131 | rm -f /tmp/telegram_response "$new_archive"
132 | else
133 | package_check "coreutils-split"
134 | log "Архив превышает 49МБ, разбиваю на части..."
135 | split -b "$max_size" -d --numeric-suffixes=001 --suffix-length=3 "$new_archive" "${new_archive}."
136 |
137 | local part_number=1
138 | local total_parts=0
139 |
140 | for part_file in "${new_archive}."*; do
141 | if [[ "$part_file" =~ \.[0-9]{3}$ ]]; then
142 | total_parts=$((total_parts + 1))
143 | fi
144 | done
145 |
146 | for part_file in "${new_archive}."*; do
147 | if [[ ! "$part_file" =~ \.[0-9]{3}$ ]]; then
148 | continue
149 | fi
150 |
151 | local part_caption="[[Часть $part_number из $total_parts]] $escaped_caption"
152 | log "Отправка части: $part_file"
153 | response=$(curl -s -o /tmp/telegram_response -w "%{http_code}" -F "chat_id=$chat_id" \
154 | -F "document=@$part_file" \
155 | -F "caption=$part_caption" \
156 | -F "parse_mode=Markdown" \
157 | ${topic_id:+-F "message_thread_id=$topic_id"} \
158 | "https://api.telegram.org/bot$BOT_TOKEN/sendDocument")
159 | response_body=$(cat /tmp/telegram_response)
160 | rm -f /tmp/telegram_response "$part_file"
161 |
162 | if [ "$response" -ne 200 ]; then
163 | error "Ошибка отправки части $part_file (HTTP $response): $response_body"
164 | return 1
165 | fi
166 | part_number=$((part_number + 1))
167 | done
168 | rm -f "$new_archive"
169 | fi
170 | fi
171 |
172 | if [ "$response" -eq 200 ]; then
173 | success "Сообщение успешно отправлено в Telegram"
174 | return 0
175 | else
176 | error "Ошибка отправки в Telegram (HTTP $response). Ответ сервера: $response_body"
177 | return 1
178 | fi
179 | }
180 |
181 | get_drive_path() {
182 | local selected_drive="$1"
183 | local date="$2"
184 | local device_uuid=""
185 | local subfolder=""
186 | local rel_path=""
187 | local ndmc_path=""
188 |
189 | if [ "$selected_drive" = "/storage" ]; then
190 | rel_path="$date"
191 | ndmc_path="storage:$rel_path"
192 | else
193 | device_uuid=$(echo "$selected_drive" | awk -F'/' '{print $4}')
194 | if [ "${selected_drive#*/tmp/mnt/$device_uuid/}" != "$selected_drive" ]; then
195 | subfolder="${selected_drive#*/tmp/mnt/$device_uuid/}"
196 | subfolder="${subfolder%/}"
197 | fi
198 | if [ -n "$subfolder" ]; then
199 | rel_path="$subfolder/$date"
200 | else
201 | rel_path="$date"
202 | fi
203 | ndmc_path="$device_uuid:$rel_path"
204 | fi
205 | echo "$ndmc_path|$rel_path"
206 | }
207 |
208 | backup_startup_config() {
209 | local success=1
210 | local item_name="startup-config"
211 |
212 | if [ -n "$SELECTED_DRIVE" ]; then
213 | log "Бэкап $item_name..."
214 | local paths_out=$(get_drive_path "$SELECTED_DRIVE" "$date")
215 | local ndmc_path="${paths_out%%|*}"
216 | local rel_path="${paths_out##*|}"
217 | local backup_file="$ndmc_path/${DEVICE_ID}_${FW_VERSION}_$item_name.txt"
218 | ndmc -c "copy $item_name $backup_file"
219 | if [ $? -eq 0 ]; then
220 | success "$item_name сохранён"
221 | success=0
222 | else
223 | error "Ошибка при сохранении $item_name"
224 | fi
225 | fi
226 | items="$items $item_name"
227 | statuses="$statuses $success"
228 | }
229 |
230 | backup_entware() {
231 | local success=1
232 | local item_name="Entware"
233 | package_check "tar"
234 | if [ -n "$SELECTED_DRIVE" ]; then
235 | log "Бэкап $item_name..."
236 | local backup_file="$SELECTED_DRIVE/$date/$(get_architecture)_$item_name.tar.gz"
237 | tar_output=$(tar cvzf "$backup_file" -C /opt --exclude="$backup_file" . 2>&1)
238 | log_operation=$(echo "$tar_output" | tail -n 2)
239 |
240 | if echo "$log_operation" | grep -iq "error\|no space left on device"; then
241 | log "Ошибка при сохранении $item_name:" "$RED"
242 | echo "$log_operation"
243 | else
244 | success "$item_name сохранён"
245 | success=0
246 | fi
247 | fi
248 |
249 | items="$items $item_name"
250 | statuses="$statuses $success"
251 | }
252 |
253 | backup_wg_private_key() {
254 | local success=1
255 | local item_name="WireGuard-Private-Key"
256 | package_check "wireguard-tools"
257 | if [ -n "$SELECTED_DRIVE" ]; then
258 | log "Бэкап $item_name..."
259 | local folder_path="$SELECTED_DRIVE/$date"
260 | local backup_file="$folder_path/$item_name.txt"
261 | wg show all private-key >"$backup_file"
262 | if [ $? -eq 0 ]; then
263 | success "$item_name сохранён"
264 | success=0
265 | else
266 | error "Ошибка при сохранении $item_name"
267 | fi
268 | items="$items $item_name"
269 | statuses="$statuses $success"
270 | fi
271 | }
272 |
273 | backup_firmware() {
274 | local success=1
275 | local item_name="firmware"
276 | if [ -n "$SELECTED_DRIVE" ]; then
277 | log "Бэкап $item_name..."
278 | local paths_out=$(get_drive_path "$SELECTED_DRIVE" "$date")
279 | local ndmc_path="${paths_out%%|*}"
280 | local rel_path="${paths_out##*|}"
281 | local backup_file="$ndmc_path/${DEVICE_ID}_${FW_VERSION}_$item_name.bin"
282 | ndmc -c "copy flash:/$item_name $backup_file"
283 | if [ $? -eq 0 ]; then
284 | success "$item_name сохранена"
285 | success=0
286 | else
287 | error "Ошибка при сохранении $item_name"
288 | fi
289 | fi
290 | items="$items $item_name"
291 | statuses="$statuses $success"
292 | }
293 |
294 | create_backup_and_send_report() {
295 | local items=""
296 | local statuses=""
297 | mkdir -p "$SELECTED_DRIVE/$date"
298 | local backup_performed=0
299 |
300 | if [ "$BACKUP_ENTWARE" = "true" ]; then
301 | backup_entware
302 | backup_performed=1
303 | fi
304 |
305 | if [ "$BACKUP_STARTUP_CONFIG" = "true" ]; then
306 | backup_startup_config
307 | backup_performed=1
308 | fi
309 |
310 | if [ "$BACKUP_FIRMWARE" = "true" ]; then
311 | backup_firmware
312 | backup_performed=1
313 | fi
314 |
315 | if [ "$BACKUP_WG_PRIVATE_KEY" = "true" ]; then
316 | backup_wg_private_key
317 | backup_performed=1
318 | fi
319 |
320 | if [ "$backup_performed" -eq 0 ]; then
321 | log "Ни один из вариантов бэкапа не выбран"
322 | return 1
323 | fi
324 |
325 | local archive_path
326 | if [ -n "$SELECTED_DRIVE" ] && [ -d "$SELECTED_DRIVE/$date" ]; then
327 | cp "$LOG_FILE" "$SELECTED_DRIVE/$date/backup_log.txt"
328 | archive_path="$SELECTED_DRIVE/${DEVICE_ID}_$date.tar.gz"
329 | log "Создание архива..."
330 | tar -czf "$archive_path" -C "$SELECTED_DRIVE" "$date"
331 | if [ $? -ne 0 ]; then
332 | error "Ошибка при создании архива"
333 | send_to_telegram "Ошибка при создании архива" "$SELECTED_DRIVE/$date/backup_log.txt"
334 | rm -rf "$archive_path"
335 | return 1
336 | fi
337 | success "Архив создан"
338 | else
339 | error "Невозможно создать архив: папка с бэкапами не найдена."
340 | return 1
341 | fi
342 |
343 | local report="Бэкап $DEVICE ($DEVICE_ID):"$'\n\n'
344 | local i=1
345 | for item in $items; do
346 | local status_value=$(echo $statuses | cut -d' ' -f$i)
347 | if [ "$status_value" -eq 0 ]; then
348 | report="$report✅ $item"$'\n'
349 | else
350 | report="$report❌ $item"$'\n'
351 | fi
352 | i=$((i + 1))
353 | done
354 |
355 | if [ "$SEND_BACKUP_TG" = "true" ]; then
356 | send_to_telegram "$report" "$archive_path"
357 | fi
358 |
359 | if [ "$DELETE_ARCHIVE_AFTER_BACKUP" = "true" ]; then
360 | rm -rf "$archive_path"
361 | log "Архив удалён"
362 | else
363 | log "Архив сохранён: $archive_path"
364 | fi
365 | }
366 |
367 | delete_temp_folder() {
368 | rm -rf "$SELECTED_DRIVE/$date"
369 | }
370 |
371 | main() {
372 | clean_log "$LOG_FILE"
373 | get_device_info
374 | log "Запуск скрипта для расписания $SCHEDULE"
375 | local drive_uuid=$(basename "$SELECTED_DRIVE")
376 | local device_uuid=$(echo "$SELECTED_DRIVE" | awk -F'/' '{print $4}')
377 | if [ -z "$SELECTED_DRIVE" ] || ([ "$SELECTED_DRIVE" != "/storage" ] && ! ndmc -c show media 2>/dev/null | grep -q "uuid: $device_uuid"); then
378 | drive_name=$(basename "$SELECTED_DRIVE")
379 | error "Выбранный накопитель $drive_name не подключён"
380 | if [ "$SEND_BACKUP_TG" = "true" ]; then
381 | send_to_telegram "❌ Выбранный накопитель $drive_name не подключён" ""
382 | fi
383 | return 1
384 | fi
385 |
386 | create_backup_and_send_report
387 | delete_temp_folder
388 | log "Скрипт завершил работу"
389 | }
390 |
391 | check_update() {
392 | local local_num=$(echo "${SCRIPT_VERSION#v}" | awk -F. '{print $1*1000000 + $2*10000 + $3*100 + ($4 == "" ? 0 : $4)}')
393 | local remote_num=$(echo "${REMOTE_VERSION#v}" | awk -F. '{print $1*1000000 + $2*10000 + $3*100 + ($4 == "" ? 0 : $4)}')
394 | if [ "$remote_num" -gt "$local_num" ]; then
395 | log "Доступна новая версия: $REMOTE_VERSION. Обновляюсь..."
396 | keensnap "script_update"
397 | fi
398 | }
399 |
400 | main "$SCHEDULE"
401 | check_update
402 | fi
403 | ;;
404 | *)
405 | exit 1
406 | ;;
407 | esac
408 | ) 2>&1 | tee -a "$LOG_FILE"
409 |
--------------------------------------------------------------------------------