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

compose_restore_local

脚本

Docker Compose 项目本地还原工具

compose_restore_local

Docker Compose 项目本地还原工具

一键脚本

bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_restore_local.sh)
bash <(curl -sL ../sh/compose_restore_local.sh)
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/lx_dirfile_backup_new.sh) /vol1/1000/compose /vol2/1000/file/myfile/compose/downloads tar.gz
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_restore_local.sh) /压缩包目录 /解压目标目录
bash <(curl -sL ../sh/compose_restore_local.sh) /压缩包目录 /解压目标目录
功能模式 执行命令 作用说明
交互模式 ./compose_restore_local.sh 不传参,手动输入配置并选择项目解压
传参免交互(2参数) ./compose_restore_local.sh /压缩包目录 /目标目录 直接指定路径,自动扫描并解压
单项目解压 交互界面输入序号 1 解压指定项目
批量解压 交互界面输入 1 3 51-5 多选或范围选择解压多个项目
全部解压 交互界面输入 99 一键解压全部项目

目录结构图

./compose_restore_local.sh /vol2/downloads /vol1/compose

# 压缩包目录(源)
/vol2/downloads
├── project-a.tar.gz
├── project-b.tar.gz
├── project-c.tar.gz
└── project-d.tar.gz

# 解压后(目标)
/vol1/compose
├── project-a/
│   ├── docker-compose.yml
│   └── .env
├── project-b/
│   ├── docker-compose.yml
│   └── config/
├── project-c/
└── project-d/

效果预览

执行脚本效果预览

补充说明

该脚本用于从本地目录批量解压 Docker Compose 项目压缩包并自动还原,支持交互式选择或传参免交互模式,适合快速恢复多个 Compose 项目环境。

功能特点

实例使用方法

# 交互式操作(会提示输入配置)
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_restore_local.sh)

# 直接传参:压缩包目录 + 解压目标目录
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_restore_local.sh) /vol2/1000/file/myfile/compose/downloads /vol1/1000/compose
bash <(curl -sL ../sh/compose_restore_local.sh)

注意事项

创建本地脚本

将上方脚本源码保存为本地文件后执行:

# 1. 将脚本源码保存为 compose_restore_local.sh
# 2. 添加执行权限并运行
chmod +x compose_restore_local.sh

# 交互模式
./compose_restore_local.sh

# 传参模式(2 参数)
./compose_restore_local.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; }

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)"
    elif command -v python3 >/dev/null 2>&1; then
        python3 -c "import time; time.sleep($seconds)"
    elif command -v python >/dev/null 2>&1; then
        python -c "import time; time.sleep($seconds)"
    else
        sleep $(echo "$seconds" | awk '{print int($1+0.999)}')
    fi
}

handle_invalid_input() {
    echo -e "${gl_hong}无效的输入,请重新输入!${gl_bai}"
    sleep_fractional 0.8
}

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 -p ""
    echo ""
    clear
}

exit_script() {
    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
}

ARCHIVE_DIR="${1:-}"
TARGET_ROOT="${2:-}"

ask_if_empty() {
    local var_name="$1"
    local prompt="$2"
    local default="${3:-}"

    if [[ -z "${!var_name}" ]]; then
        if [[ -n "$default" ]]; then
            read -r -e -p "$(echo -e "${gl_bai}${prompt}${gl_huang}[$default]${gl_bai}: ")" input
            eval "$var_name=\"${input:-$default}\""
        else
            read -r -e -p "$(echo -e "${gl_bai}${prompt}: ")" input
            eval "$var_name=\"$input\""
        fi
    fi
}

interactive_setup() {
    clear
    echo -e "${gl_zi}>>> 交互式配置${gl_bai}"
    echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"

    ask_if_empty ARCHIVE_DIR   "请输入压缩包目录"        "/vol2/1000/file/myfile/compose/downloads"
    ask_if_empty TARGET_ROOT   "请输入解压目标根目录"    "/vol1/1000/compose"

    echo ""
    echo -e "${gl_lv}配置完成:${gl_bai}"
    echo -e "压缩包目录 : ${gl_huang}$ARCHIVE_DIR${gl_bai}"
    echo -e "解压根目录 : ${gl_huang}$TARGET_ROOT${gl_bai}"
    echo ""
    sleep 1
}

