#!/bin/bash
#======================================================================
# Function: Backup and restore config files in the /etc directory
# Copyright (C) 2020-- https://github.com/unifreq/openwrt_packit
# Copyright (C) 2021-- https://github.com/ophub/luci-app-amlogic
#======================================================================

VERSION="v1.3"
ZSTD_LEVEL=6
SNAPSHOT_PRESTR=".snapshots/"
BACKUP_DIR="/.reserved"
BACKUP_NAME="openwrt_config.tar.gz"
BACKUP_FILE="${BACKUP_DIR}/${BACKUP_NAME}"
# Customize backup list
backup_list_conf="/etc/amlogic_backup_list.conf"
if [[ -s "${backup_list_conf}" ]]; then
    while IFS= read -r line; do
        BACKUP_LIST+="${line} "
    done <"${backup_list_conf}"
else
    BACKUP_LIST='./etc/AdGuardHome.yaml \
./etc/amlogic_backup_list.conf \
./etc/adblocklist/ \
./etc/amule/ \
./etc/balance_irq \
./etc/bluetooth/ \
./etc/china_ssr.txt \
./etc/cifs/cifsdpwd.db \
./etc/smbd/smbdpwd.db \
./etc/ksmbd/ksmbdpwd.db \
./etc/config/ \
./etc/crontabs/ \
./etc/dae/ \
./etc/daed/ \
./usr/share/openclash/core/ \
./etc/openclash/backup/ \
./etc/openclash/config/ \
./etc/openclash/custom/ \
./etc/openclash/game_rules/ \
./etc/openclash/rule_provider/ \
./etc/openclash/proxy_provider/ \
./etc/dnsforwarder/ \
./etc/dnsmasq.conf \
./etc/dnsmasq.d/ \
./etc/dnsmasq.oversea/ \
./etc/dnsmasq.ssr/ \
./etc/docker/daemon.json \
./etc/docker/key.json \
./etc/dropbear/ \
./etc/easy-rsa/ \
./etc/environment \
./etc/exports \
./etc/firewall.user \
./etc/gfwlist/ \
./etc/haproxy.cfg \
./etc/hosts \
./etc/ipsec.conf \
./etc/ipsec.d/ \
./etc/ipsec.secrets \
./etc/ipsec.user \
./etc/ipset/ \
./etc/mosdns/config.yaml \
./etc/mwan3.user \
./etc/nginx/nginx.conf \
./etc/ocserv/ \
./etc/openvpn/ \
./etc/pptpd.conf \
./etc/qBittorrent/ \
./etc/rc.local \
./etc/samba/smbpasswd \
./etc/shadow \
./etc/smartdns/ \
./etc/sqm/ \
./etc/ssh/*key*  \
./etc/ssl/private/  \
./etc/ssrplus/ \
./etc/sysupgrade.conf \
./etc/tailscale/ \
./etc/transmission/ \
./etc/uhttpd.crt \
./etc/uhttpd.key \
./etc/urandom.seed \
./etc/v2raya/ \
./etc/verysync/ \
./root/.ssh/'
fi

error_msg() {
    echo -e " [ERROR] ${1}"
    exit 1
}

if dmesg | grep 'meson' >/dev/null 2>&1; then
    PLATFORM="amlogic"
elif dmesg | grep 'rockchip' >/dev/null 2>&1; then
    PLATFORM="rockchip"
elif dmesg | grep 'sun50i-h6' >/dev/null 2>&1; then
    PLATFORM="allwinner"
else
    source /etc/flippy-openwrt-release
    case ${PLATFORM} in
    amlogic | rockchip | allwinner | qemu-aarch64) : ;;
    *) error_msg "Unknown platform, only support amlogic or rockchip or allwinner h6 or qemu-aarch64!" ;;
    esac
fi

get_root_partition_name() {
    local paths=("/" "/overlay" "/rom")
    local partition_name

    for path in "${paths[@]}"; do
        partition_name=$(df "${path}" | awk 'NR==2 {print $1}' | awk -F '/' '{print $3}')
        [[ -n "${partition_name}" ]] && break
    done

    [[ -z "${partition_name}" ]] && error_msg "Cannot find the root partition!"
    echo "${partition_name}"
}

