diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b97f4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +local_backups/ diff --git a/backup-configs.sh b/backup-configs.sh index a7326da..9a2ab84 100755 --- a/backup-configs.sh +++ b/backup-configs.sh @@ -1,33 +1,54 @@ #!/bin/sh -OUT_FILE="$1" -LIST_FILE="$2" -OPKG_SCR="$3" -CONFIGS="$4" +LOCAL_FILE="$1" +LOCAL_LST="$2" +SCR_OPKG="$3" +C_LIST="$4" H_REPLACE="$5" +C_EXCLUDE="$6" -[ -z "$OUT_FILE" ] || [ -z "$LIST_FILE" ] || [ -z "$OPKG_SCR" ] || [ -z "$CONFIGS" ] || [ -z "H_REPLACE" ] && echo "Missing params!" && exit 1 +[ -z "$LOCAL_FILE" ] || [ -z "$LOCAL_LST" ] || [ -z "$C_LIST" ] && echo "Missing params!" && exit 1 TMP_DIR="/tmp/backup_stack" rm -rf "$TMP_DIR" && mkdir -p "$TMP_DIR" -COUNT=0 -echo "Writing package list..." -$OPKG_SCR write "$LIST_FILE" "$H_REPLACE" +$SCR_OPKG write "$LOCAL_LST" "$H_REPLACE" -echo "Collecting files..." -for p in $CONFIGS; do +echo "Included:" +for p in $C_LIST; do + [ -z "$p" ] && continue if [ -e "$p" ]; then mkdir -p "$TMP_DIR$(dirname "$p")" - cp -a "$p" "$TMP_DIR$p" + cp -aP "$p" "$TMP_DIR$p" echo " [+] $p" - COUNT=$((COUNT + 1)) - else - echo " [-] $p (not found)" fi done -echo "Creating archive..." -tar -czf "$OUT_FILE" -C "$TMP_DIR" . +echo "Excluded:" +if [ -n "$C_EXCLUDE" ]; then + for item in $C_EXCLUDE; do + [ -z "$item" ] && continue + TARGET_REMOVE=$(echo "$TMP_DIR/$item" | sed 's|//*|/|g') + if [ -e "$TARGET_REMOVE" ]; then + rm -rf "$TARGET_REMOVE" + echo " [-] $item" + else + echo " [!] Not found in backup: $item" + fi + done +fi + +printf "Creating backup... " +TAR_ERR=$(tar -cz -f "$LOCAL_FILE" -C "$TMP_DIR" . 2>&1) + +if [ $? -eq 0 ] && [ -f "$LOCAL_FILE" ]; then + echo "OK" + echo " [+] Archive size: $(du -sh "$LOCAL_FILE" | awk '{print $1}')" +else + echo "FAILED" + [ -n "$TAR_ERR" ] && echo "[!] Tar error: $TAR_ERR" + rm -rf "$TMP_DIR" + exit 1 +fi + rm -rf "$TMP_DIR" -echo "Total $COUNT config paths saved to $OUT_FILE." diff --git a/backup-manager.sh b/backup-manager.sh index cf953ae..449868b 100755 --- a/backup-manager.sh +++ b/backup-manager.sh @@ -1,10 +1,24 @@ #!/bin/sh +# "smb" or "nfs" +STORAGE_TYPE="nfs" + +# ip or hostname +S_HOST="nfs.lan" + +# /path/to/backup +S_PATH="/mnt/media1/backup" + +SMB_USER="guest" +SMB_PASS="guest" + +LOCAL_KEEP=10 +HOST_KEEP=30 + HNAME=$(uci get system.@system[0].hostname) DATE=$(date +%Y-%m-%d_%H-%M) S_MOUNT="/tmp/share" -S_ADDR="nfs.lan:/mnt/media1/backup" C_LIST=" /etc/config/ @@ -13,178 +27,201 @@ C_LIST=" /root/.ssh/ /opt/xray-config/ /opt/backup-manager/ +/etc/nftables.d/10-netmap.nft +" + +C_EXCLUDE=" +/opt/backup-manager/local_backups +/etc/xray/ipset +/etc/xray/ipnet " H_REPLACE="wpad dnsmasq" DIR="/opt/backup-manager" +L_BACKUP_DIR="$DIR/local_backups" SCR_BACKUP="$DIR/backup-configs.sh" SCR_OPKG="$DIR/backup-opkg.sh" -check_dependencies() { - echo "--- Checking NFS dependencies ---" - local missing="" - local pkgs="kmod-fs-nfs kmod-fs-nfs-v3 kmod-fs-nfs-v4 nfs-utils" - - for pkg in $pkgs; do - if ! opkg list-installed | grep -q "^$pkg "; then - missing="$missing $pkg" - fi - done +mkdir -p "$L_BACKUP_DIR" - if [ -n "$missing" ]; then - echo " [!] Missing packages:$missing" - printf "Would you like to install them now? (y/n): " - read ans - if [ "$ans" = "y" ] || [ "$ans" = "Y" ]; then - echo "Updating package lists..." - opkg update >/dev/null 2>&1 - for m in $missing; do - printf " [+] Installing $m... " - if opkg install "$m" >/dev/null 2>&1; then - echo "OK" - else - echo "FAILED" - return 1 - fi - done - /sbin/depmod >/dev/null 2>&1 - else - echo "Error: Cannot proceed without NFS dependencies." - return 1 - fi +if [ "$STORAGE_TYPE" = "nfs" ]; then + FULL_ADDR="${S_HOST}:${S_PATH}" +else + FULL_ADDR="//${S_HOST}${S_PATH}" +fi + +check_network() { + printf "Checking network %s... " "$S_HOST" + if ! ping -c 2 "$S_HOST" >/dev/null 2>&1; then + echo "FAILED" + echo "[!] Error: Remote host $S_HOST is unreachable." + echo " Check if the NAS is powered on and the IP/Hostname is correct." + return 1 fi - echo " [+] All dependencies are satisfied." + echo "OK" + return 0 +} + +check_dependencies() { + printf "Checking %s dependencies... " "$STORAGE_TYPE" + local pkgs="" + [ "$STORAGE_TYPE" = "nfs" ] && pkgs="kmod-fs-nfs kmod-fs-nfs-v3 kmod-fs-nfs-v4 nfs-utils" + [ "$STORAGE_TYPE" = "smb" ] && pkgs="kmod-fs-cifs cifs-utils" + local missing="" + for pkg in $pkgs; do + opkg list-installed | grep -q "^$pkg " || missing="$missing $pkg" + done + if [ -n "$missing" ]; then + echo " [!] Missing: $missing. Install them to use remote backup." + return 1 + fi + echo "OK" return 0 } mount_share() { mkdir -p "$S_MOUNT" - mount -t nfs "$S_ADDR" "$S_MOUNT" -o nolock,tcp >/dev/null 2>&1 - if ! grep -qs "$S_MOUNT" /proc/mounts; then - echo "Error: Failed to mount NFS share ($S_ADDR)" + if grep -qs "$S_MOUNT" /proc/mounts; then + umount -l "$S_MOUNT" >/dev/null 2>&1 + fi + printf "Mounting %s storage... " "$STORAGE_TYPE" + local mount_err + if [ "$STORAGE_TYPE" = "nfs" ]; then + mount_err=$(mount -t nfs "$FULL_ADDR" "$S_MOUNT" -o nolock,tcp 2>&1) + else + mount_err=$(mount -t cifs "$FULL_ADDR" "$S_MOUNT" -o user=$SMB_USER,pass=$SMB_PASS,vers=3.0,sec=ntlmssp 2>&1) + fi + if grep -qs "$S_MOUNT" /proc/mounts; then + echo "OK" + return 0 + else + echo "FAILED" + echo "[!] Error: Failed to mount $STORAGE_TYPE ($FULL_ADDR)" + [ -n "$mount_err" ] && echo " System message: $mount_err" return 1 fi - return 0 } -check_nfs_write() { - echo "--- Testing NFS Write/Read access ---" - mount_share || return 1 - - local test_file="$S_MOUNT/.nfs_test_$(date +%s)" - - if ! touch "$test_file" >/dev/null 2>&1; then - echo " Error: Cannot write to NFS share (Read-only?)" - umount "$S_MOUNT" - return 1 - fi - - if ! cat "$test_file" >/dev/null 2>&1; then - echo " Error: Cannot read from NFS share" - rm -f "$test_file" - umount "$S_MOUNT" - return 1 - fi - - rm -f "$test_file" - echo " [+] NFS Write/Read test passed." - umount "$S_MOUNT" - return 0 +manage_local_backups() { + printf "Cleaning up local storage (keeping last %s)... " "$LOCAL_KEEP" + local tail_val=$((LOCAL_KEEP + 1)) + ls -t "$L_BACKUP_DIR"/*.tar.gz 2>/dev/null | tail -n +$tail_val | xargs rm -f + ls -t "$L_BACKUP_DIR"/*.lst 2>/dev/null | tail -n +$tail_val | xargs rm -f + echo "OK" } -clean_old_backups() { - mount_share || return 1 - echo "--- Cleaning up backups older than 30 days ---" - - find "$S_MOUNT" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.lst" \) -mtime +30 -exec rm -f {} \; - - echo "Cleanup complete." - umount "$S_MOUNT" +clean_old_remote_backups() { + printf "Cleaning up remote backups older than %s days... " "$HOST_KEEP" + find "$S_MOUNT" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.lst" \) -mtime +$HOST_KEEP -exec rm -f {} \; + echo "OK" } -select_remote_file() { - local ext="$1" - - check_dependencies || exit 1 - check_nfs_write || exit 1 - mount_share || return 1 - - echo "--- Select $ext file from NFS ---" >&2 - FILES=$(find "$S_MOUNT" -maxdepth 1 -name "*$ext" | sort -r) - - if [ -z "$FILES" ]; then - echo "No $ext files found." >&2 - umount "$S_MOUNT" - return 1 - fi - +select_file() { + local search_dir="$1" + local ext="$2" + echo "Select $ext file" >&2 + FILES=$(ls -t "$search_dir"/*"$ext" 2>/dev/null) + [ -z "$FILES" ] && return 1 i=1 for f in $FILES; do - size=$(du -sh "$f" | awk '{print $1}') - echo " [$i] $(basename "$f") ($size)" >&2 + echo " [$i] $(basename "$f") ($(du -sh "$f" | awk '{print $1}'))" >&2 i=$((i+1)) done - printf "Selection (0 to cancel): " >&2 read choice - - if [ -z "$choice" ] || [ "$choice" -eq 0 ] || [ "$choice" -ge "$i" ]; then - echo "Cancelled." >&2 - umount "$S_MOUNT" - return 1 - fi - - SELECTED=$(echo "$FILES" | sed -n "${choice}p") - - RET_PATH="/tmp/restore_target$ext" - cp "$SELECTED" "$RET_PATH" - - umount "$S_MOUNT" - SELECTED_PATH="$RET_PATH" + [ -z "$choice" ] || [ "$choice" -eq 0 ] || [ "$choice" -ge "$i" ] && return 1 + SELECTED_PATH=$(echo "$FILES" | sed -n "${choice}p") } case "$1" in backup) - echo "--- Remote Backup Starting ---" - - check_dependencies || exit 1 - check_nfs_write || exit 1 - mount_share || exit 1 - - REMOTE_BACKUP="$S_MOUNT/backup_${HNAME}_${DATE}.tar.gz" - REMOTE_LIST="$S_MOUNT/packages_${HNAME}_${DATE}.lst" - - $SCR_BACKUP "$REMOTE_BACKUP" "$REMOTE_LIST" "$SCR_OPKG" "$C_LIST" "$H_REPLACE" - - sync - umount "$S_MOUNT" - echo "--- Backup Successfully Saved to NFS ---" - - clean_old_backups + LOCAL_FILE="$L_BACKUP_DIR/backup_${HNAME}_${DATE}.tar.gz" + LOCAL_LST="$L_BACKUP_DIR/packages_${HNAME}_${DATE}.lst" + REMOTE_STATUS="FAILED" + $SCR_BACKUP "$LOCAL_FILE" "$LOCAL_LST" "$SCR_OPKG" "$C_LIST" "$H_REPLACE" "$C_EXCLUDE" + manage_local_backups + if check_network && check_dependencies && mount_share; then + printf "Copying to %s storage... " "$STORAGE_TYPE" + if cp "$LOCAL_FILE" "$LOCAL_LST" "$S_MOUNT/"; then + echo "OK" + REMOTE_STATUS="OK" + clean_old_remote_backups + else + echo "FAILED" + fi + sync + umount "$S_MOUNT" + else + echo " Backup remains only in $L_BACKUP_DIR" + fi + echo "Summary:" + echo " [+] Local: $LOCAL_FILE" + if [ "$REMOTE_STATUS" = "OK" ]; then + echo " [+] Remote: ${S_HOST}:${S_PATH}/$(basename "$LOCAL_FILE")" + else + echo " [!] Remote: Not saved" + fi ;; restore-pkgs) - SELECTED_PATH="" - select_remote_file ".lst" - if [ -n "$SELECTED_PATH" ] && [ -f "$SELECTED_PATH" ]; then - echo "Restoring packages from $SELECTED_PATH..." - $SCR_OPKG restore "$SELECTED_PATH" "$H_REPLACE" - rm "$SELECTED_PATH" - fi + check_network && mount_share || { echo "Remote unavailable"; exit 1; } + select_file "$S_MOUNT" ".lst" + [ -n "$SELECTED_PATH" ] && $SCR_OPKG restore "$SELECTED_PATH" "$H_REPLACE" + umount "$S_MOUNT" ;; - restore-sys) - SELECTED_PATH="" - select_remote_file ".tar.gz" - if [ -n "$SELECTED_PATH" ] && [ -f "$SELECTED_PATH" ]; then - echo "Restoring system from $SELECTED_PATH..." - sysupgrade -r "$SELECTED_PATH" - echo "Restore complete. Reboot is recommended." - fi + restore-sys-net) + check_network && mount_share || { echo "Remote unavailable"; exit 1; } + select_file "$S_MOUNT" ".tar.gz" + [ -n "$SELECTED_PATH" ] && sysupgrade -r "$SELECTED_PATH" + umount "$S_MOUNT" ;; + + restore-sys-local) + select_file "$L_BACKUP_DIR" ".tar.gz" + [ -n "$SELECTED_PATH" ] && sysupgrade -r "$SELECTED_PATH" + ;; + + restore) + echo "1. Local storage ($L_BACKUP_DIR)" + echo "2. Remote storage ($STORAGE_TYPE)" + printf "Select source: " + read s_choice + R_DIR="" + [ "$s_choice" -eq 1 ] && R_DIR="$L_BACKUP_DIR" + [ "$s_choice" -eq 2 ] && { check_network && mount_share && R_DIR="$S_MOUNT"; } + if [ -z "$R_DIR" ]; then + echo " [!] Source unavailable or cancelled." + exit 1 + fi + echo "1. System configs (.tar.gz)" + echo "2. Package list (.lst)" + printf "Select type: " + read t_choice + case "$t_choice" in + 1) + select_file "$R_DIR" ".tar.gz" + if [ -n "$SELECTED_PATH" ]; then + [ "$s_choice" -eq 2 ] && { cp "$SELECTED_PATH" /tmp/restore.tar.gz; SELECTED_PATH="/tmp/restore.tar.gz"; umount "$S_MOUNT"; } + sysupgrade -r "$SELECTED_PATH" + fi + ;; + 2) + select_file "$R_DIR" ".lst" + if [ -n "$SELECTED_PATH" ]; then + $SCR_OPKG restore "$SELECTED_PATH" "$H_REPLACE" + fi + ;; + *) + echo "Cancelled." + ;; + esac + grep -qs "$S_MOUNT" /proc/mounts && umount "$S_MOUNT" + ;; + *) - echo "Usage: $0 {backup|restore-pkgs|restore-sys}" + echo "Usage: $0 {backup|restore}" exit 1 ;; esac diff --git a/backup-opkg.sh b/backup-opkg.sh index 8d70faa..5e8e5b5 100755 --- a/backup-opkg.sh +++ b/backup-opkg.sh @@ -1,21 +1,21 @@ #!/bin/sh CMD="$1" -PKG_FILE="$2" -HOT_REPLACE="$3" +LOCAL_LST="$2" +H_REPLACE="$3" -[ -z "$CMD" ] || [ -z "$PKG_FILE" ] || [ -z "$HOT_REPLACE" ] && echo "Missing params!" && exit 1 +[ -z "$CMD" ] || [ -z "$LOCAL_LST" ] || [ -z "$H_REPLACE" ] && echo "Missing params!" && exit 1 TMP_DIR="/tmp/opkg_restore" fix_dns() { local dns1="1.1.1.1" local dns2="1.0.0.1" - if ! grep -q "$dns1" /etc/resolv.conf; then - echo " [+] Patching DNS for restore..." + printf "Patching DNS for restore... " sed -i "1i nameserver $dns1" /etc/resolv.conf sed -i "2i nameserver $dns2" /etc/resolv.conf + echo "OK" fi } @@ -28,7 +28,6 @@ BEGIN { FLASH_TIME = substr(line, 17) } close(cmd) - while ((getline < status_file) > 0) { if ($1 == "Package:") { pkg = $2 } if ($1 == "Installed-Time:") { @@ -47,42 +46,39 @@ EOF case "$CMD" in write) - mkdir -p "$(dirname "$PKG_FILE")" - echo "Scanning for user-installed packages..." - - > "$PKG_FILE" - + mkdir -p "$(dirname "$LOCAL_LST")" + echo "Package list:" + > "$LOCAL_LST" for pkg in $(list_user_packages); do - echo "$pkg" >> "$PKG_FILE" + echo "$pkg" >> "$LOCAL_LST" echo " [+] $pkg" done - - echo "Total $(wc -l < "$PKG_FILE") packages saved to $PKG_FILE" ;; + restore) - [ ! -f "$PKG_FILE" ] && echo "File not found!" && exit 1 + [ ! -f "$LOCAL_LST" ] && echo "File not found!" && exit 1 [ ! -f /etc/resolv.conf-backup ] && cp /etc/resolv.conf /etc/resolv.conf-backup fix_dns - - echo "Updating package lists..." + printf "Updating package lists... " opkg update >/dev/null 2>&1 - - echo "Phase 1: Removing conflicts ($HOT_REPLACE)..." - if [ -n "$HOT_REPLACE" ]; then - REMOVABLE=$(opkg list-installed | grep -E $(echo "$HOT_REPLACE" | tr ' ' '|') | awk '{print $1}') + echo "OK" + echo "Removing conflicts:" + if [ -n "$H_REPLACE" ]; then + REMOVABLE=$(opkg list-installed | grep -E $(echo "$H_REPLACE" | tr ' ' '|') | awk '{print $1}') for p in $REMOVABLE; do + printf " [-] %s... " "$p" opkg remove "$p" --force-depends >/dev/null 2>&1 + echo "OK" done fi - - echo "Phase 2: Installing packages from list..." - for pkg in $(cat "$PKG_FILE"); do + echo "Installing packages:" + for pkg in $(cat "$LOCAL_LST"); do if opkg list-installed | grep -q "^$pkg "; then - echo " [s] $pkg is already installed" + echo " [s] $pkg... is already installed" else fix_dns - printf " [+] Installing $pkg... " + printf " [+] $pkg... " if opkg install "$pkg" >/dev/null 2>&1; then echo "OK" else @@ -90,8 +86,9 @@ case "$CMD" in fi fi done - [ -f /etc/resolv.conf-backup ] && mv /etc/resolv.conf-backup /etc/resolv.conf - echo "Restore complete. Reboot is recommended." + echo "Summary:" + echo " Restore complete." + echo " Reboot is recommended." ;; esac