if [[ $# -lt 2 ]]; then
    interactive_setup
fi

if [[ ! -d "$ARCHIVE_DIR" ]]; then
    log_error "压缩包目录不存在: $ARCHIVE_DIR"
    exit 1
fi

mkdir -p "$TARGET_ROOT"

extract_archive() {
    local archive_path="$1"
    local dest_dir="$2"
    local filename
    filename=$(basename "$archive_path")

    # 验证是有效的 gzip 文件
    if ! gzip -t "$archive_path" 2>/dev/null; then
        log_error "不是有效的 gzip 格式: $filename"
        return 1
    fi

    mkdir -p "$dest_dir"

    log_info "解压: $filename$dest_dir"
    if tar -xzf "$archive_path" -C "$dest_dir" 2>/dev/null; then
        log_ok "解压完成: $filename"
        return 0
    else
        log_error "解压失败: $filename"
        return 1
    fi
}

declare -A projects

scan_archives() {
    projects=()
    local index=1

    local files
    if find "$ARCHIVE_DIR" -maxdepth 1 -name "*.tar.gz" -printf "%f\n" >/dev/null 2>&1; then
        mapfile -t files < <(find "$ARCHIVE_DIR" -maxdepth 1 -name "*.tar.gz" -printf "%f\n" | sort)
    else
        mapfile -t files < <(ls -1 "$ARCHIVE_DIR"/*.tar.gz 2>/dev/null | xargs -n1 basename | sort)
    fi

    if [[ ${#files[@]} -eq 0 ]]; then
        log_error "未找到任何 .tar.gz 文件"
        return 1
    fi

    for f in "${files[@]}"; do
        local name="${f%.tar.gz}"
        projects["$index"]="$name"
        ((index++))
    done
}

parse_selection() {
    local input="$1"
    local total="$2"
    local -n out_arr="$3"
    out_arr=()

    local tokens
    read -ra tokens <<< "$input"

    for token in "${tokens[@]}"; do
        if [[ "$token" =~ ^[0-9]+$ ]]; then
            if (( token >= 1 && token <= total )); then
                out_arr+=("$token")
            else
                return 1
            fi
        elif [[ "$token" =~ ^([0-9]+)-([0-9]+)$ ]]; then
            local start="${BASH_REMATCH[1]}"
            local end="${BASH_REMATCH[2]}"
            if (( start < 1 || end > total || start > end )); then
                return 1
            fi
            for ((i=start; i<=end; i++)); do
                out_arr+=("$i")
            done
        else
            return 1
        fi
    done

    if [[ ${#out_arr[@]} -gt 0 ]]; then
        mapfile -t out_arr < <(printf "%s\n" "${out_arr[@]}" | sort -n | uniq)
        return 0
    fi
    return 1
}

IDX_W=0
NAME_W=0
GAP="    "

calc_layout() {
    local max_idx=0
    local max_name=0

    for i in "${!projects[@]}"; do
        (( i > max_idx )) && max_idx=$i
        local n="${projects[$i]}"
        (( ${#n} > max_name )) && max_name=${#n}
    done

    IDX_W=${#max_idx}
    NAME_W=$max_name
}

print_row() {
    local i1=$1
    local i2="${2:-}"

    local fmt_left
    local fmt_right
    fmt_left=$(printf "%%%dd.  %%-%ds" "$IDX_W" "$NAME_W")
    fmt_right=$(printf "%s%%%dd.  %%-%ss" "$GAP" "$IDX_W" "$NAME_W")

    if [[ -n "$i2" ]]; then
        printf "${gl_bufan}${fmt_left}${gl_bai}%s${gl_bufan}${fmt_right}${gl_bai}\n" \
            "$i1" "${projects[$i1]}" \
            "$GAP" \
            "$i2" "${projects[$i2]}"
    else
        printf "${gl_bufan}${fmt_left}${gl_bai}\n" \
            "$i1" "${projects[$i1]}"
    fi
}

batch_extract() {
    local -a selected_indices=("$@")
    local ok=0 fail=0 failed_names=()

    echo -e "${gl_huang}开始批量解压(共 ${#selected_indices[@]} 个项目)${gl_bai}"
    echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"

    for idx in "${selected_indices[@]}"; do
        local name="${projects[$idx]}"
        local archive="$ARCHIVE_DIR/${name}.tar.gz"

        echo -ne "${gl_huang}[$idx] $name ... ${gl_bai}"
        if extract_archive "$archive" "$TARGET_ROOT" >/dev/null 2>&1; then
            echo -e "${gl_lv}✓ 成功${gl_bai}"
            ((ok++))
        else
            echo -e "${gl_hong}✗ 失败${gl_bai}"
            ((fail++))
            failed_names+=("$name")
        fi
    done

    echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
    echo -e "批量解压完成: ${gl_lv}成功 $ok${gl_bai}, ${gl_hong}失败 $fail${gl_bai}"

    if [[ ${#failed_names[@]} -gt 0 ]]; then
        echo -e "${gl_hong}失败项目: ${failed_names[*]}${gl_bai}"
    fi
}

main_menu() {
    scan_archives || return 1
    local total=${#projects[@]}
    calc_layout

    while true; do
        clear
        echo -e "${gl_zi}>>> Compose 项目本地还原工具${gl_bai}"
        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bai}压缩包目录 : ${gl_huang}$ARCHIVE_DIR${gl_bai}"
        echo -e "${gl_bai}解压目标   : ${gl_huang}$TARGET_ROOT${gl_bai}"
        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"

        local i=1
        while (( i <= total )); do
            j=$((i + 1))
            if (( j <= total )); then
                print_row "$i" "$j"
            else
                print_row "$i" ""
            fi
            i=$((i + 2))
        done

        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bufan}99. ${gl_bai}解压全部项目${gl_bai}"
        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_huang}0.  ${gl_bai}返回/重新配置          ${gl_hong}00. ${gl_bai}退出脚本${gl_bai}"
        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
        echo -e "${gl_bai}提示: 可多选,如 ${gl_huang}1 3 5${gl_bai}${gl_huang}1-5${gl_bai}${gl_huang}1 3-5 8${gl_bai}"
        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"

        read -r -e -p "$(echo -e "${gl_bai}请输入项目序号: ")" choice

        if [[ -z "$choice" ]]; then
            handle_invalid_input
            continue
        fi

        case "$choice" in
        0)
            if [[ $# -lt 2 ]]; then
                interactive_setup
                scan_archives || return 1
                total=${#projects[@]}
                calc_layout
            else
                log_warn "非交互模式无法重新配置"
            fi
            ;;
        00) exit_script ;;
        99)
            clear
            echo -e "${gl_huang}正在解压全部项目 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
            echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
            all_indices=()
            for ((i=1; i<=total; i++)); do
                all_indices+=("$i")
            done
            batch_extract "${all_indices[@]}"
            break_end
            ;;
        *)
            selected_indices=()
            if parse_selection "$choice" "$total" selected_indices; then
                if [[ ${#selected_indices[@]} -eq 1 ]]; then
                    idx="${selected_indices[0]}"
                    name="${projects[$idx]}"
                    archive="$ARCHIVE_DIR/${name}.tar.gz"
                    echo -e ""
                    echo -e "${gl_huang}正在处理: $name${gl_bai}"
                    echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
                    if extract_archive "$archive" "$TARGET_ROOT"; then
                        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
                        log_ok "$name 解压成功"
                    else
                        echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
                        log_error "$name 解压失败"
                    fi
                    echo -e "${gl_bufan}——————————————————————————————————————————————————————————${gl_bai}"
                    break_end
                else
                    clear
                    batch_extract "${selected_indices[@]}"
                    break_end
                fi
            else
                handle_invalid_input
            fi
            ;;
        esac
    done
}

main_menu

脚本函数说明

函数 作用
list_color_init 初始化终端彩色输出变量
log_info / log_ok / log_warn / log_error 彩色日志输出(信息/成功/警告/错误)
sleep_fractional 跨平台小数秒睡眠,支持 sleepperlpython 多种方式
handle_invalid_input 无效输入提示
break_end 操作完成后等待按键继续
exit_script 退出脚本,带动画效果
ask_if_empty 交互式输入配置(带默认值)
interactive_setup 交互式配置向导
extract_archive 解压单个压缩包到目标目录
scan_archives 扫描压缩包目录,构建项目列表
parse_selection 解析用户选择(支持序号、范围、混合选择)
calc_layout 计算显示布局(索引宽度、名称宽度)
print_row 打印双列项目列表行
batch_extract 批量解压多个项目
main_menu 主菜单循环

相关命令