# Get the partition message of the root file system
get_root_partition_msg() {
    local paths=("/" "/overlay" "/rom")
    local partition_name

    for path in "${paths[@]}"; do
        partition_msg=$(lsblk -l -o NAME,PATH,MOUNTPOINT,UUID,FSTYPE,LABEL | awk '$3 ~ "^" "'"${path}"'" "$" {print $0}')
        [[ -n "${partition_msg}" ]] && break
    done

    [[ -z "${partition_msg}" ]] && error_msg "Cannot find the root partition message!"
    echo "${partition_msg}"
}

backup() {
    cd /
    echo -n "Backup config files ... "
    [ -d "${BACKUP_DIR}" ] || mkdir -p "${BACKUP_DIR}"
    eval tar czf "${BACKUP_FILE}" "${BACKUP_LIST}" 2>/dev/null
    sync
    if [ -f "${BACKUP_FILE}" ]; then
        echo "Has been backed up to [ ${BACKUP_FILE} ], please download and save."
        exit 0
    else
        error_msg "Backup failed!"
    fi
}

restore() {
    # Find the partition where root is located
    ROOT_PTNAME="$(get_root_partition_name)"

    # Find the disk where the partition is located, only supports mmcblk?p? sd?? hd?? vd?? and other formats
    case ${ROOT_PTNAME} in
    mmcblk?p[1-4])
        EMMC_NAME=$(echo ${ROOT_PTNAME} | awk '{print substr($1, 1, length($1)-2)}')
        PARTITION_NAME="p"
        LB_PRE="EMMC_"
        ;;
    [hsv]d[a-z][1-4])
        EMMC_NAME=$(echo ${ROOT_PTNAME} | awk '{print substr($1, 1, length($1)-1)}')
        PARTITION_NAME=""
        LB_PRE=""
        ;;
    *)
        error_msg "Unable to recognize the disk type of ${ROOT_PTNAME}!"
        ;;
    esac

    [ -d "${BACKUP_DIR}" ] || mkdir -p "${BACKUP_DIR}"
    [ -f "/tmp/upload/${BACKUP_NAME}" ] && mv -f "/tmp/upload/${BACKUP_NAME}" ${BACKUP_FILE}
    [ -f "/mnt/${EMMC_NAME}${PARTITION_NAME}4/${BACKUP_NAME}" ] && mv -f "/mnt/${EMMC_NAME}${PARTITION_NAME}4/${BACKUP_NAME}" ${BACKUP_FILE}
    sync

    if [ -f "${BACKUP_FILE}" ]; then
        echo -n "restore config files ... "
        cd /
        tar xzf "${BACKUP_FILE}" 2>/dev/null && sync

        echo "Successful recovery. Will start automatically, please refresh later!"
        sleep 3
        reboot
        exit 0
    else
        error_msg "The backup file [ ${BACKUP_FILE} ] not found!"
    fi
}

gen_fstab() {
    # Find the partition where root is located
    ROOT_PTNAME="$(get_root_partition_name)"

    # Find the disk where the partition is located, only supports mmcblk?p? sd?? hd?? vd?? and other formats
    case ${ROOT_PTNAME} in
    mmcblk?p[1-4])
        EMMC_NAME=$(echo ${ROOT_PTNAME} | awk '{print substr($1, 1, length($1)-2)}')
        PARTITION_NAME="p"
        ;;
    [hsv]d[a-z][1-4])
        EMMC_NAME=$(echo ${ROOT_PTNAME} | awk '{print substr($1, 1, length($1)-1)}')
        PARTITION_NAME=""
        ;;
    *)
        error_msg "Unable to recognize the disk type of ${ROOT_PTNAME}!"
        ;;
    esac

    ROOT_MSG="$(get_root_partition_msg)"
    ROOT_NAME=$(echo $ROOT_MSG | awk '{print $1}')
    ROOT_DEV=$(echo $ROOT_MSG | awk '{print $2}')
    ROOT_UUID=$(echo $ROOT_MSG | awk '{print $4}')
    ROOT_FSTYPE=$(echo $ROOT_MSG | awk '{print $5}')
    ROOT_LABEL=$(echo $ROOT_MSG | awk '{print $6}')

    BOOT_NAME="${EMMC_NAME}${PARTITION_NAME}1"
    BOOT_MSG=$(lsblk -l -o NAME,UUID,FSTYPE,LABEL | grep "${BOOT_NAME}")
    BOOT_DEV="/dev/${BOOT_NAME}"
    BOOT_UUID=$(echo $BOOT_MSG | awk '{print $2}')
    BOOT_FSTYPE=$(echo $BOOT_MSG | awk '{print $3}')
    BOOT_LABEL=$(echo $BOOT_MSG | awk '{print $4}')

    cat >/etc/config/fstab <<EOF
