export-images.sh docker 容器镜像导出脚本

#!/bin/bash

# 脚本版本号
SCRIPT_VERSION="1.1.4"

# 日志函数
log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$TIMESTAMP] [$LEVEL] $MESSAGE"
}

# 显示帮助信息
usage() {
    echo
    echo "用法: $0 [选项]"
    echo
    echo "选项:"
    echo " --onefile       将所有镜像导出到一个tar文件中"
    echo " --compress      在导出每个镜像后立即压缩tar文件"
    echo " --dir <目录>    指定导出文件存放目录,默认为 $HOME/docker_images_<时间戳>"
    echo " -h, --help      显示此帮助信息"
    echo
    echo "版本: $SCRIPT_VERSION"
    echo
}

# 全局变量
EXPORT_DIR=""
declare -A INCOMPLETE_FILES  # 使用关联数组更可靠
SHOULD_EXIT=false
CURRENT_PID=""

# 清理函数
cleanup() {
    log "INFO" "检测到中断信号,正在终止所有操作..."
    SHOULD_EXIT=true
    
    # 终止当前正在运行的进程
    if [ -n "$CURRENT_PID" ] && ps -p "$CURRENT_PID" > /dev/null; then
        kill -TERM "$CURRENT_PID" 2>/dev/null
        wait "$CURRENT_PID" 2>/dev/null
    fi

    log "INFO" "正在清理未完成的文件..."
    # 清理未完成的文件
    for FILE in "${!INCOMPLETE_FILES[@]}"; do
        if [[ -f "$FILE" ]]; then
            log "INFO" "删除未完整保存的文件: ${FILE##*/}"
            rm -f "$FILE"
        fi
    done
    
    # 额外清理可能遗留的.incomplete文件
    find "$EXPORT_DIR" -name "*.incomplete" -exec rm -f {} \;
    find "$EXPORT_DIR" -name "*.tmp" -exec rm -f {} \;
    
    log "INFO" "未完成的文件清理完成。"

    # 提示用户是否删除导出目录
    local TS=$(date +"%Y-%m-%d %H:%M:%S")
    printf "[%s] [INFO] 是否删除保存的文件夹 %s? (y/N): " "$TS" "$EXPORT_DIR"
    
    # 设置无缓冲输入并显示输入字符
    saved_stty=$(stty -g)
    stty raw -echo
    REPLY=$(dd bs=1 count=1 2>/dev/null)
    stty "$saved_stty"
    
    # 显示用户输入并处理
    case "$REPLY" in
        [Yy]) echo -n "y" ;;
        [Nn]) echo -n "n" ;;
        *)    echo -n "" ;;
    esac
    echo  # 换行
    
    if [[ $REPLY =~ ^[Yy]$ ]]; then
        log "INFO" "删除导出目录: $EXPORT_DIR"
        rm -rf "$EXPORT_DIR"
    else
        log "INFO" "保留导出目录: $EXPORT_DIR"
    fi
    exit 1
}

# 捕获退出信号
trap cleanup SIGINT SIGTERM SIGHUP

# 解析参数
while [[ $# -gt 0 ]]; do
    case "$1" in
        --onefile)
            ONEFILE=true
            ;;
        --compress)
            COMPRESS=true
            ;;
        --dir)
            if [[ -n "$2" ]]; then
                EXPORT_DIR="$2"
                shift
            else
                log "ERROR" "--dir 参数需要一个目录路径"
                usage
                exit 1
            fi
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            log "ERROR" "未知选项: $1"
            usage
            exit 1
            ;;
    esac
    shift
done

# 获取当前时间戳
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# 默认导出目录
EXPORT_DIR="${EXPORT_DIR:-$HOME/docker_images_$TIMESTAMP}"

# 创建导出目录
mkdir -p "$EXPORT_DIR"
if [ $? -ne 0 ]; then
    log "ERROR" "无法创建导出目录: $EXPORT_DIR"
    exit 1
fi
log "INFO" "创建导出目录: $EXPORT_DIR"

# 获取所有镜像ID
IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}")

