脚本

https://github.xyh.moe:8888/xian.yuheng/docker-push
#! /bin/bash

# 禁止 ^C 显示
if [ -t 0 ]; then
    stty -echoctl
    #stty echoctl
fi
################################################################################
# 帮助文档
help_str=$(cat << EOF
用法: $0 [选项]
    -h, --help               帮助文档
    -f, --file               指定镜像列表文件
    -r, --registry           自定义推送仓库地址
    -a, --arch               指定架构 "linux/amd64" "linux/arm64"
    --delete                 删除已推送本地镜像
    CTRL + C                 跳过当前镜像
    CTRL + \                 退出脚本程序
EOF
)
#################################################################################
# 函数
function check_requirements() {
    local missing=()

    for cmd in skopeo jq docker; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done

    if [[ ${#missing[@]} -gt 0 ]]; then
        echo "以下必要命令未安装,请先安装后再运行脚本:"
        for cmd in "${missing[@]}"; do
            echo "  - $cmd"
        done
        exit 1
    fi
}

function generate_horizontal_line() {
    # 创建一条和终端等宽的 分割线 [horizontal line]
    if [ -t 1 ]; then
        printf "%0.s=" $(seq $(tput cols)) && printf "\n"
    else
        printf "%0.s=" $(seq 100) && printf "\n"
    fi
}

skip_signal=false
function handle_skip()
{   # 跳过信号
    skip_signal=true
}
trap handle_skip SIGINT     # Ctrl+C -> 跳过

quit_signal=false
function handle_quit()
{   # 退出信号
    echo -e "❌ 收到退出信号,正在终止脚本..."
    quit_signal=true
}
trap handle_quit SIGQUIT    # Ctrl+\ -> 退出

function check_signals()
{   # 检查跳过和终止信号
    local image="$1"
    if [[ "$quit_signal" == true ]]; then
        exit 0
    fi

    if [[ "$skip_signal" == true ]]; then
        timed_echo "\e[33m[跳过镜像] $image\e[0m"
        skip_signal=false
        return 1   # 返回 1 表示跳过当前镜像
    fi

    return 0       # 返回 0 表示继续处理
}

function timed_echo()
{
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo -e "[$timestamp] $*"
}

function check_remote_image() {
    # $1 源镜像
    # $2 标镜像
    
    # return 0 一致
    # return 1 不同 或 标镜像不存在
    # return 2 源镜像不存在
    # return 3 拉取次数受限

    $dockerhub_limited && [[ "$1" == docker.io/* ]] && return 3
    
    local ajson bjson acreated bcreated

    export HTTP_PROXY="http://tianjin.xyh.moe:42200"
    export HTTPS_PROXY="http://tianjin.xyh.moe:42200"
    export NO_PROXY="localhost,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.xyh.moe"
    
    ajson=$(skopeo inspect docker://$1 2>&1)
    [[ "$ajson" == *"manifest unknown"* ]] && return 2
    [[ "$ajson" == *"toomanyrequests"* ]] && dockerhub_limited=true && return 3
    bjson=$(skopeo inspect docker://$2 2>/dev/null)
    [[ -z "$bjson" ]] && return 1
    
    acreated=$(echo "$ajson" | jq -r .Created)
    bcreated=$(echo "$bjson" | jq -r .Created)

    # echo $acreated
    # echo $bcreated

    if [[ "$acreated" == "$bcreated" ]]; then
        return 0
    else
        return 1
    fi
}

function read_image_file()
{   # 处理 输入文件
    # 排除 空白字符行
    # 排除 # 开头的行
    # 排除 > 开头的行
    # 排除 ` 开头的行

    local input_file="$1"
    local image_list=()
    while IFS= read -r line; do
        [[ "$line" =~ ^\s*$ ]] && continue
        [[ "$line" =~ ^[#\>] ]] && continue
        [[ "$line" =~ ^\` ]] && continue
        image_list+=("$line")
    done < "$input_file"
    echo "${image_list[@]}"
}

function format_image_name()
{   # 格式化镜像名
    local image="$1"
    local fallback_registry="$2"

    local tag name

    # 判断是否带tag,是否带:,且冒号后是否不带 /。
    if [[ "$image" == *:* && "${image##*:}" != */* ]]; then
        tag="${image##*:}"
        name="${image%:*}"
    else
        tag="latest"
        name="$image"
    fi

    IFS='/' read -ra parts <<< "$name"

    local registry repo_parts image_name repo

    if [[ "${#parts[@]}" -ge 2 && ( "${parts[0]}" == *.* || "${parts[0]}" == *:* || "${parts[0]}" == "localhost" ) ]]; then
        registry="$fallback_registry"
        image_name="${parts[${#parts[@]}-1]}"
        repo_parts=("${parts[@]:1:${#parts[@]}-2}")
    else
        registry="$fallback_registry"
        image_name="${parts[${#parts[@]}-1]}"
        repo_parts=("${parts[@]:0:${#parts[@]}-1}")
        [[ "${#repo_parts[@]}" -eq 0 ]] && repo_parts=("library")
    fi

    repo="$(IFS=/; echo "${repo_parts[*]}")"

    if [[ -n "$repo" ]]; then
        echo "${registry}/${repo}/${image_name}:${tag}"
    else
        echo "${registry}/library/${image_name}:${tag}"
    fi
}
################################################################################
# 依赖检测
check_requirements
# 检测是否有 docker 运行权限
docker version    1>/dev/null 2>/dev/null || { echo -e "\e[31m[错误] 无 Docker 运行权限。\e[0m"; exit 1; }
# 指定架构
architecture=""
# DockerHub 拉取受限默认值
dockerhub_limited=false
################################################################################
# 参数判断
ARGS=` \
    getopt \
    -o hf:r:t:a: \
    -l help,file:,registry:,arch,force-tag:,delete \
    -n "$0" \
    -- "$@" \
`
[ $? != 0 ] && echo -e "$help_str" && exit 1;
eval set -- "${ARGS}"

while true; do
    case "$1" in
        -h | --help)             echo -e "$help_str"; exit  0;;
        -f | --file)                       file="$2"; shift 2;;
        -r | --registry)        custom_registry="$2"; shift 2;;
        -a | --arch)               architecture="$2"; shift 2;;
        --delete)                  remove_local=true; shift  ;;
        --) shift; break;;
        *) echo "未知错误"; exit 1 ;;
    esac