config global
	option anon_swap '0'
	option anon_mount '1'
	option auto_swap '0'
	option auto_mount '1'
	option delay_root '5'
	option check_fs '0'

config mount
	option target '/rom'
	option uuid '${ROOT_UUID}'
	option enabled '1'
	option enabled_fsck '1'
	option fstype '${ROOT_FSTYPE}'
EOF

    if [ "${ROOT_FSTYPE}" == "btrfs" ]; then
        echo "	option options 'compress=zstd:${ZSTD_LEVEL}'" >>/etc/config/fstab
    fi

    cat >>/etc/config/fstab <<EOF

config mount
	option target '/boot'
EOF

    if [ "${BOOT_FSTYPE}" == "vfat" ]; then
        echo "	option label '${BOOT_LABEL}'" >>/etc/config/fstab
    else
        echo "	option uuid '${BOOT_UUID}'" >>/etc/config/fstab
    fi

    cat >>/etc/config/fstab <<EOF
	option enabled '1'
	option enabled_fsck '1'
	option fstype '${BOOT_FSTYPE}'

EOF
    echo "/etc/config/fstab generated."
    echo "please reboot"
    exit 0
}

print_list() {
    echo "${BACKUP_LIST}"
    exit 0
}

list_snapshot() {
    echo "----------------------------------------------------------------"
    btrfs subvolume list -rt /
    echo "----------------------------------------------------------------"
    read -p "Press [ enter ] to return." q
}

create_snapshot() {
    default_snap_name="etc-$(date +"%m.%d.%H%M%S")"
    echo "The default snapshot name is: ${default_snap_name}"
    echo "If you want to modify the snapshot name, please enter it below. Cannot contain spaces."
    echo "If you do not want to modify it, just press [ Enter ]. Or press the [ q ] key to go back directly."
    while :; do
        read -p "[${default_snap_name}] : " nname
        if [ "${nname}" == "" ]; then
            snap_name="${default_snap_name}"
            break
        elif echo "${nname}" | grep -E "\s+" >/dev/null; then
            echo "The name [${nname}] contains spaces, please re-enter!"
            continue
        elif [ "${nname}" == "q" ] || [ "${nname}" == "Q" ]; then
            return
        else
            if btrfs subvolume list -rt / | awk '{print $4}' | grep "^\\${SNAPSHOT_PRESTR}${nname}$" >/dev/null; then
                echo "Name: [ ${nname} ] has been used, please re-enter!"
                continue
            else
                snap_name="${nname}"
                break
            fi
        fi
    done

    (
        cd /
        chattr -ia etc/config/fstab
        btrfs subvolume snapshot -r /etc "${SNAPSHOT_PRESTR}${snap_name}"
        if [[ "$?" -eq "0" ]]; then
            echo "The snapshot is created successfully: ${snap_name}"
        else
            echo "Snapshot creation failed!"
        fi
    )
    read -p "Press [ enter ] to return." q
}