# 检查是否获取到镜像
if [ -z "$IMAGES" ]; then
    log "ERROR" "未找到Docker镜像。"
    exit 1
fi

# 导出镜像函数
export_images() {
    if [ "$ONEFILE" = true ]; then
        # 合并所有镜像到一个tar包
        log "INFO" "正在将所有镜像导出到一个tar文件..."
        TAR_FILE="$EXPORT_DIR/docker_images_$TIMESTAMP.tar"
        INCOMPLETE_FILES["$TAR_FILE.incomplete"]=1
        
        docker save $IMAGES > "$TAR_FILE.incomplete" &
        CURRENT_PID=$!
        wait $CURRENT_PID
        CURRENT_PID=""
        
        if [ $? -eq 0 ] && [ "$SHOULD_EXIT" != true ]; then
            mv "$TAR_FILE.incomplete" "$TAR_FILE"
            unset INCOMPLETE_FILES["$TAR_FILE.incomplete"]
        else
            return 1
        fi

        if [ "$COMPRESS" = true ] && [ "$SHOULD_EXIT" != true ]; then
            log "INFO" "正在压缩tar文件: $TAR_FILE"
            GZ_FILE="$TAR_FILE.gz"
            INCOMPLETE_FILES["$GZ_FILE.incomplete"]=1
            
            (gzip -c "$TAR_FILE" > "$GZ_FILE.incomplete" && \
             mv "$GZ_FILE.incomplete" "$GZ_FILE") &
            CURRENT_PID=$!
            wait $CURRENT_PID
            CURRENT_PID=""
            
            if [ $? -eq 0 ] && [ "$SHOULD_EXIT" != true ]; then
                unset INCOMPLETE_FILES["$GZ_FILE.incomplete"]
            else
                return 1
            fi
        fi
        log "INFO" "所有镜像已导出到 $TAR_FILE"
    else
        # 分别导出每个镜像到单独的tar包
        for IMAGE in $IMAGES; do
            if [ "$SHOULD_EXIT" = true ]; then
                break
            fi
            
            log "INFO" "正在导出镜像: $IMAGE"
            SAFE_NAME=$(echo "$IMAGE" | tr '/' '_' | tr ':' '_')
            IMAGE_FILE="$EXPORT_DIR/${SAFE_NAME}.tar"
            INCOMPLETE_FILES["$IMAGE_FILE.incomplete"]=1
            
            docker save "$IMAGE" > "$IMAGE_FILE.incomplete" &
            CURRENT_PID=$!
            wait $CURRENT_PID
            CURRENT_PID=""
            
            if [ $? -eq 0 ] && [ "$SHOULD_EXIT" != true ]; then
                mv "$IMAGE_FILE.incomplete" "$IMAGE_FILE"
                unset INCOMPLETE_FILES["$IMAGE_FILE.incomplete"]
            else
                continue
            fi

            if [ "$COMPRESS" = true ] && [ "$SHOULD_EXIT" != true ]; then
                log "INFO" "正在压缩tar文件: ${IMAGE_FILE##*/}"
                GZ_FILE="$IMAGE_FILE.gz"
                INCOMPLETE_FILES["$GZ_FILE.incomplete"]=1
                
                (gzip -c "$IMAGE_FILE" > "$GZ_FILE.incomplete" && \
                 mv "$GZ_FILE.incomplete" "$GZ_FILE") &
                CURRENT_PID=$!
                wait $CURRENT_PID
                CURRENT_PID=""
                
                if [ $? -eq 0 ] && [ "$SHOULD_EXIT" != true ]; then
                    unset INCOMPLETE_FILES["$GZ_FILE.incomplete"]
                    rm -f "$IMAGE_FILE"  # 压缩成功后删除原tar文件
                fi
            fi
        done
        log "INFO" "镜像导出完成。"
    fi
}

# 执行导出
export_images

if [ "$SHOULD_EXIT" = true ]; then
    exit 1
fi

log "INFO" "导出完成。"

  

posted @ 2025-08-12 18:06  Nihaorz  阅读(10)  评论(0)    收藏  举报