done

# 推送源
push_registry="${custom_registry:-docker.io}"
################################################################################
if [[ -n "$file" ]]; then
    images=($(read_image_file "$file"))
else
    images=($(docker images --format '{{.Repository}}:{{.Tag}}'))
fi

generate_horizontal_line
printf "%-24s: %s\n" 推送仓库地址 "$push_registry"
printf "%-24s: %s\n" 镜像列表文件 "${file:-[使用本地镜像]}"
generate_horizontal_line

# 输出 镜像 列表
index=1
for image in "${images[@]}"; do
    printf "%4d. %s\n" "$index" "$image"
    ((index++))
done
generate_horizontal_line
################################################################################
# 推送镜像
for image in "${images[@]}";do
    check_signals "$image" || continue
    
    formated_image_name=$(format_image_name "$image" "$push_registry")
    
    # 获取 tag
    # tag="${image##*:}"
    # force_tag=false
    
    timed_echo "\e[32m[检测镜像] $image\e[0m"
    
    check_remote_image "$image" "$formated_image_name"
    
    case $? in
        0)
            # 镜像一致
            timed_echo "\e[34m[镜像一致] $image\e[0m"
            continue
            ;;
        1)
            # 标镜像不存在 或 镜像不同
            timed_echo "\e[34m[开始拉取] $image\e[0m"
            if [[ -n "$architecture" ]]; then
                docker pull --platform "$architecture" "$image" 1>/dev/null 2>/dev/null
            else
                docker pull                            "$image" 1>/dev/null 2>/dev/null
            fi
            
            if [[ $? == 0 ]]; then
                timed_echo "\e[34m[拉取成功] $image\e[0m"
                check_signals "$image" || continue
                docker tag "$image" "$formated_image_name"
                docker push "$formated_image_name" 1>/dev/null
                if [[ $? == 0 ]]; then
                    cleaning_list+=("$image")
                    timed_echo "\e[34m[推送成功] $formated_image_name\e[0m"
                else
                    timed_echo "\e[31m[推送失败] $formated_image_name\e[0m"
                fi
                docker rmi "$formated_image_name" 1>/dev/null 2>/dev/null
            else
                check_signals "$image" || continue
                timed_echo "\e[31m[拉取失败] $image\e[0m"
            fi
            ;;
        2)
            # 源镜像不存在
            timed_echo "\e[31m[无源镜像] $image\e[0m"
            continue
            ;;
        3)
            # DockerHub 拉取受限
            timed_echo "\e[33m[拉取受限] $image\e[0m"
            continue
            ;;
        *)
            # 其它
            timed_echo "\e[31m[其它故障] $image\e[0m"
            continue
            ;;
    esac
done
################################################################################
# 清理镜像
if [[ "$remove_local" == true ]]; then
    #generate_horizontal_line
    #echo "开始删除本地镜像..."
    for image in "${cleaning_list[@]}"; do
        docker rmi "$image" 1>/dev/null 2>/dev/null
        [ $? -eq 0 ] && timed_echo "\e[32m[清理镜像] $image\e[0m"
    done
    #echo "本地镜像删除完成"
    #generate_horizontal_line
fi
################################################################################

exit 0

镜像文件格式

# List

## Nginx

> <https://nginx.org/>

```text
docker.io/library/nginx:1.25-alpine
docker.io/library/nginx:1.25.0-alpine
docker.io/library/nginx:1.25.1-alpine
docker.io/library/nginx:1.25.2-alpine
```