restore_snapshot() {
    echo "Below are the existing etc snapshots, please enter the name of one of them."
    echo "Tip: [ etc-000 ] This is the factory initial configuration."
    echo "     [ etc-001 ] if it exists, it is the initial configuration after upgrading from the previous version."
    echo "----------------------------------------------------------------"
    btrfs subvolume list -rt /
    echo "----------------------------------------------------------------"
    read -p "Please enter the name of the snapshot to be restored (only the part after ${SNAPSHOT_PRESTR} needs to be entered): " snap_name
    if btrfs subvolume list -rt / | grep "${SNAPSHOT_PRESTR}${snap_name}" >/dev/null; then
        while :; do
            echo "Once the snapshot is restored, the current [ /etc ] will be overwritten!"
            read -p "Are you sure you want to restore the snapshot: [$snap_name]? y/n [n] " yn
            case $yn in
            y | Y)
                (
                    cd /
                    chattr -ia etc/config/fstab
                    mv etc etc.backup
                    btrfs subvolume snapshot "${SNAPSHOT_PRESTR}${snap_name}" etc
                    if [[ "$?" -eq "0" ]]; then
                        btrfs subvolume delete -c etc.backup
                        echo "Successfully restored, please enter [ reboot ] to restart the openwrt."
                    else
                        rm -rf etc
                        mv etc.backup etc
                        echo "Recovery failed, [ etc ] has not changed!"
                    fi
                )
                read -p "Press [ enter ] to return." q
                break
                ;;
            *)
                break
                ;;
            esac
        done
    else
        read -p "The snapshot name is incorrect, please run the program again! Press [ Enter ] to go back." q
    fi
}

delete_snapshot() {
    echo "Below are the existing [ etc ] snapshots, please enter the name of one of them."
    echo "Tip: [ etc-000 ] This is the factory initial configuration (cannot be deleted)"
    echo "     [ etc-001 ] if it exists, it is the initial configuration after upgrading from the previous version (cannot be deleted)"
    echo "----------------------------------------------------------------"
    btrfs subvolume list -rt /
    echo "----------------------------------------------------------------"
    read -p "Please enter the name of the snapshot to be deleted (only the part after ${SNAPSHOT_PRESTR} needs to be entered): " snap_name
    if [ "${snap_name}" == "etc-000" ] || [ "${snap_name}" == "etc-001" ]; then
        read -p "The key snapshot cannot be deleted! Press [ enter ] to return." q
    elif [ "${snap_name}" == "" ]; then
        read -p "Name is empty! Press [ enter ] to return." q
    else
        if btrfs subvolume list -rt / | grep "${SNAPSHOT_PRESTR}${snap_name}" >/dev/null; then
            read -p "Are you sure you want to delete ${snap_name}? y/n [n] " yn
            case $yn in
            y | Y)
                (
                    cd /
                    btrfs subvolume delete -c "${SNAPSHOT_PRESTR}${snap_name}"
                    if [[ "$?" -eq "0" ]]; then
                        echo "Snapshot [ ${snap_name} ] has been deleted."
                    else
                        echo "Snapshot [ ${snap_name} ] failed to delete!"
                    fi
                )
                read -p "Press [ Enter ] to return." q
                ;;
            *)
                break
                ;;
            esac
        else
            read -p "The name of the snapshot is incorrect, press [ Enter ] to return." q
        fi
    fi
}

