├── 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 | --------------------------------------------------------------------------------