脚本

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
```