migrate_snapshot() {
    cur_rootdev="$(get_root_partition_name)"

    dev_pre=$(echo "${cur_rootdev}" | awk '{print substr($1, 1, length($1)-1);}')
    rootdev_idx=$(echo "${cur_rootdev}" | awk '{print substr($1, length($1),1);}')
    case $rootdev_idx in
    2)
        old_rootpath="/mnt/${dev_pre}3"
        ;;
    3)
        old_rootpath="/mnt/${dev_pre}2"
        ;;
    *)
        echo "Judge the old version of rootfs path failed!"
        read -p "Press [ enter ] to return." q
        return
        ;;
    esac
    echo "The following are snapshots of etc found from the old version of rootfs, please enter the name of one of them."
    echo "Tip: Automatically exclude etc-000 and etc-001"
    echo "-----------------------------------------------------------------------------------"
    btrfs subvolume list -rt "${old_rootpath}" | grep -v "${SNAPSHOT_PRESTR}etc-000" | grep -v "${SNAPSHOT_PRESTR}etc-001"
    echo "-----------------------------------------------------------------------------------"
    read -p "Please enter the name of the snapshot to be migrated (only the part after $(SNAPSHOT_PRESTR) needs to be entered): " old_snap_name
    if [ "${old_snap_name}" == "" ]; then
        read -p "The name is empty, Press [ enter ] to return." q
        return
    elif ! btrfs subvolume list -rt "${old_rootpath}" | awk '{print $4}' | grep "^${SNAPSHOT_PRESTR}${old_snap_name}$" >/dev/null; then
        echo "The name was entered incorrectly, and the corresponding snapshot was not found!"
        read -p "Press [ enter ] to return." q
        return
    elif [ "${old_snap_name}" == "etc-000" ] || [ "${old_snap_name}" == "etc-001" ]; then
        echo "Critical snapshots are not allowed to migrate!"
        read -p "Press [ enter ] to return." q
        return
    fi

    # Find out if there is a snapshot with the same name under the current rootfs
    if btrfs subvolume list -rt / | awk '{print $4}' | grep "^\\${SNAPSHOT_PRESTR}${old_snap_name}$" >/dev/null; then
        echo "A snapshot with the name [ ${old_snap_name} ] already exists and cannot be migrated! (But you can delete the existing snapshot with the same name and then migrate)"
        read -p "Press [ enter ] to return." q
        return
    fi

    need_size=$(du -h -d0 ${old_rootpath}/${SNAPSHOT_PRESTR}${old_snap_name} | tail -n1 | awk '{print $1}')
    echo "----------------------------------------------------------------------------------------------"
    df -h
    echo "----------------------------------------------------------------------------------------------"
    echo -e "Note: To migrate the snapshot [ ${old_snap_name} ] of [ ${old_rootpath} ] to the current rootfs, it takes about [ ${need_size} ] space,"
    echo -e "      Please confirm whether the partition [/dev/${cur_rootdev}] where [/] is located has enough free space (Available)?"
    read -p "Are you sure to migrate? y/n [n] " yn
    if [ "$yn" == "y" ] || [ "$yn" == "Y" ]; then
        (
            cd /
            btrfs send ${old_rootpath}/${SNAPSHOT_PRESTR}${old_snap_name} | btrfs receive ${SNAPSHOT_PRESTR}
            if [ $? -eq 0 ]; then
                btrfs property set -ts ${SNAPSHOT_PRESTR}${old_snap_name} ro false
                cp ${SNAPSHOT_PRESTR}etc-000/config/fstab ${SNAPSHOT_PRESTR}${old_snap_name}/config/
                cp ${SNAPSHOT_PRESTR}etc-000/fstab ${SNAPSHOT_PRESTR}${old_snap_name}/
                cp ${SNAPSHOT_PRESTR}etc-000/openwrt_release ${SNAPSHOT_PRESTR}${old_snap_name}/
                cp ${SNAPSHOT_PRESTR}etc-000/openwrt_version ${SNAPSHOT_PRESTR}${old_snap_name}/
                cp ${SNAPSHOT_PRESTR}etc-000/flippy-openwrt-release ${SNAPSHOT_PRESTR}${old_snap_name}/
                cp ${SNAPSHOT_PRESTR}etc-000/banner ${SNAPSHOT_PRESTR}${old_snap_name}/banner
                btrfs property set -ts ${SNAPSHOT_PRESTR}${old_snap_name} ro true
                echo "The migration is complete, if you want to apply the snapshot [ ${old_snap_name} ], please use the restore snapshot function."
            else
                echo "The migration failed!"
            fi
            read -p "Press [ enter ] to return." q
            return
        )
    fi
}

