随机
Enter 搜索 ↑↓ 切换 Esc 清空

docker_image_manager

脚本

Docker 镜像备份、迁移、加载、清理、管理于一体的交互式可视化工具,支持批量操作、远程传输与自动恢复,一键管理镜像全生命周期。

docker_image_manager

Docker 镜像备份、迁移、加载、清理、管理于一体的交互式可视化工具,支持批量操作、远程传输与自动恢复,一键管理镜像全生命周期。

一键脚本

bash <(curl -sL gitee.com/meimolihan/linux-command_sh/raw/master/docker_image_manager.sh)

效果预览

执行脚本效果预览

脚本源码

#!/bin/bash
set -uo pipefail

list_color_init() {
    export gl_hui=$'\033[38;5;59m'
    export gl_hong=$'\033[38;5;9m'
    export gl_lv=$'\033[38;5;10m'
    export gl_huang=$'\033[38;5;11m'
    export gl_lan=$'\033[38;5;32m'
    export gl_bai=$'\033[38;5;15m'
    export gl_zi=$'\033[38;5;13m'
    export gl_bufan=$'\033[38;5;14m'
    export reset=$'\033[0m'
}
list_color_init

log_info()  { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok()    { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn()  { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }

break_end() {
    echo -e "${gl_lv}操作完成${gl_bai}"
    echo -e "${gl_bai}按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
    read -r -n 1 -s
    echo ""
    clear
}

sleep_fractional() {
    local seconds=$1
    if sleep "$seconds" 2>/dev/null; then return 0; fi
    if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
    if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
    if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
    local int_seconds=$(awk -v s="$seconds" 'BEGIN{print int(s+0.999)}')
    sleep "$int_seconds"
}

exit_animation() {
    echo -ne "\r${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
}

cancel_empty() {
    local menu_name="${1:-上一级选单}"
    echo -e "${gl_hong}空输入,返回 ${gl_huang}${menu_name}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
    clear
}

cancel_return() {
    local menu_name="${1:-上一级选单}"
    echo -ne "${gl_lv}即将返回 ${gl_huang}${menu_name}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
    clear
}

cancel_return() {
    local menu_name="${1:-上一级选单}"
    echo -ne "${gl_lv}即将返回 ${gl_huang}${menu_name}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
    clear
}

column_if_available() {
    if command -v column &> /dev/null; then
        column -t -s $'\t'
    else
        cat
    fi
}

list_beautify_docker_system() {
    {
        docker system df | awk -v gray="$gl_hui" -v green="$gl_lv" -v yellow="$gl_huang" \
            -v blue="$gl_lan" -v cyan="$gl_bufan" -v reset="$reset" '
        BEGIN {
            print gray "类型\t总数\t活跃\t大小\t可回收" reset
            print gray "----------\t--------\t--------\t----------\t----------" reset
        }
        NR > 1 {
            type = $1
            total = $2
            active = $3
            size = $4
            reclaim = $5
            if (type == "Local") {
                type = $1 " " $2
                total = $3
                active = $4
                size = $5
                reclaim = $6
            }
            if (type == "Images") color = green
            else if (type == "Containers") color = yellow
            else if (type == "Local Volumes") color = blue
            else if (type == "Build Cache") color = cyan
            else color = reset
            print color type "\t" total "\t" active "\t" size "\t" reclaim reset
        }' | column_if_available
    }
}

handle_invalid_input() {
    echo -ne "\r${gl_hong}无效的输入,请重新输入 ${gl_zi} 2 ${gl_hong}秒后返回 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.3
    echo -ne "\r${gl_huang}无效的输入,请重新输入 ${gl_zi} 1 ${gl_huang}秒后返回 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.3
    echo -e "\r${gl_lv}无效的输入,请重新输入 ${gl_zi} 0 ${gl_lv}秒后返回 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    return 2
}

exit_script() {
    echo ""
    echo -ne "${gl_hong}感谢使用,再见!${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    clear
    exit 0
}

install() {
    [[ $# -eq 0 ]] && {
        log_error "未提供软件包参数!"
        return 1
    }

    local pkg mgr ver cmd_ver pkg_ver installed=false
    for pkg in "$@"; do
        installed=false
        ver=""
        
        if command -v "$pkg" &>/dev/null; then
            cmd_ver=$("$pkg" --version 2>/dev/null | head -n1 | tr -cd '[:print:]' | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
            [[ -n "$cmd_ver" ]] && ver="$cmd_ver"
            installed=true
        fi

        if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
            if command -v 7z &>/dev/null; then
                ver=$(7z 2>&1 | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
                [[ -n "$ver" ]] && installed=true
            fi
        fi

        if [[ "$installed" == false ]]; then
            if command -v opkg &>/dev/null; then
                if opkg list-installed | grep -q "^${pkg} "; then
                    installed=true
                    ver=$(opkg list-installed | grep "^${pkg} " | awk '{print $3}' 2>/dev/null || echo "")
                fi
            elif command -v dpkg-query &>/dev/null; then
                if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
                    installed=true
                    ver=$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || echo "")
                fi
            elif command -v rpm &>/dev/null; then
                if rpm -q "$pkg" &>/dev/null; then
                    installed=true
                    ver=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null || echo "")
                fi
            elif command -v apk &>/dev/null; then
                if apk info "$pkg" 2>/dev/null | grep -q "^installed"; then
                    installed=true
                    ver=$(apk info -a "$pkg" 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
                fi
            elif command -v pacman &>/dev/null; then
                if pacman -Qi "$pkg" &>/dev/null; then
                    installed=true
                    ver=$(pacman -Qi "$pkg" 2>/dev/null | grep -i "version" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
                fi
            fi
        fi
        
        if [[ "$installed" == true ]]; then
            echo -e "${gl_huang}${pkg}${gl_bai} ${gl_lv}已安装${gl_bai}" \
                "$([[ -n "$ver" ]] && echo "版本 ${gl_lv}${ver}${gl_bai}")"
            continue
        fi
        
        echo -e ""
        echo -e "${gl_huang}开始安装:${gl_bai}${pkg}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        
        local install_success=false
        
        for mgr in opkg dnf yum apt apk pacman zypper pkg; do
            if ! command -v "$mgr" &>/dev/null; then
                continue
            fi
            
            case $mgr in
            opkg)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}opkg (OpenWrt/iStoreOS)${gl_bai}"
                if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
                    echo -e "${gl_bai}正在安装: ${gl_lv}p7zip${gl_bai}"
                    opkg update && opkg install p7zip && install_success=true
                else
                    opkg update && opkg install "$pkg" && install_success=true
                fi
                ;;
            dnf)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}dnf (Fedora/RHEL)${gl_bai}"
                dnf -y update && dnf install -y "$pkg" && install_success=true
                ;;
            yum)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}yum (CentOS/RHEL)${gl_bai}"
                yum -y update && yum install -y "$pkg" && install_success=true
                ;;
            apt)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}apt (Debian/Ubuntu)${gl_bai}"
                apt update -y && apt install -y "$pkg" && install_success=true
                ;;
            apk)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}apk (Alpine)${gl_bai}"
                apk update && apk add "$pkg" && install_success=true
                ;;
            pacman)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}pacman (Arch/Manjaro)${gl_bai}"
                pacman -Syu --noconfirm && pacman -S --noconfirm "$pkg" && install_success=true
                ;;
            zypper)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}zypper (openSUSE)${gl_bai}"
                zypper refresh && zypper install -y "$pkg" && install_success=true
                ;;
            pkg)
                echo -e "${gl_bai}使用包管理器: ${gl_zi}pkg (FreeBSD)${gl_bai}"
                pkg update && pkg install -y "$pkg" && install_success=true
                ;;
            esac
            
            [[ "$install_success" == true ]] && break
        done
        
        if [[ "$install_success" == true ]]; then
            echo -e "${gl_lv}${pkg} 安装成功${gl_bai}"
        else
            echo -e "${gl_hong}${pkg} 安装失败${gl_bai}"
        fi
        
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
    done
}

