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