snapshot_help() {
    clear
    cat <<EOF
============================================================================================================================
1.  What is a snapshot?
    A snapshot is the state record of a certain subvolume at a certain point in time, and a snapshot is a special subvolume;
    When the local snapshot is first generated, it shares the disk space with the original subvolume,
    so it does not occupy additional space, and only the files that have changed subsequently occupy space;
    Snapshots migrated from other places are not snapshots in essence, but just ordinary subvolumes, so they take up space.
2.  How to display existing snapshots?
    input the command: btrfs subvolume list -rt /
---------------------------------------------------------------------------------------------
EOF
    btrfs subvolume list -rt /
    cat <<EOF
---------------------------------------------------------------------------------------------
2.  How to create a snapshot?
    btrfs subvolume snapshot -r /etc /.snapshots/snapshot_1  # -r It means to generate a read-only snapshot
3.  How to delete a snapshot?
    btrfs subvolume delete -c /.snapshots/snapshot_1  # -c 是 commit 的意思
4.  How to rename a snapshot?
    Just use the [ mv ] command of the operating system, for example:
    mv /.snapshots/snapshot_1 /.snapshots/snapshot_2
5.  How to restore the snapshot?
    mv /etc /etc.backup  # Back up /etc to /etc.backup, that is, rename the subvolume
    btrfs subvolume snapshot /.snapshots/etc-001  /etc   # Use snapshot etc-001 to generate snapshot etc, no -r parameter
    # (Yes, snapshots can also be re-generated snapshots)
    btrfs delete -c /etc.backup # After the previous step is successful, you can delete the etc backup just now
6.  How to restore a certain file from the snapshot?
    Example A: Restore the mount point configuration file from snapshot etc-000: /etc/config/fstab
        cp /.snapshots/etc-000/config/fstab /etc/config/
    Example B: Restore network configuration from snapshot etc-001:
        cp /.snapshots/etc-001/config/network  /etc/config/
    Example C: Restore the ssr configuration file from snapshot etc-001:
        cp /.snapshots/etc-001/config/shadowsocksr /etc/config/
    # (Yes, it is the [ cp ] command of the operating system)
7.  How to migrate snapshots from remote?
    btrfs supports ssh remote migration snapshots, for example:
    ssh 192.168.1.1 btrfs send /.snapshots/snapshot_x  |  btrfs receive /.snapshots
    After the command is completed, the remote snapshot_x is copied to the local /.snapshots/snapshot_x
    (as mentioned above, the migration is a subvolume, which takes up space)
============================================================================================================================
EOF
    exit 0
}

print_help() {
    echo "Usage: $0  -b    [ backup ]"
    echo "       $0  -r    [ restore ]"
    echo "       $0  -g    [ generate fstab ]"
    echo "       $0  -p    [ print backup list ]"
    echo "       $0  -l    [ list snapshots ]"
    echo "       $0  -c    [ create snapshot ]"
    echo "       $0  -s    [ restore snapshot ]"
    echo "       $0  -d    [ delete snapshot ]"
    echo "       $0  -h    [ help ]"
    echo "       $0  -q    [ quit ]"
    exit 0
}

menu() {
    while :; do
        clear
        cat <<EOF

        ┌────────[ backup config ]────────┐
        │                                 │
        │       b. backup config          │
        │       r. restore config         │
        │       g. generate fstab         │
        │       p. print backup list      │
        │                                 │
        ├─────[ Snapshot management ]─────┤
        │                                 │
        │       l. list snapshots         │
        │       c. create snapshot        │
        │       d. delete snapshot        │
        │       R. restore snapshot       │
        │       m. migrate snapshot       │
        │       s. snapshot help          │
        │                                 │
        ╞═════════════════════════════════╡
        │                                 │
        │       h. help                   │
        │       q. quit                   │
        │                                 │
        └─────────────────────────────────┘

EOF
        echo -ne "please select: [ ]\b\b"
        read select
        case $select in
        b | backup) backup ;;
        r | restore)
            restore
            gen_fstab
            ;;
        g | gen_fstab) gen_fstab ;;
        p | print_list) print_list ;;
        l | list_snapshot) list_snapshot ;;
        c | create_snapshot) create_snapshot ;;
        d | delete_snapshot) delete_snapshot ;;
        R | restore_snapshot) restore_snapshot ;;
        m | migrate_snapshot) migrate_snapshot ;;
        s | snapshot_help) snapshot_help ;;
        h | help) print_help ;;
        q | quit) exit 0 ;;
        esac
    done
}

getopts 'brgplcRmsdhq' opts
case $opts in
b | backup) backup ;;
r | restore)
    restore
    gen_fstab
    ;;
g | gen_fstab) gen_fstab ;;
p | print_list) print_list ;;
l | list_snapshot) list_snapshot ;;
c | create_snapshot) create_snapshot ;;
d | delete_snapshot) delete_snapshot ;;
R | restore_snapshot) restore_snapshot ;;
m | migrate_snapshot) migrate_snapshot ;;
s | snapshot_help) snapshot_help ;;
h | help) print_help ;;
q | quit) exit 0 ;;
*) menu ;;
esac