docker_image_backup_tools() {
    clear
    list_image_backups() {
        local BACKUP_ROOT="/mnt/backup_images"
        mkdir -p "$BACKUP_ROOT"
        chmod 755 "$BACKUP_ROOT"
        echo -e "${gl_huang}>>> 当前镜像备份列表${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        shopt -s nullglob
        local list
        list=("${BACKUP_ROOT}"/images_backup_*)
        shopt -u nullglob
        ((${#list[@]})) && ls -1d "${list[@]}" || echo -e "${gl_huang}暂无镜像备份${gl_bai}"

        for backup_dir in "${list[@]}"; do
            if [[ -f "${backup_dir}/manifest.json" ]]; then
                local img_count=$(jq '.images | length' "${backup_dir}/manifest.json" 2>/dev/null || echo "?")
                local backup_size=$(du -sh "${backup_dir}" 2>/dev/null | cut -f1 || echo "未知")
                echo -e "${gl_bai}备份 ${gl_huang}$(basename "${backup_dir}")${gl_bai}: ${gl_zi}${img_count}${gl_bai} 个镜像, ${gl_lv}${backup_size}${gl_bai}"
            fi
        done
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
    }

    backup_docker_images() {
        echo -e ""
        echo -e "${gl_zi}安装依赖中${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        install tar jq gzip sshpass pigz
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if ! command -v docker &>/dev/null || ! docker info &>/dev/null; then
            echo -e "${gl_hong}错误: Docker 未安装或未运行${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            exit_animation
            return 1
        fi

        local BACKUP_ROOT="/mnt/backup_images"
        local DATE_STR=$(date +%Y%m%d_%H%M%S)
        local BACKUP_DIR="${BACKUP_ROOT}/images_backup_${DATE_STR}"
        mkdir -p "$BACKUP_DIR"

        clear
        echo -e "${gl_zi}>>> 备份 ${gl_huang}Docker${gl_zi} 镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        log_info "正在获取 Docker 镜像列表${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        mapfile -t IMAGES < <(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>" | grep -v "REPOSITORY:TAG")

        if [[ ${#IMAGES[@]} -eq 0 ]]; then
            log_warn "未找到任何 Docker 镜像"
            exit_animation
            return
        fi

        echo -e "${gl_zi}找到以下 ${gl_huang}${#IMAGES[@]}${gl_zi} 个镜像:${gl_bai}"
        for i in "${!IMAGES[@]}"; do
            echo -e "  ${gl_huang}$((i + 1)).${gl_bai} ${IMAGES[i]}"
        done

        echo -e ""
        echo -e "${gl_huang}>>> 请选择备份方式:${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bufan}1.  ${gl_bai} 备份所有镜像 (${gl_huang}${#IMAGES[@]}${gl_bai} 个)"
        echo -e "${gl_bufan}2.  ${gl_bai} 手动选择镜像"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_huang}0.  ${gl_bai} 返回主上一级选单"
        echo -e "${gl_hong}00. ${gl_bai} 退出脚本"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        read -r -e -p "请输入你的选择 : " backup_choice

        case "$backup_choice" in
        1)
            images_to_backup=("${IMAGES[@]}")
            ;;
        2)
            images_to_backup=()
            echo -e ""
            echo -e "${gl_huang}>>> 手动选择镜像${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_huang}请输入要备份的镜像编号 (用空格分隔,例如: ${gl_hong}1 ${gl_huang}3 ${gl_lv}5${gl_bai}):"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            read -r -e -p "请输入你的选择: " selected_nums

            for num in $selected_nums; do
                if [[ "$num" =~ ^[0-9]+$ ]] && [[ $num -ge 1 ]] && [[ $num -le ${#IMAGES[@]} ]]; then
                    images_to_backup+=("${IMAGES[$((num - 1))]}")
                fi
            done

            if [[ ${#images_to_backup[@]} -eq 0 ]]; then
                log_warn "未选择任何镜像"
                exit_animation
                return
            fi
            echo -e "${gl_zi}已选择 ${gl_huang}${#images_to_backup[@]}${gl_zi} 个镜像${gl_bai}"

            ;;
        0) cancel_return; return ;;
        00 | 000 | 0000) exit_script ;;
        *) handle_invalid_input ;;
        esac

        local RESTORE_SCRIPT="${BACKUP_DIR}/restore_images.sh"
        cat >"$RESTORE_SCRIPT" <<'EOF'
#!/bin/bash
set -e

gl_bai='\033[0m'
gl_bufan='\033[96m'
gl_lv='\033[32m'
gl_huang='\033[33m'
gl_hong='\033[31m'
gl_zi='\033[35m'

exit_animation() {
    echo -ne "\r${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.5
    echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    sleep_fractional 0.6
    echo ""
}

BACKUP_DIR="$(cd "$(dirname "$0")"; pwd)"
MANIFEST="${BACKUP_DIR}/manifest.json"

echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_zi}>>> 恢复 Docker 镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

if [[ ! -f "$MANIFEST" ]]; then
    echo -e "${gl_hong}错误: 未找到 manifest.json 文件${gl_bai}"
    exit_animation
    exit 1
fi

IMAGE_COUNT=$(jq '.images | length' "$MANIFEST")
echo -e "${gl_bai}发现 ${gl_huang}${IMAGE_COUNT}${gl_bai} 个镜像需要加载${gl_bai}"

read -r -e -p "$(echo -e "${gl_bai}是否要加载所有镜像? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
    echo -e "${gl_huang}已取消${gl_bai}"
    exit_animation
    exit 0
fi

for i in $(seq 0 $((IMAGE_COUNT-1))); do
    IMAGE_NAME=$(jq -r ".images[$i].name" "$MANIFEST")
    IMAGE_FILE=$(jq -r ".images[$i].file" "$MANIFEST")
    IMAGE_PATH="${BACKUP_DIR}/${IMAGE_FILE}"
    
    echo -e "${gl_bai}[$((i+1))/${IMAGE_COUNT}] 加载镜像: ${gl_huang}${IMAGE_NAME}${gl_bai}"
    
    if [[ -f "$IMAGE_PATH" ]]; then
        docker load -i "$IMAGE_PATH"
        echo -e "${gl_lv}✓ 完成${gl_bai}"
    else
        echo -e "${gl_hong}✗ 文件不存在: ${IMAGE_FILE}${gl_bai}"
    fi
done

echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}所有镜像加载完成!${gl_bai}"
echo -e "${gl_bai}使用 ${gl_huang}docker images${gl_bai} 查看已加载的镜像${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
EOF
        chmod +x "$RESTORE_SCRIPT"

        local MANIFEST="${BACKUP_DIR}/manifest.json"
        cat >"$MANIFEST" <<EOF
{
    "backup_date": "$(date -Iseconds)",
    "images": []
}
EOF

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_zi}开始备份 ${gl_huang}${#images_to_backup[@]}${gl_zi} 个镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

        success_count=0
        fail_count=0

        for i in "${!images_to_backup[@]}"; do
            local image="${images_to_backup[i]}"
            local safe_name=$(echo "$image" | sed 's/[\/:]/-/g' | sed 's/^-*//')
            local backup_file="${safe_name}.tar"

            echo -e "${gl_bai}[$((i + 1))/${#images_to_backup[@]}] 备份: ${gl_huang}${image}${gl_bai}"

            if docker save -o "${BACKUP_DIR}/${backup_file}" "$image" 2>/dev/null; then
                if command -v pigz &>/dev/null; then
                    pigz "${BACKUP_DIR}/${backup_file}"
                    backup_file="${backup_file}.gz"
                else
                    gzip "${BACKUP_DIR}/${backup_file}"
                    backup_file="${backup_file}.gz"
                fi

                jq --arg name "$image" --arg file "$backup_file" \
                    '.images += [{"name": $name, "file": $file}]' \
                    "$MANIFEST" >"${MANIFEST}.tmp" && mv "${MANIFEST}.tmp" "$MANIFEST"

                echo -e "${gl_lv}✓ 成功${gl_bai}"
                ((success_count++))
            else
                echo -e "${gl_hong}✗ 失败${gl_bai}"
                ((fail_count++))
            fi
        done

        local total_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1 || echo "未知")

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_lv}备份完成!${gl_bai}"
        echo -e "  ${gl_bai}备份目录: ${gl_huang}${BACKUP_DIR}${gl_bai}"
        echo -e "  ${gl_bai}镜像数量: ${gl_huang}${success_count}/${#images_to_backup[@]}${gl_bai} 成功"
        echo -e "  ${gl_bai}备份大小: ${gl_huang}${total_size}${gl_bai}"
        echo -e "  ${gl_bai}恢复脚本: ${gl_huang}${RESTORE_SCRIPT}${gl_bai}"
        echo -e "  ${gl_bai}使用说明: bash ${RESTORE_SCRIPT}${gl_bai}"

        if [[ $fail_count -gt 0 ]]; then
            echo -e "${gl_hong}警告: ${fail_count} 个镜像备份失败${gl_bai}"
        fi

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        break_end
    }

    migrate_docker_images() {
        echo -e ""
        echo -e "${gl_zi}安装依赖中${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        install tar jq gzip sshpass pigz
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if ! command -v docker &>/dev/null || ! docker info &>/dev/null; then
            echo -e "${gl_hong}错误: Docker 未安装或未运行${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            exit_animation
            return 1
        fi

        clear
        echo -e "${gl_zi}>>> 迁移 ${gl_huang}Docker${gl_zi} 镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        local BACKUP_ROOT="/mnt/backup_images"
        mkdir -pm 755 "$BACKUP_ROOT"

        local latest_backup=""
        shopt -s nullglob
        local backups=("${BACKUP_ROOT}"/images_backup_*)
        shopt -u nullglob

        local latest_backup_name=""
        if ((${#backups[@]} > 0)); then
            latest_backup=$(printf '%s\n' "${backups[@]}" | sort -r | head -n1)
            latest_backup_name=$(basename "$latest_backup")
        fi

        if [[ -n "$latest_backup" ]]; then
            log_info "${gl_bai}最新备份: ${gl_lv}$latest_backup${gl_bai}"
        fi

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        log_info "可用备份列表:"
        if ((${#backups[@]} > 0)); then
            for i in "${!backups[@]}"; do
                local backup_dir="${backups[i]}"
                local img_count="?"
                local backup_size="未知"

                if [[ -f "${backup_dir}/manifest.json" ]]; then
                    img_count=$(jq '.images | length' "${backup_dir}/manifest.json" 2>/dev/null || echo "?")
                fi
                backup_size=$(du -sh "${backup_dir}" 2>/dev/null | cut -f1 || echo "未知")

                echo -e "${gl_huang}$((i + 1)).${gl_bai} $(basename "${backup_dir}") (${gl_huang}${img_count}${gl_bai} 个镜像, ${gl_huang}${backup_size}${gl_bai})"
            done
        else
            log_warn "无备份"
            return 1
        fi
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        local BACKUP_DIR=""
        if [[ -n "$latest_backup_name" ]]; then
            read -r -e -p "$(echo -e "${gl_bai}请输入备份目录路径 (回车使用最新备份: ${gl_lv}$latest_backup_name${gl_bai})(${gl_huang}0${gl_bai}返回): ")" BACKUP_DIR

            [[ -z "$BACKUP_DIR" ]] && { cancel_empty "上一级选单"; continue; }
            [[ "$BACKUP_DIR" == "0" ]] && { cancel_return "上一级选单"; continue; }

            if [[ -z "$BACKUP_DIR" ]]; then
                BACKUP_DIR="$latest_backup"
                log_info "${gl_bai}使用默认最新备份: ${gl_lv}$latest_backup${gl_bai}"
            fi
        else
            read -r -e -p "$(echo -e "${gl_bai}请输入备份目录路径: ")" BACKUP_DIR
        fi

        if [[ "$BACKUP_DIR" =~ ^[0-9]+$ ]] && [[ $BACKUP_DIR -ge 1 ]] && [[ $BACKUP_DIR -le ${#backups[@]} ]]; then
            BACKUP_DIR="${backups[$((BACKUP_DIR - 1))]}"
            log_info "选择备份: $BACKUP_DIR"
        fi

        if [[ -n "$BACKUP_DIR" ]] && [[ ! "$BACKUP_DIR" =~ ^/ ]] && [[ ! "$BACKUP_DIR" =~ ^[0-9]+$ ]]; then
            local full_path="$BACKUP_ROOT/$BACKUP_DIR"
            if [[ -d "$full_path" ]]; then
                BACKUP_DIR="$full_path"
            fi
        fi

        [[ ! -d "$BACKUP_DIR" ]] && {
            log_error "备份目录不存在: $BACKUP_DIR"
            exit_animation
            return 1
        }

        if [[ ! -d "$BACKUP_DIR" ]]; then
            log_error "${gl_bai}备份目录不存在: ${gl_hong}$BACKUP_DIR${gl_bai}"
            exit_animation
            return 1
        fi

        local img_count=$(jq '.images | length' "$BACKUP_DIR/manifest.json" 2>/dev/null || echo "0")
        local backup_date=$(jq -r '.backup_date' "$BACKUP_DIR/manifest.json" 2>/dev/null || echo "未知")
        local backup_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1 || echo "未知")

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bai}备份信息:${gl_bai}"
        echo -e "${gl_bai}  目录: ${gl_huang}$BACKUP_DIR${gl_bai}"
        echo -e "${gl_bai}  时间: ${gl_huang}${backup_date}${gl_bai}"
        echo -e "${gl_bai}  镜像: ${gl_huang}${img_count}${gl_bai}"
        echo -e "${gl_bai}  大小: ${gl_huang}${backup_size}${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        read -r -e -p "$(echo -e "${gl_bai}目标服务器 IP: ")" TARGET_IP
        [[ -z "$TARGET_IP" ]] && {
            log_error "目标服务器 IP 不能为空"
            exit_animation
            return 1
        }

        read -r -e -p "$(echo -e "${gl_bai}目标服务器 SSH 用户名 [默认 ${gl_huang}root${gl_bai}]: ")" TARGET_USER
        TARGET_USER=${TARGET_USER:-root}

        read -r -e -p "$(echo -e "${gl_bai}目标服务器 SSH 端口 [默认 ${gl_huang}22${gl_bai}]: ")" TARGET_PORT
        TARGET_PORT=${TARGET_PORT:-22}

        read -s -p "$(echo -e "${gl_bai}目标服务器 ${TARGET_USER} 密码: ")" SSHPASS
        echo
        export SSHPASS

        log_info "验证目标服务器连接性${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if ! sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
            "${TARGET_USER}@${TARGET_IP}" "whoami" &>/dev/null; then
            log_error "无法连接到目标服务器或认证失败"
            exit_animation
            return 1
        fi

        if ! sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
            "${TARGET_USER}@${TARGET_IP}" "docker info &>/dev/null" &>/dev/null; then
            echo -e "${gl_hong}警告: 目标服务器 Docker 可能未安装或未运行${gl_bai}"
            read -r -e -p "$(echo -e "${gl_bai}是否继续? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" continue_choice
            if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
                log_info "已取消迁移"
                exit_animation
                return 1
            fi
        fi

        local target_backup_dir="/mnt/backup_images"
        local backup_base_name=$(basename "$BACKUP_DIR")

        log_info "开始传输镜像备份到目标服务器${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if ! sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
            "${TARGET_USER}@${TARGET_IP}" "mkdir -p '$target_backup_dir'" 2>/dev/null; then
            log_error "${gl_bai}无法在目标服务器创建目录: ${gl_huang}$target_backup_dir${gl_bai}"
            exit_animation
            return 1
        fi

        local total_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1 || echo "未知")
        echo -e "${gl_bai}传输大小: ${gl_huang}${total_size}${gl_bai}"
        echo -e "${gl_bai}目标路径: ${gl_huang}${TARGET_USER}@${TARGET_IP}:${target_backup_dir}/${backup_base_name}${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if sshpass -e scp -o StrictHostKeyChecking=no -P "$TARGET_PORT" -r \
            "$BACKUP_DIR" "${TARGET_USER}@${TARGET_IP}:${target_backup_dir}/" 2>/dev/null; then
            log_ok "备份传输完成"
        else
            log_info "SCP 传输失败,尝试备用传输方式${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            if sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
                "${TARGET_USER}@${TARGET_IP}" "mkdir -p '${target_backup_dir}/${backup_base_name}'" &&
                tar -czf - -C "$(dirname "$BACKUP_DIR")" "$backup_base_name" |
                sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
                    "${TARGET_USER}@${TARGET_IP}" "tar -xzf - -C '${target_backup_dir}'"; then
                log_ok "备用传输方式完成"
            else
                log_error "所有传输方式均失败"
                exit_animation
                return 1
            fi
        fi

        if sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
            "${TARGET_USER}@${TARGET_IP}" "test -f '${target_backup_dir}/${backup_base_name}/manifest.json'" 2>/dev/null; then

            local remote_img_count=$(sshpass -e ssh -p "$TARGET_PORT" -o StrictHostKeyChecking=no \
                "${TARGET_USER}@${TARGET_IP}" "jq '.images | length' '${target_backup_dir}/${backup_base_name}/manifest.json'" 2>/dev/null || echo "?")

            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            log_ok "迁移完成!"
            echo -e "${gl_bai}镜像备份已传输到目标服务器${gl_bai}"
            echo -e "${gl_bai}目标路径: ${gl_huang}${target_backup_dir}/${backup_base_name}${gl_bai}"
            echo -e "${gl_bai}镜像数量: ${gl_huang}${remote_img_count}${gl_bai} 个"
            echo -e ""
            echo -e "${gl_bai}在目标服务器上执行以下命令加载镜像:${gl_bai}"
            echo -e "${gl_huang}cd '${target_backup_dir}/${backup_base_name}' && ./restore_images.sh${gl_bai}"
            echo -e ""
            echo -e "${gl_bai}或者手动加载:${gl_bai}"
            echo -e "${gl_huang}cd '${target_backup_dir}/${backup_base_name}'${gl_bai}"
            echo -e "${gl_huang}for tar_file in *.tar.gz; do docker load -i \"\$tar_file\"; done${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            break_end
        else
            log_error "传输验证失败,备份可能不完整"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            break_end
            return 1
        fi
    }

    load_docker_images() {
        echo -e ""
        echo -e "${gl_zi}安装依赖中${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        install tar jq gzip pigz
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        if ! command -v docker &>/dev/null || ! docker info &>/dev/null; then
            echo -e "${gl_hong}错误: Docker 未安装或未运行${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"exit_animation
            return 1
        fi

        clear
        echo -e "${gl_zi}>>> 加载 ${gl_huang}Docker${gl_zi} 镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        local BACKUP_ROOT="/mnt/backup_images"
        mkdir -pm 755 "$BACKUP_ROOT"

        local latest_backup=""
        shopt -s nullglob
        local backups=("${BACKUP_ROOT}"/images_backup_*)
        shopt -u nullglob

        local latest_backup_name=""
        if ((${#backups[@]} > 0)); then
            latest_backup=$(printf '%s\n' "${backups[@]}" | sort -r | head -n1)
            latest_backup_name=$(basename "$latest_backup")
        fi

        if [[ -n "$latest_backup" ]]; then
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            log_info "${gl_bai}最新备份: ${gl_lv}$latest_backup${gl_bai}"
        fi

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        log_info "可用备份列表:"
        if ((${#backups[@]} > 0)); then
            for i in "${!backups[@]}"; do
                local backup_dir="${backups[i]}"
                local img_count="?"
                local backup_size="未知"

                if [[ -f "${backup_dir}/manifest.json" ]]; then
                    img_count=$(jq '.images | length' "${backup_dir}/manifest.json" 2>/dev/null || echo "?")
                fi
                backup_size=$(du -sh "${backup_dir}" 2>/dev/null | cut -f1 || echo "未知")

                echo -e "${gl_huang}$((i + 1)).${gl_bai} $(basename "${backup_dir}") (${gl_huang}${img_count}${gl_bai} 个镜像, ${gl_huang}${backup_size}${gl_bai})"
            done
        else
            log_warn "无备份"
            return
        fi
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        local BACKUP_DIR=""
        if [[ -n "$latest_backup_name" ]]; then
            read -r -e -p "$(echo -e "${gl_bai}请输入备份目录路径 (回车使用最新备份: ${gl_lv}$latest_backup_name${gl_bai})(${gl_huang}0${gl_bai}返回): ")" BACKUP_DIR

            [[ -z "$BACKUP_DIR" ]] && { cancel_empty "上一级选单"; continue; }
            [[ "$BACKUP_DIR" == "0" ]] && { cancel_return "上一级选单"; continue; }

            if [[ -z "$BACKUP_DIR" ]]; then
                BACKUP_DIR="$latest_backup"
                log_info "使用默认最新备份: $latest_backup"
            fi
        else
            read -r -e -p "$(echo -e "${gl_bai}请输入备份目录路径: ")" BACKUP_DIR
        fi

        if [[ "$BACKUP_DIR" =~ ^[0-9]+$ ]] && [[ $BACKUP_DIR -ge 1 ]] && [[ $BACKUP_DIR -le ${#backups[@]} ]]; then
            BACKUP_DIR="${backups[$((BACKUP_DIR - 1))]}"
            log_info "${gl_bai}选择备份:${gl_huang} $BACKUP_DIR${gl_bai}"
        fi

        if [[ -n "$BACKUP_DIR" ]] && [[ ! "$BACKUP_DIR" =~ ^/ ]] && [[ ! "$BACKUP_DIR" =~ ^[0-9]+$ ]]; then
            local full_path="$BACKUP_ROOT/$BACKUP_DIR"
            if [[ -d "$full_path" ]]; then
                BACKUP_DIR="$full_path"
            fi
        fi

        [[ ! -d "$BACKUP_DIR" ]] && {
            log_error "${gl_bai}备份目录不存在: ${gl_hong}$BACKUP_DIR${gl_bai}"
            exit_animation
            return
        }

        local RESTORE_SCRIPT="$BACKUP_DIR/restore_images.sh"
        if [[ -f "$RESTORE_SCRIPT" ]]; then
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            log_info "找到自动恢复脚本"
            echo -e "${gl_bai}是否使用自动恢复脚本? ${gl_lv}(推荐)${gl_bai}"
            read -r -e -p "$(echo -e "${gl_bai}使用自动脚本? (${gl_lv}Y${gl_bai}/${gl_hong}n${gl_bai}): ")" use_auto

            if [[ -z "$use_auto" || "$use_auto" =~ ^[Yy]$ ]]; then
                log_info "正在执行自动恢复脚本${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                bash "$RESTORE_SCRIPT"
                exit_animation
                return
            fi
        fi

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_zi}手动加载模式${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

        shopt -s nullglob
        local image_files=("$BACKUP_DIR"/*.tar.gz "$BACKUP_DIR"/*.tar)
        shopt -u nullglob

        if [[ ${#image_files[@]} -eq 0 ]]; then
            log_warn "未找到镜像文件 (.tar 或 .tar.gz)"
            exit_animation
            return
        fi

        echo -e "${gl_bai}找到 ${gl_huang}${#image_files[@]}${gl_bai} 个镜像文件:${gl_bai}"
        for i in "${!image_files[@]}"; do
            local file_size=$(du -h "${image_files[i]}" 2>/dev/null | cut -f1 || echo "未知")
            echo -e "${gl_huang}$((i + 1)).${gl_bai} $(basename "${image_files[i]}") (${gl_huang}${file_size}${gl_bai})"
        done

        echo -e ""
        echo -e "${gl_huang}>>> 请选择加载方式:${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bufan}1.  ${gl_bai} 加载所有镜像"
        echo -e "${gl_bufan}2.  ${gl_bai} 选择特定镜像"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_huang}0.  ${gl_bai} 返回上一级选单"
        echo -e "${gl_hong}00. ${gl_bai} 退出脚本"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        read -r -e -p "请输入你的选择: " load_choice

        case "$load_choice" in
        1)
            files_to_load=("${image_files[@]}")
            ;;
        2)
            files_to_load=()
            echo -e "${gl_bai}请输入要加载的镜像文件编号 (用空格分隔):${gl_bai}"
            read -r -e -p "选择: " selected_nums

            for num in $selected_nums; do
                if [[ "$num" =~ ^[0-9]+$ ]] && [[ $num -ge 1 ]] && [[ $num -le ${#image_files[@]} ]]; then
                    files_to_load+=("${image_files[$((num - 1))]}")
                fi
            done

            if [[ ${#files_to_load[@]} -eq 0 ]]; then
                log_warn "未选择任何镜像文件"
                exit_animation
                return
            fi
            ;;
        0) cancel_return; break ;;
        00 | 000 | 0000) exit_script ;;
        *) handle_invalid_input ;;
        esac

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_zi}开始加载 ${gl_huang}${#files_to_load[@]}${gl_zi} 个镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

        success_count=0
        fail_count=0

        for i in "${!files_to_load[@]}"; do
            local image_file="${files_to_load[i]}"
            local filename=$(basename "$image_file")

            echo -e "${gl_bai}[$((i + 1))/${#files_to_load[@]}] 加载: ${gl_huang}${filename}${gl_bai}"

            if docker load -i "$image_file" 2>/dev/null; then
                echo -e "${gl_lv}✓ 成功${gl_bai}"
                ((success_count++))
            else
                echo -e "${gl_hong}✗ 失败${gl_bai}"
                ((fail_count++))
            fi
        done

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_lv}加载完成!${gl_bai}"
        echo -e "  ${gl_bai}成功: ${gl_huang}${success_count}${gl_bai} 个"
        echo -e "  ${gl_bai}失败: ${gl_huang}${fail_count}${gl_bai} 个"
        echo -e ""
        echo -e "${gl_bai}使用 ${gl_huang}docker images${gl_bai} 查看已加载的镜像${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
        break_end
    }

    manage_docker_images() {
        if ! command -v docker &>/dev/null || ! docker info &>/dev/null; then
            echo -e "${gl_hong}错误: Docker 未安装或未运行${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            return 1
        fi

        while true; do
            clear
            echo -e "${gl_huang}>>> 当前 Docker 镜像列表${gl_bai}"

            mapfile -t ALL_IMAGES < <(docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}" | tail -n +2)

            if [[ ${#ALL_IMAGES[@]} -eq 0 ]]; then
                echo -e "${gl_huang}没有找到 Docker 镜像${gl_bai}"
            else
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

                for i in "${!ALL_IMAGES[@]}"; do
                    IFS=$'\t' read -r repo tag id size <<<"${ALL_IMAGES[i]}"
                    local full_name="${repo}:${tag}"
                    if [[ "$tag" == "<none>" ]]; then
                        full_name="${repo}"
                    fi

                    local num_width=$((${#ALL_IMAGES[@]} > 99 ? 3 : ${#ALL_IMAGES[@]} > 9 ? 2 : 1))
                    printf "${gl_huang}%-${num_width}d${gl_bai}\t%-40s\t%-12s\t%s\n" \
                        $((i + 1)) \
                        "${full_name:0:40}" \
                        "${id:0:12}" \
                        "$size"
                done
            fi

            local total_images=$(docker images -q | wc -l)
            local total_size=$(docker system df --format "{{.TotalSize}}" 2>/dev/null || echo "未知")

            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

            echo -e ""
            echo -e "${gl_zi}>>> 管理 ${gl_huang}Docker${gl_zi} 镜像${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_bufan}1.  ${gl_bai} 删除指定镜像      ${gl_bufan}2.  ${gl_bai} 清理悬空镜像"
            echo -e "${gl_bufan}3.  ${gl_bai} 清理未使用镜像    ${gl_bufan}4.  ${gl_bai} 导出单个镜像"
            echo -e "${gl_bufan}5.  ${gl_bai} 加载单个镜像      ${gl_bufan}6.  ${gl_bai} 管理导出的镜像"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_huang}0.  ${gl_bai} 返回上一级选单    ${gl_hong}00.  ${gl_bai}退出脚本"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            read -r -e -p "请输入你的选择: " manage_choice

            case "$manage_choice" in
            1)
                clear
                echo -e "${gl_huang}>>> 当前 Docker 镜像列表${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

                mapfile -t ALL_IMAGES < <(docker images --format "{{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}" | grep -v "<none>")

                if [[ ${#ALL_IMAGES[@]} -eq 0 ]]; then
                    echo -e "${gl_huang}没有可删除的镜像${gl_bai}"
                    echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                    read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                    echo
                    continue
                fi

                for i in "${!ALL_IMAGES[@]}"; do
                    IFS=$'\t' read -r repo tag id size <<<"${ALL_IMAGES[i]}"
                    full_name="${repo}:${tag}"
                    num_width=$(( ${#ALL_IMAGES[@]} > 9 ? 2 : 1 ))
                    printf "${gl_huang}%-${num_width}d${gl_bai}\t%-40s\t%-12s\t%s\n" $((i+1)) "${full_name:0:40}" "${id:0:12}" "$size"
                done

                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                read -r -e -p "请输入要删除的镜像序号(空格分隔多个):" delete_nums

                for num in $delete_nums; do
                    if [[ ! $num =~ ^[0-9]+$ ]] || [[ $num -lt 1 || $num -gt ${#ALL_IMAGES[@]} ]]; then
                        continue
                    fi
                    IFS=$'\t' read -r repo tag id size <<<"${ALL_IMAGES[$((num-1))]}"
                    echo -e "${gl_bai}准备删除:${gl_huang}${repo}:${tag}${gl_bai}"

                    containers=$(docker ps -a -q --filter ancestor="$id")
                    if [[ -n $containers ]]; then
                        echo -e "${gl_hong}警告:镜像正在被容器使用,无法删除${gl_bai}"
                        continue
                    fi

                    read -r -e -p "确认删除?(y/N):" confirm
                    if [[ "$confirm" =~ ^[Yy]$ ]]; then
                        docker rmi "$id" >/dev/null 2>&1
                        if [[ $? -eq 0 ]]; then
                            echo -e "${gl_lv}✓ 删除成功${gl_bai}"
                        else
                            echo -e "${gl_hong}✗ 删除失败${gl_bai}"
                        fi
                    fi
                done

                read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                echo
                ;;
            2)
                echo -e "${gl_bai}正在查找悬空镜像${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
                local dangling_images
                dangling_images=$(docker images -f "dangling=true" -q)

                if [[ -z "${dangling_images}" ]]; then
                    echo -e "${gl_huang}没有悬空镜像${gl_bai}"
                    read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                    echo ""
                else
                    echo -e "${gl_bai}找到悬空镜像:${gl_bai}"
                    docker images -f "dangling=true" --format "  {{.ID}}  {{.Repository}}:{{.Tag}}"

                    read -r -e -p "$(echo -e "是否清理所有悬空镜像? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
                    if [[ "${confirm}" =~ ^[Yy]$ ]]; then
                        if echo "${dangling_images}" | xargs -r docker rmi -f 2>/dev/null; then
                            echo -e "${gl_lv}✓ 悬空镜像清理完成${gl_bai}"
                        else
                            echo -e "${gl_hong}✗ 清理失败,请检查权限或Docker状态${gl_bai}"
                        fi
                        read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                        echo ""
                    else
                        echo -e "${gl_huang}已取消清理${gl_bai}"
                        read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                        echo ""
                    fi
                fi
                ;;
            3)
                echo -e ""
                echo -e "${gl_bai}当前镜像空间使用情况:${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                list_beautify_docker_system
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                read -r -e -p "$(echo -e "是否清理所有未使用的镜像? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
                if [[ "$confirm" =~ ^[Yy]$ ]]; then
                    docker image prune -a -f
                    echo -e "${gl_lv}✓ 未使用镜像清理完成${gl_bai}"
                fi
                ;;
            4)
                if [[ ${#ALL_IMAGES[@]} -eq 0 ]]; then
                    echo -e "${gl_huang}没有可导出的镜像${gl_bai}"
                    exit_animation    # 即将退出动画
                    continue
                fi

                clear
                echo -e "${gl_huang}>>> 当前 Docker 镜像列表${gl_bai}"

                mapfile -t ALL_IMAGES < <(docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}" | tail -n +2)

                if [[ ${#ALL_IMAGES[@]} -eq 0 ]]; then
                    echo -e "${gl_huang}没有找到 Docker 镜像${gl_bai}"
                else
                    echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

                    for i in "${!ALL_IMAGES[@]}"; do
                        IFS=$'\t' read -r repo tag id size <<<"${ALL_IMAGES[i]}"
                        local full_name="${repo}:${tag}"
                        if [[ "$tag" == "<none>" ]]; then
                            full_name="${repo}"
                        fi

                        local num_width=$((${#ALL_IMAGES[@]} > 99 ? 3 : ${#ALL_IMAGES[@]} > 9 ? 2 : 1))
                        printf "${gl_huang}%-${num_width}d${gl_bai}\t%-40s\t%-12s\t%s\n" \
                            $((i + 1)) \
                            "${full_name:0:40}" \
                            "${id:0:12}" \
                            "$size"
                    done
                fi

                local total_images=$(docker images -q | wc -l)
                local total_size=$(docker system df --format "{{.TotalSize}}" 2>/dev/null || echo "未知")
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

                echo -e ""
                echo -e "${gl_zi}>>> 请输入要导出的镜像编号:${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                read -r -e -p "请输入你的选择: " export_num

                if [[ "$export_num" =~ ^[0-9]+$ ]] && [[ $export_num -ge 1 ]] && [[ $export_num -le ${#ALL_IMAGES[@]} ]]; then
                    local image_info="${ALL_IMAGES[$((export_num - 1))]}"
                    local repo=$(echo "$image_info" | grep -oP '^\S+')
                    local tag=$(echo "$image_info" | grep -oP '\S+\s+\K\S+' | head -1)
                    local export_name="${repo}:${tag}"

                    local safe_name=$(echo "$export_name" | sed 's/[^a-zA-Z0-9._-]/-/g')
                    local export_file="/mnt/backup_images/${safe_name}_$(date +%Y%m%d_%H%M%S).tar"

                    echo -e "${gl_bai}正在导出镜像: ${gl_huang}${export_name}${gl_bai}"
                    echo -e "${gl_bai}保存到: ${gl_huang}${export_file}${gl_bai}"

                    mkdir -p "/mnt/backup_images"
                    if docker save -o "$export_file" "$export_name" 2>/dev/null; then
                        echo -e "${gl_lv}✓ 导出成功${gl_bai}"

                        read -r -e -p "$(echo -e "是否压缩? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" compress
                        if [[ "$compress" =~ ^[Yy]$ ]]; then
                            if gzip "$export_file"; then
                                echo -e "${gl_lv}✓ 压缩完成: ${export_file}.gz${gl_bai}"
                            fi
                        fi
                    else
                        echo -e "${gl_hong}✗ 导出失败${gl_bai}"
                        docker save "$export_name" 2>&1 | grep -i error
                    fi
                fi
                ;;
            5)
                mkdir -p "/mnt/backup_images"

                local backup_files=()
                local i=1

                echo -e "${gl_bai}正在扫描备份目录 /mnt/backup_images${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

                while IFS= read -r -d $'\0' file; do
                    backup_files+=("$file")
                done < <(find "/mnt/backup_images" -type f \( -name "*.tar" -o -name "*.tar.gz" -o -name "*.tgz" \) -print0 2>/dev/null | sort -z)

                if [[ ${#backup_files[@]} -eq 0 ]]; then
                    echo -e "${gl_huang}在 /mnt/backup_images 目录中没有找到备份文件${gl_bai}"
                    echo -e "${gl_bai}支持的格式: .tar, .tar.gz, .tgz${gl_bai}"
                    read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                    echo
                    continue
                fi

                clear
                echo -e "${gl_huang}>>> 可加载的备份文件列表${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                echo -e "${gl_lv}编号\t文件大小\t最后修改\t文件名${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

                for i in "${!backup_files[@]}"; do
                    local file="${backup_files[i]}"
                    local file_name=$(basename "$file")
                    local file_size=$(du -h "$file" | cut -f1)
                    local mod_time=$(stat -c "%y" "$file" | cut -d' ' -f1,2 | cut -d'.' -f1)
                    local file_num=$((i + 1))

                    printf "${gl_huang}%3d${gl_bai}\t%-8s\t%s\t${gl_lv}%s${gl_bai}\n" \
                        "$file_num" \
                        "$file_size" \
                        "$mod_time" \
                        "$file_name"
                done

                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                echo -e ""
                echo -e "${gl_zi}>>> 请输入要加载的备份文件编号:${gl_bai}"
                echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                read -r -e -p "请输入你的选择: " file_num

                if [[ ! "$file_num" =~ ^[0-9]+$ ]] || [[ $file_num -lt 1 ]] || [[ $file_num -gt ${#backup_files[@]} ]]; then
                    echo -e "${gl_hong}✗ 无效的编号${gl_bai}"
                    continue
                fi

                local selected_file="${backup_files[$((file_num - 1))]}"
                local file_size=$(du -h "$selected_file" | cut -f1)
                local file_name=$(basename "$selected_file")

                echo -e ""
                echo -e "${gl_bai}选择的文件: ${gl_huang}${file_name}${gl_bai}"
                echo -e "${gl_bai}文件大小: ${gl_huang}${file_size}${gl_bai}"
                echo -e "${gl_bai}完整路径: ${gl_huang}${selected_file}${gl_bai}"
                echo -e ""

                read -r -e -p "$(echo -e "确认加载此镜像? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm_load
                if [[ ! "$confirm_load" =~ ^[Yy]$ ]]; then
                    echo -e "${gl_huang}已取消加载${gl_bai}"
                    continue
                fi

                echo -e "${gl_bai}正在加载镜像,请稍候${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"

                echo -n "["
                for i in {1..20}; do
                    echo -n " "
                done
                echo -n "]"

                local load_output=$(docker load -i "$selected_file" 2>&1)
                local load_status=$?

                echo -ne "\r\033[K"

                if [[ $load_status -eq 0 ]]; then
                    echo -e "${gl_lv}✓ 镜像加载成功${gl_bai}"

                    local loaded_image=$(echo "$load_output" | grep -oP "Loaded image: \K.*" || echo "")

                    if [[ -n "$loaded_image" ]]; then
                        echo -e "${gl_bai}加载的镜像: ${gl_huang}${loaded_image}${gl_bai}"

                        echo -e ""
                        echo -e "${gl_bai}当前镜像列表中的最新镜像:${gl_bai}"
                        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                        docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}" | head -n 6
                        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
                    else
                        echo -e "${gl_bai}加载的输出: ${gl_huang}${load_output}${gl_bai}"
                    fi

                    echo -e ""
                    read -r -e -p "$(echo -e "是否删除备份文件? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" delete_file
                    if [[ "$delete_file" =~ ^[Yy]$ ]]; then
                        if rm -f "$selected_file"; then
                            echo -e "${gl_lv}✓ 备份文件已删除${gl_bai}"
                        else
                            echo -e "${gl_hong}✗ 文件删除失败${gl_bai}"
                        fi
                    fi
                else
                    echo -e "${gl_hong}✗ 镜像加载失败${gl_bai}"
                    echo -e "${gl_bai}错误信息:${gl_bai}"
                    echo -e "${gl_huang}${load_output}${gl_bai}"
                    echo -e ""
                    echo -e "${gl_huang}可能的原因:${gl_bai}"
                    echo -e "  1. 文件损坏或不完整"
                    echo -e "  2. 磁盘空间不足"
                    echo -e "  3. Docker 服务异常"
                    echo -e "  4. 权限不足"
                    echo -e "  5. 镜像格式不正确"
                fi
                read -n 1 -s -r -p "$(echo -e "按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}")"
                echo
                ;;
            6)
                cd "/mnt/backup_images" && bash <(curl -sL gitee.com/meimolihan/linux-command_sh/raw/master/linux_file_menu.sh)
                ;;
            0) cancel_return; break ;;
            00 | 000 | 0000) exit_script ;;
            *) handle_invalid_input ;;
            esac
            echo
        done
    }

    delete_image_backup() {
        local BACKUP_ROOT="/mnt/backup_images"

        echo -e ""
        echo -e "${gl_zi}>>> 删除镜像备份${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        shopt -s nullglob
        local backups=("${BACKUP_ROOT}"/images_backup_*)
        shopt -u nullglob

        if [[ ${#backups[@]} -eq 0 ]]; then
            echo -e "${gl_huang}没有找到镜像备份${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            exit_animation
            return
        fi

        echo -e "${gl_bai}可用备份列表:${gl_bai}"
        for i in "${!backups[@]}"; do
            local backup_dir="${backups[i]}"
            local img_count="?"
            local backup_size="未知"

            if [[ -f "${backup_dir}/manifest.json" ]]; then
                img_count=$(jq '.images | length' "${backup_dir}/manifest.json" 2>/dev/null || echo "?")
            fi
            backup_size=$(du -sh "${backup_dir}" 2>/dev/null | cut -f1 || echo "未知")

            echo -e "${gl_huang}$((i + 1)).${gl_bai} $(basename "${backup_dir}") (${gl_huang}${img_count}${gl_bai} 个镜像, ${gl_huang}${backup_size}${gl_bai})"
        done
        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"

        read -r -e -p "$(echo -e "${gl_bai}请输入要删除的备份编号 (用空格分隔多个),${gl_hong}回车${gl_bai}删除全部: ")" delete_input

        if [[ -z "$delete_input" ]]; then
            read -r -e -p "$(echo -e "确认删除 ${gl_huang}${#backups[@]}${gl_bai} 个备份? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm

            if [[ "$confirm" =~ ^[Yy]$ ]]; then
                for backup in "${backups[@]}"; do
                    rm -rf "$backup"
                done
                echo -e "${gl_lv}✓ 已删除所有备份${gl_bai}"
            else
                echo -e "${gl_huang}已取消${gl_bai}"
            fi
        else
            for num in $delete_input; do
                if [[ "$num" =~ ^[0-9]+$ ]] && [[ $num -ge 1 ]] && [[ $num -le ${#backups[@]} ]]; then
                    local backup_dir="${backups[$((num - 1))]}"
                    read -r -e -p "$(echo -e "确认删除 ${gl_huang}$(basename "${backup_dir}")${gl_bai}? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm

                    if [[ "$confirm" =~ ^[Yy]$ ]]; then
                        rm -rf "$backup_dir"
                        echo -e "${gl_lv}✓ 已删除: $(basename "${backup_dir}")${gl_bai}"
                    else
                        echo -e "${gl_huang}跳过: $(basename "${backup_dir}")${gl_bai}"
                    fi
                fi
            done
        fi

        echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
    }

    main_menu() {
        while true; do
            clear
            list_image_backups
            echo -e ""
            echo -e "${gl_zi}>>> Docker 镜像备份/迁移/还原工具${gl_bai}"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_bufan}1.  ${gl_bai}备份 Docker 镜像"
            echo -e "${gl_bufan}2.  ${gl_bai}迁移 Docker 镜像"
            echo -e "${gl_bufan}3.  ${gl_bai}加载 Docker 镜像"
            echo -e "${gl_bufan}4.  ${gl_bai}管理 Docker 镜像"
            echo -e "${gl_bufan}5.  ${gl_bai}删除 Docker 镜像备份"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            echo -e "${gl_huang}0.  ${gl_bai}返回上一级选单"
            echo -e "${gl_hong}00. ${gl_bai}退出脚本"
            echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
            read -r -e -p "请输入你的选择: " choice
            case $choice in
            1) backup_docker_images ;;
            2) migrate_docker_images ;;
            3) load_docker_images ;;
            4) manage_docker_images ;;
            5) delete_image_backup ;;
            0) cancel_return; break ;;
            00 | 000 | 0000) exit_script ;;
            *) handle_invalid_input ;;
            esac
        done
    }

    main_menu
}


docker_image_backup_tools

创建本地脚本

new_script="new_test.sh"

cat > "$new_script" << 'EOF'
#!/bin/bash

# 粘贴脚本源码

EOF

# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"