feat: 更新默认shell脚本

This commit is contained in:
2025-04-09 17:22:26 +08:00
parent 319443102e
commit 1c52e8c3e9
18 changed files with 1652 additions and 931 deletions
-108
View File
@@ -1,108 +0,0 @@
#!/bin/bash
# Ver: 1.5 by Endial Fang (endial@126.com)
#
# 通用函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 打印包含包含Logo的欢迎信息
print_welcome_info() {
[[ -n "${APP_NAME:-}" ]] && github_repo="https://github.com/colovu/docker-${APP_NAME}"
LOG_I " ____ _ "
LOG_I " / ___|___ | | _____ ___ _ "
LOG_I "| | / _ \| |/ _ \ \ / / | | | Docker : ${BOLD}${APP_NAME:-undefined}${RESET}"
LOG_I "| |__| (_) | | (_) \ V /| |_| | Version: ${BOLD}${APP_VER:-0.0}${RESET}"
LOG_I " \____\___/|_|\___/ \_/ \__,_| PowerBy: ${BOLD}Endial@126.com${RESET}"
LOG_D " Project Repo: ${github_repo:-}"
LOG_I ""
}
# 根据需要打印欢迎信息
print_image_welcome() {
if [[ ! "$(id -u)" = "0" ]]; then
print_welcome_info
fi
}
# 检测可能导致容器执行后直接退出的命令,如"--help";如果存在,直接返回 0
# 参数:
# $1 - 待检测的参数表
print_command_help() {
local arg
for arg; do
case "$arg" in
-'?'|-H|-h|--help|-help|-V|-v|--version|-version)
exec "$@"
exit
;;
esac
done
}
# 检测应用相应的配置文件是否存在,如果不存在,则从默认配置文件目录拷贝一份
# 默认配置文件路径:/etc/${APP_NAME}
# 目标配置文件路径:/srv/conf/${APP_NAME}
# 参数:
# $1 - 基础路径
# $* - 基础路径下的文件及目录列表,以" "分割
# 例子:
# ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf
ensure_config_file_exist() {
local -r base_path="${1:?paths is missing}"
local f=""
local dist=""
shift 1
LOG_D "List to check: $@"
while [ "$#" -gt 0 ]; do
f="${1}"
LOG_D " Process \"${f}\""
if [ -d "${base_path}/${f}" ]; then
dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')"
[[ ! -d "${dist}" ]] && LOG_D " Create directory: ${dist}" && mkdir -p "${dist}"
[[ ! -z $(ls -A "${base_path}/${f}") ]] && ensure_config_file_exist "${base_path}/${f}" $(ls -A "${base_path}/${f}")
else
dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')"
[[ ! -e "${dist}" ]] && LOG_D " Copy: ${base_path}/${f} ===> ${dist}" && cp "${base_path}/${f}" "${dist}" && rm -rf "/srv/conf/${APP_NAME}/.app_init_flag"
fi
shift
done
}
# 根据脚本扩展名及权限,执行相应的初始化脚本
# 参数:
# $1 - 文件列表,支持路径通配符
# 使用:
# process_init_files [file [file [...]]]
# 例子:
# process_init_files /src/conf/${APP_NAME}/initdb.d/*
process_init_files() {
echo
local f
for f; do
case "$f" in
*.sh)
if [ -x "$f" ]; then
LOG_I "$0: running $f"
"$f"
else
LOG_I "$0: sourcing $f"
. "$f"
fi
;;
*) LOG_W "$0: ignoring $f" ;;
esac
echo
done
}
# 检测当前脚本是被直接执行的,还是从其他脚本中使用 "source" 调用的
is_sourced() {
[ "${#FUNCNAME[@]}" -ge 2 ] \
&& [ "${FUNCNAME[0]}" = 'is_sourced' ] \
&& [ "${FUNCNAME[1]}" = 'source' ]
}
-93
View File
@@ -1,93 +0,0 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 文件操作函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
# 函数列表
# 检测"*_FILE"文件,并从文件中读取信息作为参数值;环境变量不允许 VAR 与 VAR_FILE 方式并存
# 变量:
# $1 - 需要设置的环境变量名称
# $2 - 该变量对应的默认值(Option)
#
# 使用: file_env ENV_VAR [DEFAULT]
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
LOG_E "Both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# 使用规则表达式在文件中替换数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 替代数据表达式
# $4 - 是否使用POSIX表达式. Default: true
replace_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local substitute_regex="${3:?substitute regex is required}"
local posix_regex=${4:-true}
local result
# 因部分系统兼容性问题,需要防止使用 'sed in-place' 方式操作
if [[ $posix_regex = true ]]; then
result="$(sed -E "s@$match_regex@$substitute_regex@g" "$filename")"
else
result="$(sed "s@$match_regex@$substitute_regex@g" "$filename")"
fi
echo "$result" > "$filename"
}
# 使用规则表达式在文件中删除数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 是否使用POSIX表达式. Default: true
remove_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local posix_regex=${3:-true}
local result
# 因部分系统兼容性问题,需要防止使用 'sed in-place' 方式操作
if [[ $posix_regex = true ]]; then
result="$(sed -E "/$match_regex/d" "$filename")"
else
result="$(sed "/$match_regex/d" "$filename")"
fi
echo "$result" > "$filename"
}
# 在符合条件的行后增加文本
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 待增加的文本
append_in_file() {
local file="${1:?missing file}"
local match_regex="${2:?missing pattern}"
local value="${3:?missing value}"
# We read the file in reverse, replace the first match (0,/pattern/s) and then reverse the results again
result="$(tac "$file" | sed -E "0,/($match_regex)/s||${value}\n\1|" | tac)"
echo "$result" > "$file"
}
-83
View File
@@ -1,83 +0,0 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 日志输出函数库
#[[ ${ENV_DEBUG:-false} = true ]] && set -x
MODULE="$(basename "$0")"
RESET='\033[0m'
BOLD='\033[1m'
# 前景色
BLACK='\033[38;5;0m'
RED='\033[38;5;1m'
GREEN='\033[38;5;2m'
YELLOW='\033[38;5;3m'
BLUE='\033[38;5;4m'
MAGENTA='\033[38;5;5m'
CYAN='\033[38;5;6m'
WHITE='\033[38;5;7m'
# 背景色
ON_BLACK='\033[48;5;0m'
ON_RED='\033[48;5;1m'
ON_GREEN='\033[48;5;2m'
ON_YELLOW='\033[48;5;3m'
ON_BLUE='\033[48;5;4m'
ON_MAGENTA='\033[48;5;5m'
ON_CYAN='\033[48;5;6m'
ON_WHITE='\033[48;5;7m'
# 函数列表
# 打印输出到 STDERR 设备
stderr_print() {
printf "%b\\n" "${*}" >&2
}
# 输出实际日志信息
# 参数:
# $1 - 日志信息
LOG() {
local -r bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
debugInfo="${CYAN}${APP_NAME:-}:${MODULE:-}"
else
debugInfo="${CYAN}${APP_NAME:-}"
fi
stderr_print "${debugInfo} ${MAGENTA}$(date "+%F %T.%3N")${RESET} ${*}"
}
# 输出调试类日志信息,尽量少使用
# 参数:
# $1 - 日志信息
LOG_D() {
local -r bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
LOG "${BLUE}DBG${RESET}: ${*}"
fi
}
# 输出提示信息类日志信息
# 参数:
# $1 - 日志信息
LOG_I() {
LOG "${GREEN}INF${RESET}: ${*}"
}
# 输出警告类日志信息至sterr
# 参数:
# $1 - 日志信息
LOG_W() {
LOG "${YELLOW}WRN${RESET}: ${*}"
}
# 输出错误类日志信息至sterr,并退出脚本
# 参数:
# $1 - 日志信息
LOG_E() {
LOG "${RED}ERR${RESET}: ${*}"
}
-302
View File
@@ -1,302 +0,0 @@
#!/bin/bash
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 操作系统控制函数库
# 加载依赖项
. /colovu/lib/libfs.sh # 文件系统函数库
# 函数列表
# 检测指定用户账户是否存在
# 参数:
# $1 - 用户账户
# 返回值:
# 0 / 1
is_user_exists() {
local user="${1:?user is missing}"
id "$user" >/dev/null 2>&1
}
# 检测指定用户分组是否存在
# 参数:
# $1 - 用户组
# 返回值:
# 0 / 1
is_group_exists() {
local group="${1:?group is missing}"
getent group "$group" >/dev/null 2>&1
}
# 检测当前是否为 root 用户
# 返回值:
# true / false
is_root() {
if [[ "$(id -u)" = "0" ]]; then
LOG_D "Run as root."
true
else
LOG_D "Run as non-root: $(id -u)"
false
fi
}
# 确保指定用户组在系统中存在
# 参数:
# $1 - 用户组
# 标志位:
# -s|--system - 创建系统用户 (uid <= 999)
ensure_group_exists() {
local group="${1:?group is missing}"
local is_system_user=false
# 检测标志位
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-s|--system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! is_group_exists "$group"; then
local -a args=("$group")
$is_system_user && args+=("--system")
groupadd "${args[@]}" >/dev/null 2>&1
fi
}
# 确保指定用户在系统中存在
# 参数:
# $1 - 用户
# 标志位:
# -g|--group - 用户组
# -h|--home - 用户家目录
# -s|--system - 创建系统用户 (uid <= 999)
ensure_user_exists() {
local user="${1:?user is missing}"
local group=""
local home=""
local is_system_user=false
# Validate arguments
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-g|--group)
shift
group="${1:?missing group}"
;;
-h|--home)
shift
home="${1:?missing home directory}"
;;
-s|--system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! is_user_exists "$user"; then
local -a user_args=("-N" "$user")
$is_system_user && user_args+=("--system")
useradd "${user_args[@]}" >/dev/null 2>&1
fi
if [[ -n "$group" ]]; then
local -a group_args=("$group")
$is_system_user && group_args+=("--system")
ensure_group_exists "${group_args[@]}"
usermod -g "$group" "$user" >/dev/null 2>&1
fi
if [[ -n "$home" ]]; then
mkdir -p "$home"
usermod -d "$home" "$user" >/dev/null 2>&1
configure_permissions_ownership "$home" -d "775" -f "664" -u "$user" -g "$group"
fi
}
# 获取系统可用内存大小(MB)信息
# 返回值:
# 内存大小(兆字节)
get_total_memory() {
echo $(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024))
}
# 获取以内存定量方式描述的机器类型
# 标志位:
# --memory - 内存大小 (MB,可选)
# 返回值:
# 类型名称
get_machine_size() {
local memory=""
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
--memory)
shift
memory="${1:?missing memory}"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if [[ -z "$memory" ]]; then
debug "Memory was not specified, detecting available memory automatically"
memory="$(get_total_memory)"
fi
sanitized_memory=$(convert_to_mb "$memory")
if [[ "$sanitized_memory" -gt 26000 ]]; then
echo 2xlarge
elif [[ "$sanitized_memory" -gt 13000 ]]; then
echo xlarge
elif [[ "$sanitized_memory" -gt 6000 ]]; then
echo large
elif [[ "$sanitized_memory" -gt 3000 ]]; then
echo medium
elif [[ "$sanitized_memory" -gt 1500 ]]; then
echo small
else
echo micro
fi
}
# 获取已定义的所有内存大小描述
# 返回值:
# 描述值列表
get_supported_machine_sizes() {
echo micro small medium large xlarge 2xlarge
}
# 将以字符串表示的内存大小转换为以MB为单位的内存大小值 (i.e. 2G -> 2048)
# 参数:
# $1 - 内存大小
# 返回值:
# 转换后的数值
convert_to_mb() {
local amount="${1:-}"
if [[ $amount =~ ^([0-9]+)(M|G) ]]; then
size="${BASH_REMATCH[1]}"
unit="${BASH_REMATCH[2]}"
if [[ "$unit" = "G" ]]; then
amount="$((size * 1024))"
else
amount="$size"
fi
fi
echo "$amount"
}
# 如果禁用调试模式,将输出信息重定向至 /dev/null
# 参数:
# $@ - 待执行的命令
debug_execute() {
local bool="${ENV_DEBUG:-false}"
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
# 重试执行命令
# 参数:
# $1 - 命令 (字符串)
# $2 - 最大尝试次数. 默认值: 12
# $3 - 重试前等待时间(秒). 默认值: 5
# 返回值:
# 0 / 1
retry_while() {
local -r cmd="${1:?cmd is missing}"
local -r retries="${2:-12}"
local -r sleep_time="${3:-5}"
local return_value=1
read -r -a command <<< "$cmd"
for ((i = 1 ; i <= retries ; i+=1 )); do
"${command[@]}" && return_value=0 && break
sleep "$sleep_time"
done
return $return_value
}
# 生成随机字符串
# 标志位:
# -t|--type - 字符串类型 (ascii, alphanumeric, numeric). 默认值: ascii
# -c|--count - 字符串长度. 默认值: 32
# 返回值:
# 字符串
generate_random_string() {
local type="ascii"
local count="32"
local filter
local result
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
-t|--type)
shift
type="$1"
;;
-c|--count)
shift
count="$1"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
# 检测类型
case "$type" in
ascii)
filter="[:print:]"
;;
alphanumeric)
filter="a-zA-Z0-9"
;;
numeric)
filter="0-9"
;;
*)
echo "Invalid type ${type}" >&2
return 1
esac
# Obtain count + 10 lines from /dev/urandom to ensure that the resulting string has the expected size
# Note there is a very small chance of strings starting with EOL character
# Therefore, the higher amount of lines read, this will happen less frequently
result="$(head -n "$((count + 10))" /dev/urandom | tr -dc "$filter" | head -c "$count")"
echo "$result"
}
# 为指定字符串生成 MD5 值
# 参数:
# $1 - 字符串
# 返回值:
# 字符串对应的 MD5
generate_md5_hash() {
local -r str="${1:?missing input string}"
echo -n "$str" | md5sum | awk '{print $1}'
}
-212
View File
@@ -1,212 +0,0 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
#
# 服务管理函数库
# shellcheck disable=SC1091
# 加载依赖项
. /colovu/lib/libvalidations.sh # 数据有效性检测函数库
# 函数列表
# 获取并返回服务 PID
# 参数:
# $1 - PID 文件
# 返回值:
# PID
get_pid_from_file() {
local pid_file="${1:?pid file is missing}"
if [[ -f "$pid_file" ]]; then
if [[ -n "$(< "$pid_file")" ]] && [[ "$(< "$pid_file")" -gt 0 ]]; then
echo "$(< "$pid_file")"
fi
fi
}
# 检测 PID 对应的服务是否在运行中
# 参数:
# $1 - PID
# 返回值:
# 0 / 1
is_service_running() {
local pid="${1:?pid is missing}"
kill -0 "$pid" 2>/dev/null
}
# 通过发送信号停止一个指定 PID 的服务
# 参数:
# $1 - PID 文件
# $2 - 信号 (可选)
stop_service_using_pid() {
local pid_file="${1:?pid file is missing}"
local signal="${2:-}"
local pid
pid="$(get_pid_from_file "$pid_file")"
[[ -z "$pid" ]] || ! is_service_running "$pid" && return
if [[ -n "$signal" ]]; then
kill "-${signal}" "$pid"
else
kill "$pid"
fi
local counter=10
while [[ "$counter" -ne 0 ]] && is_service_running "$pid"; do
sleep 1
counter=$((counter - 1))
done
}
# 启动一个 cron 守护进程
# 返回值:
# true / false
cron_start() {
if [[ -x "/usr/sbin/cron" ]]; then
/usr/sbin/cron
elif [[ -x "/usr/sbin/crond" ]]; then
/usr/sbin/crond
else
false
fi
}
# 为指定的服务生成 cron 配置文件
# 参数:
# $1 - 服务名称
# $2 - 命令
# 标志位:
# --run-as - 运行的用户. 默认值: root
# --schedule - Cron 周期配置. 默认值: * * * * *
generate_cron_conf() {
local service_name="${1:?service name is missing}"
local cmd="${2:?command is missing}"
local run_as="root"
local schedule="* * * * *"
local clean="true"
local clean="true"
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--run-as)
shift
run_as="$1"
;;
--schedule)
shift
schedule="$1"
;;
--no-clean)
clean="false"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p /etc/cron.d
if "$clean"; then
echo "${schedule} ${run_as} ${cmd}" > /etc/cron.d/"$service_name"
else
echo "${schedule} ${run_as} ${cmd}" >> /etc/cron.d/"$service_name"
fi
}
# 为指定的服务生成 monit 配置文件
# 参数:
# $1 - 服务名
# $2 - PID 文件
# $3 - 启动命令
# $4 - 停止命令
# 标志位:
# --disabled - 是否禁用. 默认值: no
generate_monit_conf() {
local service_name="${1:?service name is missing}"
local pid_file="${2:?pid file is missing}"
local start_command="${3:?start command is missing}"
local stop_command="${4:?stop command is missing}"
local monit_conf_dir="/etc/monit/conf.d"
local disabled="no"
# 检测标志位
shift 4
while [[ "$#" -gt 0 ]]; do
case "$1" in
--disabled)
shift
disabled="$1"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
is_boolean_yes "$disabled" && conf_suffix=".disabled"
mkdir -p "$monit_conf_dir"
cat >"${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" <<EOF
check process ${service_name}
with pidfile "${pid_file}"
start program = "${start_command}" with timeout 90 seconds
stop program = "${stop_command}" with timeout 90 seconds
EOF
}
# 为指定的服务生成 Logrotate 配置文件
# 参数:
# $1 - 应用名称
# $2 - 日志路径
# 标志位:
# --period - 周期
# --rotations - Rotations 存储的数量
# --extra - 扩展参数 (可选)
generate_logrotate_conf() {
local service_name="${1:?service name is missing}"
local log_path="${2:?log path is missing}"
local period="weekly"
local rotations="150"
local extra=""
local logrotate_conf_dir="/etc/logrotate.d"
local var_name
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--period|--rotations|--extra)
var_name="$(echo "$1" | sed -e "s/^--//" -e "s/-/_/g")"
shift
declare "$var_name"="${1:?"$var_name" is missing}"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p "$logrotate_conf_dir"
cat <<EOF | sed '/^\s*$/d' >"${logrotate_conf_dir}/${service_name}"
${log_path} {
${period}
rotate ${rotations}
dateext
compress
copytruncate
missingok
$(indent "$extra" 2)
}
EOF
}
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
# Ver: 1.6 by Endial Fang (endial@126.com)
#
# 通用函数库
# 加载依赖项
source "/opt/colovu/lib/liblog.sh"
# 函数列表
# 打印包含包含Logo的欢迎信息
print_welcome_info() {
[[ -n "${APP_NAME:-}" ]] && github_repo="https://gitee.com/colovu/docker-${APP_NAME}"
info " ____ _ "
info " / ___|___ | | _____ ___ _ "
info "| | / _ \| |/ _ \ \ / / | | | Application: ${APP_NAME:-undefined}"
info "| |__| (_) | | (_) \ V /| |_| | Version: ${APP_VER:-0.0.0}"
info " \____\___/|_|\___/ \_/ \__,_| PowerBy: Endial@126.com"
debug " Project Repo: ${github_repo:-}"
info ""
}
# 根据需要打印欢迎信息
print_image_welcome() {
if [[ -z "${DISABLE_WELCOME_MESSAGE:-}" ]]; then
if [[ ! "$(id -u)" = "0" ]]; then
print_welcome_info
fi
fi
}
# 检测可能导致容器执行后直接退出的命令,如"--help";如果存在,直接返回 0
# 参数:
# $1 - 待检测的参数表
print_command_help() {
local arg
for arg; do
if [[ "$arg" =~ ^(-[?HhVv]|--help|--version)$ ]]; then
exec "$@"
exit
fi
done
}
# 检测应用相应的配置文件是否存在,如果不存在,则从默认配置文件目录拷贝一份
# 默认配置文件路径:/etc/${APP_NAME}
# 目标配置文件路径:/srv/conf/${APP_NAME}
# 参数:
# $1 - 基础路径
# $* - 基础路径下的文件及目录列表,以" "分割
# 例子:
# ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf
ensure_config_file_exist() {
local -r base_path="${1:?paths is missing}"
shift
local files=("$@")
debug "List to check: ${files[*]}"
for f in "${files[@]}"; do
debug "Processing: ${f}"
local dist="${base_path/$'\/etc'/$'\/srv\/conf'}/${f}"
if [[ -d "${base_path}/${f}" ]]; then
[[ ! -d "$dist" ]] && { debug "Creating directory: ${dist}"; mkdir -p "$dist"; }
[[ $(ls -A "${base_path}/${f}") ]] &&
ensure_config_file_exist "${base_path}/${f}" $(ls -A "${base_path}/${f}")
else
[[ ! -e "$dist" ]] && {
debug "Copying: ${base_path}/${f} => ${dist}"
cp "${base_path}/${f}" "$dist"
rm -f "/srv/conf/${APP_NAME}/.app_init_flag"
}
fi
done
}
# 根据脚本扩展名及权限,执行相应的初始化脚本
# 参数:
# $1 - 文件列表,支持路径通配符
# 使用:
# process_init_files [file [file [...]]]
# 例子:
# process_init_files /src/conf/${APP_NAME}/initdb.d/*
process_init_files() {
local f
while IFS= read -r -d '' f; do
case "$f" in
*.sh)
if [[ -x "$f" ]]; then
info "$0: executing $f"
"$f"
else
info "$0: sourcing $f"
source "$f"
fi
;;
*) warn "$0: ignoring $f" ;;
esac
done < <(find "$@" -type f -print0 2>/dev/null || true)
}
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 文件操作函数库
# 加载依赖项
source "/opt/colovu/lib/libos.sh"
# 函数列表
# 检测"*_FILE"文件,并从文件中读取信息作为参数值;环境变量不允许 VAR 与 VAR_FILE 方式并存
# 变量:
# $1 - 需要设置的环境变量名称
# $2 - 该变量对应的默认值(Option)
#
# 使用: file_env ENV_VAR [DEFAULT]
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
error "Both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# 使用规则表达式在文件中替换数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 替代数据表达式
# $4 - 是否使用POSIX表达式. Default: true
replace_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local substitute_regex="${3:?substitute regex is required}"
local posix_regex=${4:-true}
local result
# 规避使用'sed in-place' 方式操作
# 1) 部分系统兼容性问题,如从ConfigMap中加载的文件
# 2) 部分系统不支持"in-place" 方式操作
local -r del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues
if [[ $posix_regex = true ]]; then
result="$(sed -E "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")"
else
result="$(sed "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")"
fi
echo "$result" > "$filename"
}
# Replace a regex-matching multiline string in a file
# 使用规则表达式在文件中替换多行数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 替代数据表达式
replace_in_file_multiline() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local substitute_regex="${3:?substitute regex is required}"
local result
local -r del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues
result="$(perl -pe "BEGIN{undef $/;} s${del}${match_regex}${del}${substitute_regex}${del}sg" "$filename")"
echo "$result" > "$filename"
}
# 使用规则表达式在文件中删除数据
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 是否使用POSIX表达式. Default: true
remove_in_file() {
local filename="${1:?filename is required}"
local match_regex="${2:?match regex is required}"
local posix_regex=${3:-true}
local result
# 规避使用'sed in-place' 方式操作
# 1) 部分系统兼容性问题,如从ConfigMap中加载的文件
# 2) 部分系统不支持"in-place" 方式操作
if [[ $posix_regex = true ]]; then
result="$(sed -E "/$match_regex/d" "$filename")"
else
result="$(sed "/$match_regex/d" "$filename")"
fi
echo "$result" > "$filename"
}
# 在符合条件的行后增加文本
# 参数:
# $1 - 文件名
# $2 - 正则表达式
# $3 - 待增加的文本
append_in_file() {
local file="${1:?missing file}"
local match_regex="${2:?missing pattern}"
local value="${3:?missing value}"
# We read the file in reverse, replace the first match (0,/pattern/s) and then reverse the results again
result="$(tac "$file" | sed -E "0,/($match_regex)/s||${value}\n\1|" | tac)"
echo "$result" > "$file"
}
# 等待日志文件中包含指定的条目
# 参数:
# $1 - 待查找的条目
# $2 - 日志文件
# $3 - 最大重试次数. Default: 12
# $4 - 重试间隔时间(秒). Default: 5
wait_for_log_entry() {
local -r entry="${1:-missing entry}"
local -r log_file="${2:-missing log file}"
local -r retries="${3:-12}"
local -r interval_time="${4:-5}"
local attempt=0
check_log_file_for_entry() {
if ! grep -qE "$entry" "$log_file"; then
debug "Entry \"${entry}\" still not present in ${log_file} (attempt $((++attempt))/${retries})"
return 1
fi
}
debug "Checking that ${log_file} log file contains entry \"${entry}\""
if retry_while check_log_file_for_entry "$retries" "$interval_time"; then
debug "Found entry \"${entry}\" in ${log_file}"
true
else
error "Could not find entry \"${entry}\" in ${log_file} after ${retries} retries"
debug_execute cat "$log_file"
return 1
fi
}
@@ -1,10 +1,10 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 文件管理函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
source "/opt/colovu/lib/liblog.sh"
# 函数列表
@@ -12,24 +12,32 @@
# 参数:
# $1 - 文件路径
# $2 - 用户
# $3 - 用户组
ensure_owned_by() {
local path="${1:?path is missing}"
local owner="${2:?owner is missing}"
local group="${3:-}"
chown "$owner":"$owner" "$path"
if [[ -n $group ]]; then
chown "$owner":"$group" "$path"
else
chown "$owner":"$owner" "$path"
fi
}
# 检测目录是否存在,如果不存在则创建,同时修改为指定的用户
# 参数:
# $1 - 目录路径
# $2 - 用户
# $3 - 用户组
ensure_dir_exists() {
local dir="${1:?directory is missing}"
local owner="${2:-}"
local owner_user="${2:-}"
local owner_group="${3:-}"
mkdir -p "${dir}"
if [[ -n $owner ]]; then
ensure_owned_by "$dir" "$owner"
[ -d "${dir}" ] || mkdir -p "${dir}"
if [[ -n $owner_user ]]; then
ensure_owned_by "$dir" "$owner_user" "$owner_group"
fi
}
@@ -39,6 +47,8 @@ ensure_dir_exists() {
is_dir_empty() {
local dir="${1:?missing directory}"
# 计算真实路径,避免符号链接问题
local -r dir="$(realpath "$path")"
if [[ ! -e "$dir" ]] || [[ -z "$(ls -A "$dir")" ]]; then
true
else
@@ -46,23 +56,47 @@ is_dir_empty() {
fi
}
# 检测挂载的路径是否存在或为空
# 参数:
# $1 - 文件或路径
is_mounted_dir_empty() {
local dir="${1:?missing directory}"
if is_dir_empty "$dir" || find "$dir" -mindepth 1 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" -exec false {} +; then
true
else
false
fi
}
# 检测指定的路径当前用户是否可写入
# 参数:
# $1 - 文件或路径
# 返回值:
# true / false
is_writable() {
local file="${1:?missing file}"
local dir
dir="$(dirname "$file")"
if [[ ( -f "$file" && -w "$file" ) || ( ! -f "$file" && -d "$dir" && -w "$dir" ) ]]; then
if [[ (-f "$file" && -w "$file") || (! -f "$file" && -d "$dir" && -w "$dir") ]]; then
true
else
false
fi
}
# 生成相对路径
# 参数:
# $1 - 路径
# $2 - 基准路径
relativize() {
local -r path="${1:?missing path}"
local -r base="${2:?missing base}"
pushd "$base" >/dev/null || exit
realpath -q --no-symlinks --relative-base="$base" "$path" | sed -e 's|^/$|.|' -e 's|^/||'
popd >/dev/null || exit
}
# 循环设置目录中子目录及文件权限
# 参数:
# $1 - paths (as a string).
@@ -78,7 +112,7 @@ configure_permissions_ownership() {
local user=""
local group=""
# Validate arguments
# 参数有效性校验
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
@@ -99,7 +133,7 @@ configure_permissions_ownership() {
group="${1:?missing group}"
;;
*)
LOG_E "Invalid command line flag $1" >&2
error "Invalid command line flag $1" >&2
return 1
;;
esac
@@ -109,27 +143,28 @@ configure_permissions_ownership() {
read -r -a filepaths <<< "$paths"
for p in "${filepaths[@]}"; do
if [[ -e "$p" ]]; then
LOG_D "Check $p"
debug "Check $p"
find -L "$p" -printf ""
if [[ -n ${dir_mode} ]]; then
LOG_D "Change permissions to ${dir_mode} of directories in $p"
find -L "$p" -type d -print | xargs -i chmod "${dir_mode}" '{}'
debug "Change permissions to ${dir_mode} of directories in $p"
find -L "$p" -type d ! -perm "$dir_mode" -print0 | xargs -r -0 chmod "$dir_mode"
fi
if [[ -n ${file_mode} ]]; then
LOG_D "Change permissions to ${file_mode} of files in $p"
find -L "$p" -type f -print | xargs -i chmod "${file_mode}" '{}'
debug "Change permissions to ${file_mode} of files in $p"
find -L "$p" -type f ! -perm "$file_mode" -print0 | xargs -r -0 chmod "$file_mode"
fi
if [[ -n $user ]] && [[ -n ${group} ]]; then
LOG_D "Change ownership to ${user}:${group} of files and directories in $p"
find -L "$p" \( \! -user ${user} -or \! -group ${group} \) -print | xargs -i chown -L "${user}":"${group}" '{}'
debug "Change ownership to ${user}:${group} of files and directories in $p"
find -L "$p" -print0 | xargs -r -0 chown "${user}:${group}"
elif [[ -n $user ]] && [[ -z $group ]]; then
LOG_D "Change user to ${user} of files and directories in $p"
find -L "$p" \! -user ${user} -print | xargs -i chown -L "${user}" '{}'
debug "Change user to ${user} of files and directories in $p"
find -L "$p" -print0 | xargs -r -0 chown "${user}"
elif [[ -z $user ]] && [[ -n $group ]]; then
LOG_D "Change group to ${group} of files and directories in $p"
find -L "$p" \! -group ${group} -print | xargs -i chgrp -L "${group}" '{}'
debug "Change group to ${group} of files and directories in $p"
find -L "$p" -print0 | xargs -r -0 chgrp "${group}"
fi
else
LOG_E "$p does not exist"
error "$p does not exist"
fi
done
}
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 日志输出函数库
#[[ ${ENV_DEBUG:-false} = true ]] && set -x
MODULE="$(basename "$0")"
# 定义颜色代码函数
get_color_code() {
local color="$1"
case "$color" in
"reset") echo '\033[0m' ;;
"red") echo '\033[38;5;1m' ;;
"green") echo '\033[38;5;2m' ;;
"yellow") echo '\033[38;5;3m' ;;
"blue") echo '\033[38;5;4m' ;;
"magenta") echo '\033[38;5;5m' ;;
"cyan") echo '\033[38;5;6m' ;;
*) echo "" ;;
esac
}
# 打印输出到 STDERR 设备
stderr_print() {
printf "%b\\n" "${*}" >&2
}
# 判断是否处于调试模式
is_debug_mode() {
local debug_var="${ENV_DEBUG:-false}"
shopt -s nocasematch
[[ "$debug_var" = 1 || "$debug_var" =~ ^(yes|true)$ ]]
}
# 输出实际日志信息
# 参数:
# $1 - 日志信息
log() {
local debug_info
if is_debug_mode; then
debug_info="$(get_color_code "cyan")${APP_NAME:-}:${MODULE:-}"
else
debug_info="$(get_color_code "cyan")${APP_NAME:-}"
fi
local timestamp="$(get_color_code "magenta")$(date "+%F %T.%3N")$(get_color_code "reset")"
stderr_print "$debug_info $timestamp $*"
}
# 输出调试类日志信息,尽量少使用
# 参数:
# $1 - 日志信息
debug() {
if is_debug_mode; then
log "$(get_color_code "blue")DBG$(get_color_code "reset"): $*"
fi
}
# 输出提示信息类日志信息
# 参数:
# $1 - 日志信息
info() {
log "$(get_color_code "green")INF$(get_color_code "reset"): $*"
}
# 输出警告类日志信息至sterr
# 参数:
# $1 - 日志信息
warn() {
log "$(get_color_code "yellow")WRN$(get_color_code "reset"): $*"
}
# 输出错误类日志信息至sterr,并退出脚本
# 参数:
# $1 - 日志信息
error() {
log "$(get_color_code "red")ERR$(get_color_code "reset"): $*"
}
# 缩进一个字符串
# 参数:
# $1 - 待缩进的字符串 (默认为空字符串)
# $2 - 缩进的字符数 (默认: 4)
# $3 - 缩进使用的字符 (默认: " ")
indent() {
local string="${1:-}"
local num="${2:?missing num}"
local char="${3:-" "}"
# 生成缩进填充的字符串
local indent_unit=""
for ((i = 0; i < num; i++)); do
indent_unit="${indent_unit}${char}"
done
echo "$string" | sed "s/^/${indent_unit}/"
}
@@ -1,10 +1,12 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
# Ver: 1.3 by Endial Fang (endial@126.com)
#
# 文件管理函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
source "/opt/colovu/lib/liblog.sh"
source "/opt/colovu/lib/libos.sh"
source "/opt/colovu/lib/libvalidations.sh"
# 函数列表
@@ -16,7 +18,9 @@
# IP地址
dns_lookup() {
local host="${1:?host is missing}"
getent ahosts "$host" | awk '/STREAM/ {print $1 }'
local ip_version="${2:-}"
getent "ahosts${ip_version}" "$host" | awk '/STREAM/ {print $1 }' | head -n 1
}
# 尝试解析域名并返回对应的 IP
@@ -30,6 +34,7 @@ wait_for_dns_lookup() {
local hostname="${1:?hostname is missing}"
local retries="${2:-5}"
local seconds="${3:-1}"
check_host() {
if [[ $(dns_lookup "$hostname") == "" ]]; then
false
@@ -51,12 +56,17 @@ get_machine_ip() {
hostname="$(hostname)"
read -r -a ip_addresses <<< "$(dns_lookup "$hostname" | xargs echo)"
if [[ "${#ip_addresses[@]}" -gt 1 ]]; then
LOG_W "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}"
warn "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}"
elif [[ "${#ip_addresses[@]}" -lt 1 ]]; then
LOG_E "Could not find any IP address associated to hostname ${hostname}"
error "Could not find any IP address associated to hostname ${hostname}"
exit 1
fi
echo "${ip_addresses[0]}"
# Check if the first IP address is IPv6 to add brackets
if validate_ipv6 "${ip_addresses[0]}" ; then
echo "[${ip_addresses[0]}]"
else
echo "${ip_addresses[0]}"
fi
}
# 检测指定的主机名是否可解析
@@ -66,6 +76,7 @@ get_machine_ip() {
# true / false
is_hostname_resolved() {
local -r host="${1:?missing value}"
if [[ -n "$(dns_lookup "$host")" ]]; then
true
else
@@ -125,3 +136,20 @@ parse_uri() {
esac
[[ "$uri" =~ $URI_REGEX ]] && echo "${BASH_REMATCH[${index}]}"
}
# Wait for a HTTP connection to succeed
# 等待 HTTP 连接成功
# 参数:
# $1 - URL 地址
# $2 - 最大重试次数 (可选)
# $3 - 重试间隔时间 (可选)
wait_for_http_connection() {
local url="${1:?missing url}"
local retries="${2:-}"
local sleep_time="${3:-}"
if ! retry_while "debug_execute curl --silent ${url}" "$retries" "$sleep_time"; then
error "Could not connect to ${url}"
return 1
fi
}
+511
View File
@@ -0,0 +1,511 @@
#!/bin/bash
# Ver: 1.4 by Endial Fang (endial@126.com)
#
# 操作系统控制函数库
# 加载依赖项
source "/opt/colovu/lib/liblog.sh"
source "/opt/colovu/lib/libfs.sh"
source "/opt/colovu/lib/libvalidations.sh"
# 函数列表
# 检测指定用户账户是否存在
# 参数:
# $1 - 用户账户
# 返回值:
# 0 / 1
is_user_exists() {
local user="${1:?user is missing}"
id "$user" >/dev/null 2>&1
}
# 检测指定用户分组是否存在
# 参数:
# $1 - 用户组
# 返回值:
# 0 / 1
is_group_exists() {
local group="${1:?group is missing}"
getent group "$group" >/dev/null 2>&1
}
# 检测当前是否为 root 用户
# 返回值:
# true / false
is_root() {
if [[ "$(id -u)" = "0" ]]; then
debug "Run as root."
true
else
debug "Run as non-root: $(id -u)"
false
fi
}
# 确保指定用户组在系统中存在
# 参数:
# $1 - 用户组
# 标志位:
# -i|--gid - 用户组 ID
# -s|--system - 创建系统用户 (uid <= 999)
ensure_group_exists() {
local group="${1:?group is missing}"
local gid=""
local is_system_user=false
# 检测标志位
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-i | --gid)
shift
gid="${1:?missing gid}"
;;
-s | --system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! group_exists "$group"; then
local -a args=("$group")
if [[ -n "$gid" ]]; then
if group_exists "$gid"; then
error "The GID $gid is already in use." >&2
return 1
fi
args+=("--gid" "$gid")
fi
$is_system_user && args+=("--system")
groupadd "${args[@]}" >/dev/null 2>&1
fi
}
# 确保指定用户在系统中存在
# 参数:
# $1 - 用户
# 标志位:
# -i|--uid - 用户 ID
# -a|--append-groups - 附加用户组 (逗号分隔)
# -g|--group - 用户组
# -h|--home - 用户家目录
# -s|--system - 创建系统用户 (uid <= 999)
ensure_user_exists() {
local user="${1:?user is missing}"
local uid=""
local group=""
local append_groups=""
local home=""
local is_system_user=false
# Validate arguments
shift 1
while [ "$#" -gt 0 ]; do
case "$1" in
-i | --uid)
shift
uid="${1:?missing uid}"
;;
-g | --group)
shift
group="${1:?missing group}"
;;
-a | --append-groups)
shift
append_groups="${1:?missing append_groups}"
;;
-h | --home)
shift
home="${1:?missing home directory}"
;;
-s | --system)
is_system_user=true
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if ! user_exists "$user"; then
local -a user_args=("-N" "$user")
if [[ -n "$uid" ]]; then
if user_exists "$uid"; then
error "The UID $uid is already in use."
return 1
fi
user_args+=("--uid" "$uid")
else
$is_system_user && user_args+=("--system")
fi
useradd "${user_args[@]}" >/dev/null 2>&1
fi
if [[ -n "$group" ]]; then
local -a group_args=("$group")
$is_system_user && group_args+=("--system")
ensure_group_exists "${group_args[@]}"
usermod -g "$group" "$user" >/dev/null 2>&1
fi
if [[ -n "$append_groups" ]]; then
local -a groups
read -ra groups <<<"$(tr ',;' ' ' <<<"$append_groups")"
for group in "${groups[@]}"; do
ensure_group_exists "$group"
usermod -aG "$group" "$user" >/dev/null 2>&1
done
fi
if [[ -n "$home" ]]; then
mkdir -p "$home"
usermod -d "$home" "$user" >/dev/null 2>&1
configure_permissions_ownership "$home" -d "775" -f "664" -u "$user" -g "$group"
fi
}
# 获取系统可用内存大小(MB)信息
# 返回值:
# 内存大小(兆字节)
get_total_memory() {
echo $(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024))
}
# 获取以内存定量方式描述的机器类型
# 标志位:
# --memory - 内存大小 (MB,可选)
# 返回值:
# 类型名称
get_machine_size() {
local memory=""
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
--memory)
shift
memory="${1:?missing memory}"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
if [[ -z "$memory" ]]; then
debug "Memory was not specified, detecting available memory automatically"
memory="$(get_total_memory)"
fi
sanitized_memory=$(convert_to_mb "$memory")
if [[ "$sanitized_memory" -gt 26000 ]]; then
echo 2xlarge
elif [[ "$sanitized_memory" -gt 13000 ]]; then
echo xlarge
elif [[ "$sanitized_memory" -gt 6000 ]]; then
echo large
elif [[ "$sanitized_memory" -gt 3000 ]]; then
echo medium
elif [[ "$sanitized_memory" -gt 1500 ]]; then
echo small
else
echo micro
fi
}
# 获取已定义的所有内存大小描述
# 返回值:
# 描述值列表
get_supported_machine_sizes() {
echo micro small medium large xlarge 2xlarge
}
# 将以字符串表示的内存大小转换为以MB为单位的内存大小值 (i.e. 2G -> 2048)
# 参数:
# $1 - 内存大小
# 返回值:
# 转换后的数值
convert_to_mb() {
local amount="${1:-}"
if [[ $amount =~ ^([0-9]+)(m|M|g|G) ]]; then
size="${BASH_REMATCH[1]}"
unit="${BASH_REMATCH[2]}"
if [[ "$unit" = "g" || "$unit" = "G" ]]; then
amount="$((size * 1024))"
else
amount="$size"
fi
fi
echo "$amount"
}
# 如果禁用调试模式,将输出信息重定向至 /dev/null
# 参数:
# $@ - 待执行的命令
debug_execute() {
if is_boolean_yes "${ENV_DEBUG:-false}"; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
# 重试执行命令
# 参数:
# $1 - 命令 (字符串)
# $2 - 最大尝试次数. 默认值: 12
# $3 - 重试前等待时间(秒). 默认值: 5
# 返回值:
# 0 / 1
retry_while() {
local cmd="${1:?cmd is missing}"
local retries="${2:-12}"
local sleep_time="${3:-5}"
local return_value=1
read -r -a command <<<"$cmd"
for ((i = 1; i <= retries; i += 1)); do
"${command[@]}" && return_value=0 && break
sleep "$sleep_time"
done
return $return_value
}
# 生成随机字符串
# 标志位:
# -t|--type - 字符串类型 (ascii, alphanumeric, numeric). 默认值: ascii
# -c|--count - 字符串长度. 默认值: 32
# 返回值:
# 字符串
generate_random_string() {
local type="ascii"
local count="32"
local filter
local result
# 检测标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
-t | --type)
shift
type="$1"
;;
-c | --count)
shift
count="$1"
;;
*)
echo "Invalid command line flag $1" >&2
return 1
;;
esac
shift
done
# 检测类型
case "$type" in
ascii)
filter="[:print:]"
;;
numeric)
filter="0-9"
;;
alphanumeric)
filter="a-zA-Z0-9"
;;
alphanumeric+special|special+alphanumeric)
# Limit variety of special characters, so there is a higher chance of containing more alphanumeric characters
# Special characters are harder to write, and it could impact the overall UX if most passwords are too complex
filter='a-zA-Z0-9:@.,/+!='
;;
*)
echo "Invalid type ${type}" >&2
return 1
;;
esac
# Obtain count + 10 lines from /dev/urandom to ensure that the resulting string has the expected size
# Note there is a very small chance of strings starting with EOL character
# Therefore, the higher amount of lines read, this will happen less frequently
result="$(head -n "$((count + 10))" /dev/urandom | tr -dc "$filter" | head -c "$count")"
echo "$result"
}
# 为指定字符串生成 MD5 值
# 参数:
# $1 - 字符串
# 返回值:
# 字符串对应的 MD5
generate_md5_hash() {
local -r str="${1:?missing input string}"
echo -n "$str" | md5sum | awk '{print $1}'
}
# 为指定字符串生成 SHA1 值
# 参数:
# $1 - 字符串
# $2 - 算法 - 1 (default), 224, 256, 384, 512
# 返回值:
# 字符串对应的 SHA1
generate_sha_hash() {
local -r str="${1:?missing input string}"
local -r algorithm="${2:-1}"
echo -n "$str" | "sha${algorithm}sum" | awk '{print $1}'
}
# 将一个字符串转换为其十六进制表示形式
# 参数:
# $1 - 字符串
# 返回值:
# 十六进制表示形式
convert_to_hex() {
local -r str=${1:?missing input string}
local -i iterator
local char
for ((iterator = 0; iterator < ${#str}; iterator++)); do
char=${str:iterator:1}
printf '%x' "'${char}"
done
}
# 获取启动时间
# 返回值:
# 启动时间
get_boot_time() {
stat /proc --format=%Y
}
# 获取机器 ID
# 返回值:
# 机器 ID
get_machine_id() {
local machine_id
if [[ -f /etc/machine-id ]]; then
machine_id="$(cat /etc/machine-id)"
fi
if [[ -z "$machine_id" ]]; then
# Fallback to the boot-time, which will at least ensure a unique ID in the current session
machine_id="$(get_boot_time)"
fi
echo "$machine_id"
}
# 获取根分区的磁盘设备 ID(如: /dev/sda1
# 返回值:
# 根分区磁盘设备 ID
get_disk_device_id() {
local device_id=""
if grep -q ^/dev /proc/mounts; then
device_id="$(grep ^/dev /proc/mounts | awk '$2 == "/" { print $1 }' | tail -1)"
fi
# If it could not be autodetected, fallback to /dev/sda1 as a default
if [[ -z "$device_id" || ! -b "$device_id" ]]; then
device_id="/dev/sda1"
fi
echo "$device_id"
}
# 获取根磁盘设备 ID(如: /dev/sda
# 返回值:
# 根磁盘设备 ID
get_root_disk_device_id() {
get_disk_device_id | sed -E 's/p?[0-9]+$//'
}
# 获取根磁盘大小
# 返回值:
# 根磁盘大小(字节)
get_root_disk_size() {
fdisk -l "$(get_root_disk_device_id)" | grep 'Disk.*bytes' | sed -E 's/.*, ([0-9]+) bytes,.*/\1/' || true
}
# 以指定用户身份运行命令
# 注意:此函数将使用 chroot 命令来模拟指定用户身份运行命令,
# 参数:
# $1 - 用户(可选)
# $2..$n - 命令及命令参数
run_as_user() {
run_chroot "$@"
}
# 以指定用户身份执行命令
# 注意:此命令将替换当前进程映像,
# 参数:
# $1 - 用户(可选)
# $2..$n - 命令及命令参数
exec_as_user() {
run_chroot --replace-process "$@"
}
# 使用 chroot 命令以指定用户身份运行命令
# 参数:
# $1 - 用户(可选)
# $2..$n - 命令及命令参数
# 标志:
# -r | --replace-process - 替换当前进程映像
run_chroot() {
local userspec
local user
local homedir
local replace=false
local -r cwd="$(pwd)"
# 解析并验证标志位
while [[ "$#" -gt 0 ]]; do
case "$1" in
-r | --replace-process)
replace=true
;;
--)
shift
break
;;
-*)
stderr_print "unrecognized flag $1"
return 1
;;
*)
break
;;
esac
shift
done
# 解析并验证参数
if [[ "$#" -lt 2 ]]; then
echo "expected at least 2 arguments"
return 1
else
userspec=$1
shift
# 用户名可以包含组,因此我们解析用户
user=$(echo "$userspec" | cut -d':' -f1)
fi
if ! is_root; then
error "Could not switch to '${userspec}': Operation not permitted"
return 1
fi
# 获取用户的家目录,因为 chroot 不会正确更新此环境变量,并且一些脚本依赖于它
homedir=$(eval echo "~${user}")
if [[ ! -d $homedir ]]; then
homedir="${HOME:-/}"
fi
# 取得 "$@" 的值,以便正确支持 shell 参数扩展
if [[ "$replace" = true ]]; then
exec chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@"
else
chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@"
fi
}
+449
View File
@@ -0,0 +1,449 @@
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 服务管理函数库
# shellcheck disable=SC1091
# 加载依赖项
source "/opt/colovu/lib/liblog.sh"
source "/opt/colovu/lib/libvalidations.sh"
# 函数列表
# 获取并返回服务 PID
# 参数:
# $1 - PID 文件
# 返回值:
# PID
get_pid_from_file() {
local pid_file="${1:?pid file is missing}"
if [[ -f "$pid_file" ]]; then
if [[ -n "$(< "$pid_file")" ]] && [[ "$(< "$pid_file")" -gt 0 ]]; then
echo "$(< "$pid_file")"
fi
fi
}
# 检测 PID 对应的服务是否在运行中
# 参数:
# $1 - PID
# 返回值:
# 0 / 1
is_service_running() {
local pid="${1:?pid is missing}"
kill -0 "$pid" 2>/dev/null
}
# 通过发送信号停止一个指定 PID 的服务
# 参数:
# $1 - PID 文件
# $2 - 信号 (可选)
stop_service_using_pid() {
local pid_file="${1:?pid file is missing}"
local signal="${2:-}"
local pid
pid="$(get_pid_from_file "$pid_file")"
[[ -z "$pid" ]] || ! is_service_running "$pid" && return
if [[ -n "$signal" ]]; then
kill "-${signal}" "$pid"
else
kill "$pid"
fi
local counter=10
while [[ "$counter" -ne 0 ]] && is_service_running "$pid"; do
sleep 1
counter=$((counter - 1))
done
}
# 启动一个 cron 守护进程
# 返回值:
# true / false
cron_start() {
if [[ -x "/usr/sbin/cron" ]]; then
/usr/sbin/cron
elif [[ -x "/usr/sbin/crond" ]]; then
/usr/sbin/crond
else
false
fi
}
# 为指定的服务生成 cron 配置文件
# 参数:
# $1 - 服务名称
# $2 - 命令
# 标志位:
# --run-as - 运行的用户. 默认值: root
# --schedule - Cron 周期配置. 默认值: * * * * *
generate_cron_conf() {
local service_name="${1:?service name is missing}"
local cmd="${2:?command is missing}"
local run_as="root"
local schedule="* * * * *"
local clean="true"
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--run-as)
shift
run_as="$1"
;;
--schedule)
shift
schedule="$1"
;;
--no-clean)
clean="false"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p /etc/cron.d
if "$clean"; then
cat > "/etc/cron.d/${service_name}" <<EOF
${schedule} ${run_as} ${cmd}
EOF
else
echo "${schedule} ${run_as} ${cmd}" >> /etc/cron.d/"$service_name"
fi
}
# 删除指定服务的 cron 配置文件
# 参数:
# $1 - 服务名称
remove_cron_conf() {
local service_name="${1:?service name is missing}"
local cron_conf_dir="/etc/monit/conf.d"
rm -f "${cron_conf_dir}/${service_name}"
}
# 为指定的服务生成 monit 配置文件
# 参数:
# $1 - 服务名
# $2 - PID 文件
# $3 - 启动命令
# $4 - 停止命令
# 标志位:
# --disabled - 是否禁用. 默认值: no
generate_monit_conf() {
local service_name="${1:?service name is missing}"
local pid_file="${2:?pid file is missing}"
local start_command="${3:?start command is missing}"
local stop_command="${4:?stop command is missing}"
local monit_conf_dir="/etc/monit/conf.d"
local disabled="no"
# 检测标志位
shift 4
while [[ "$#" -gt 0 ]]; do
case "$1" in
--disable)
disabled="yes"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
is_boolean_yes "$disabled" && conf_suffix=".disabled"
mkdir -p "$monit_conf_dir"
cat > "${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" <<EOF
check process ${service_name}
with pidfile "${pid_file}"
start program = "${start_command}" with timeout 90 seconds
stop program = "${stop_command}" with timeout 90 seconds
EOF
}
# 删除指定服务的 monit 配置文件
# 参数:
# $1 - 服务名称
remove_monit_conf() {
local service_name="${1:?service name is missing}"
local monit_conf_dir="/etc/monit/conf.d"
rm -f "${monit_conf_dir}/${service_name}.conf"
}
# 为指定的服务生成 Logrotate 配置文件
# 参数:
# $1 - 应用名称
# $2 - 日志路径
# 标志位:
# --period - 周期
# --rotations - Rotations 存储的数量
# --extra - 扩展参数 (可选)
generate_logrotate_conf() {
local service_name="${1:?service name is missing}"
local log_path="${2:?log path is missing}"
local period="weekly"
local rotations="150"
local extra=""
local logrotate_conf_dir="/etc/logrotate.d"
local var_name
# 检测标志位
shift 2
while [[ "$#" -gt 0 ]]; do
case "$1" in
--period|--rotations|--extra)
var_name="$(echo "$1" | sed -e "s/^--//" -e "s/-/_/g")"
shift
declare "$var_name"="${1:?"$var_name" is missing}"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
mkdir -p "$logrotate_conf_dir"
cat <<EOF | sed '/^\s*$/d' > "${logrotate_conf_dir}/${service_name}"
${log_path} {
${period}
rotate ${rotations}
dateext
compress
copytruncate
missingok
$(indent "$extra" 2)
}
EOF
}
# 删除指定服务的 Logrotate 配置文件
# 参数:
# $1 - 服务名称
remove_logrotate_conf() {
local service_name="${1:?service name is missing}"
local logrotate_conf_dir="/etc/logrotate.d"
rm -f "${logrotate_conf_dir}/${service_name}"
}
# 为指定的服务生成 Systemd 配置文件
# 参数:
# $1 - 服务名称
# 标志位:
# --custom-service-content - 自定义内容
# --environment - 环境变量(可选)
# --environment-file - 环境变量文件(可选)
# --exec-start - 启动命令(必须)
# --exec-start-pre - 启动前命令(可选)
# --exec-start-post - 启动后命令(可选)
# --exec-stop - 停止命令(可选)
# --exec-reload - 重载命令(可选)
# --group - 系统组
# --name - 服务名称(默认为 $1)
# --restart - 重启策略
# --pid-file - PID 文件
# --standard-output - 标准输出文件
# --standard-error - 标准错误文件
# --success-exit-status - 成功退出状态码
# --type - 服务类型(默认为 Fork)
# --user - 系统用户
# --working-directory - 工作目录
generate_systemd_conf() {
local -r service_name="${1:?service name is missing}"
local -r systemd_units_dir="/etc/systemd/system"
local -r service_file="${systemd_units_dir}/bitnami.${service_name}.service"
# Default values
local name="$service_name"
local type="forking"
local user=""
local group=""
local environment=""
local environment_file=""
local exec_start=""
local exec_start_pre=""
local exec_start_post=""
local exec_stop=""
local exec_reload=""
local restart="always"
local pid_file=""
local standard_output="journal"
local standard_error=""
local limits_content=""
local success_exit_status=""
local custom_service_content=""
local working_directory=""
# Parse CLI flags
shift
while [[ "$#" -gt 0 ]]; do
case "$1" in
--name \
| --type \
| --user \
| --group \
| --exec-start \
| --exec-stop \
| --exec-reload \
| --restart \
| --pid-file \
| --standard-output \
| --standard-error \
| --success-exit-status \
| --custom-service-content \
| --working-directory \
)
var_name="$(echo "$1" | sed -e "s/^--//" -e "s/-/_/g")"
shift
declare "$var_name"="${1:?"${var_name} value is missing"}"
;;
--limit-*)
[[ -n "$limits_content" ]] && limits_content+=$'\n'
var_name="${1//--limit-}"
shift
limits_content+="Limit${var_name^^}=${1:?"--limit-${var_name} value is missing"}"
;;
--exec-start-pre)
shift
[[ -n "$exec_start_pre" ]] && exec_start_pre+=$'\n'
exec_start_pre+="ExecStartPre=${1:?"--exec-start-pre value is missing"}"
;;
--exec-start-post)
shift
[[ -n "$exec_start_post" ]] && exec_start_post+=$'\n'
exec_start_post+="ExecStartPost=${1:?"--exec-start-post value is missing"}"
;;
--environment)
shift
# It is possible to add multiple environment lines
[[ -n "$environment" ]] && environment+=$'\n'
environment+="Environment=${1:?"--environment value is missing"}"
;;
--environment-file)
shift
# It is possible to add multiple environment-file lines
[[ -n "$environment_file" ]] && environment_file+=$'\n'
environment_file+="EnvironmentFile=${1:?"--environment-file value is missing"}"
;;
*)
echo "Invalid command line flag ${1}" >&2
return 1
;;
esac
shift
done
# Validate inputs
local error="no"
if [[ -z "$exec_start" ]]; then
error "The --exec-start option is required"
error="yes"
fi
if [[ "$error" != "no" ]]; then
return 1
fi
# Generate the Systemd unit
cat > "$service_file" <<EOF
# Copyright Broadcom, Inc. All Rights Reserved.
# SPDX-License-Identifier: APACHE-2.0
[Unit]
Description=Bitnami service for ${name}
# Starting/stopping the main bitnami service should cause the same effect for this service
PartOf=bitnami.service
[Service]
Type=${type}
EOF
if [[ -n "$working_directory" ]]; then
cat >> "$service_file" <<< "WorkingDirectory=${working_directory}"
fi
if [[ -n "$exec_start_pre" ]]; then
# This variable may contain multiple ExecStartPre= directives
cat >> "$service_file" <<< "$exec_start_pre"
fi
if [[ -n "$exec_start" ]]; then
cat >> "$service_file" <<< "ExecStart=${exec_start}"
fi
if [[ -n "$exec_start_post" ]]; then
# This variable may contain multiple ExecStartPost= directives
cat >> "$service_file" <<< "$exec_start_post"
fi
# Optional stop and reload commands
if [[ -n "$exec_stop" ]]; then
cat >> "$service_file" <<< "ExecStop=${exec_stop}"
fi
if [[ -n "$exec_reload" ]]; then
cat >> "$service_file" <<< "ExecReload=${exec_reload}"
fi
# User and group
if [[ -n "$user" ]]; then
cat >> "$service_file" <<< "User=${user}"
fi
if [[ -n "$group" ]]; then
cat >> "$service_file" <<< "Group=${group}"
fi
# PID file allows to determine if the main process is running properly (for Restart=always)
if [[ -n "$pid_file" ]]; then
cat >> "$service_file" <<< "PIDFile=${pid_file}"
fi
if [[ -n "$restart" ]]; then
cat >> "$service_file" <<< "Restart=${restart}"
fi
# Environment flags
if [[ -n "$environment" ]]; then
# This variable may contain multiple Environment= directives
cat >> "$service_file" <<< "$environment"
fi
if [[ -n "$environment_file" ]]; then
# This variable may contain multiple EnvironmentFile= directives
cat >> "$service_file" <<< "$environment_file"
fi
# Logging
if [[ -n "$standard_output" ]]; then
cat >> "$service_file" <<< "StandardOutput=${standard_output}"
fi
if [[ -n "$standard_error" ]]; then
cat >> "$service_file" <<< "StandardError=${standard_error}"
fi
if [[ -n "$custom_service_content" ]]; then
# This variable may contain multiple miscellaneous directives
cat >> "$service_file" <<< "$custom_service_content"
fi
if [[ -n "$success_exit_status" ]]; then
cat >> "$service_file" <<EOF
# When the process receives a SIGTERM signal, it exits with code ${success_exit_status}
SuccessExitStatus=${success_exit_status}
EOF
fi
cat >> "$service_file" <<EOF
# Optimizations
TimeoutStartSec=2min
TimeoutStopSec=30s
IgnoreSIGPIPE=no
KillMode=mixed
EOF
if [[ -n "$limits_content" ]]; then
cat >> "$service_file" <<EOF
# Limits
${limits_content}
EOF
fi
cat >> "$service_file" <<EOF
[Install]
# Enabling/disabling the main bitnami service should cause the same effect for this service
WantedBy=bitnami.service
EOF
}
@@ -1,10 +1,10 @@
#!/bin/bash
# Ver: 1.1 by Endial Fang (endial@126.com)
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 数据有效性校验函数库
# 加载依赖项
. /colovu/lib/liblog.sh # 日志输出函数库
source "/opt/colovu/lib/liblog.sh"
# 函数列表
@@ -15,6 +15,7 @@
# true / false
is_int() {
local -r int="${1:?missing value}"
if [[ "$int" =~ ^-?[0-9]+ ]]; then
true
else
@@ -29,6 +30,7 @@ is_int() {
# true / false
is_positive_int() {
local -r int="${1:?missing value}"
if is_int "$int" && (( "${int}" >= 0 )); then
true
else
@@ -43,7 +45,8 @@ is_positive_int() {
# true / false
is_boolean_yes() {
local -r bool="${1:-}"
# comparison is performed without regard to the case of alphabetic characters
# 忽略字母大小写
shopt -s nocasematch
if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then
true
@@ -59,6 +62,7 @@ is_boolean_yes() {
# true / false
is_yes_no_value() {
local -r bool="${1:-}"
if [[ "$bool" =~ ^(yes|no)$ ]]; then
true
else
@@ -80,6 +84,32 @@ is_true_false_value() {
fi
}
# 检测数据是否为布尔值 'true/false'
# 参数:
# $1 - 待检测的数据
is_true_false_value() {
local -r bool="${1:-}"
if [[ "$bool" =~ ^(true|false)$ ]]; then
true
else
false
fi
}
# 检测提供的参数是否为布尔值 1/0
# 参数:
# $1 - 待检测的数据
is_1_0_value() {
local -r bool="${1:-}"
if [[ "$bool" =~ ^[10]$ ]]; then
true
else
false
fi
}
# 检测提供的参数是否为空字符串或未定义
# 参数:
# $1 - 待检测的数据
@@ -87,6 +117,7 @@ is_true_false_value() {
# true / false
is_empty_value() {
local -r val="${1:-}"
if [[ -z "$val" ]]; then
true
else
@@ -116,7 +147,7 @@ validate_port() {
break
;;
-*)
LOG_E "unrecognized flag $1"
error "unrecognized flag $1"
return 1
;;
*)
@@ -127,35 +158,50 @@ validate_port() {
done
if [[ "$#" -gt 1 ]]; then
LOG_E "too many arguments provided"
error "too many arguments provided"
return 2
elif [[ "$#" -eq 0 ]]; then
LOG_E "missing port argument"
error "missing port argument"
return 1
else
value=$1
fi
if [[ -z "$value" ]]; then
LOG_E "the value is empty"
error "the value is empty"
return 1
else
if ! is_int "$value"; then
LOG_W "value is not an integer"
warn "value is not an integer"
return 2
elif [[ "$value" -lt 0 ]]; then
LOG_W "negative value provided"
warn "negative value provided"
return 2
elif [[ "$value" -gt 65535 ]]; then
LOG_W "requested port is greater than 65535"
warn "requested port is greater than 65535"
return 2
elif [[ "$unprivileged" = 1 && "$value" -lt 1024 ]]; then
LOG_W "privileged port requested"
warn "privileged port requested"
return 3
fi
fi
}
# 检测数据是否为有效的IPv6地址
# 参数:
# $1 - 待检测的数据
validate_ipv6() {
local ip="${1:?ip is missing}"
local stat=1
local full_address_regex='^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$'
local short_address_regex='^((([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}){0,6}::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}){0,6})$'
if [[ $ip =~ $full_address_regex || $ip =~ $short_address_regex || $ip == "::" ]]; then
stat=0
fi
return $stat
}
# 检测数据是否为有效的IPv4地址
# 参数:
# $1 - 待检测的数据
@@ -166,7 +212,7 @@ validate_ipv4() {
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
read -r -a ip_array <<< "$(tr '.' ' ' <<< "$ip")"
read -r -a ip_array <<< "$(tr '.' ' ' <<< "$ip")"
[[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 \
&& ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]]
stat=$?
@@ -174,6 +220,21 @@ validate_ipv4() {
return $stat
}
# 检测数据是否为有效的IP地址
# 参数:
# $1 - 待检测的数据
validate_ip() {
local ip="${1:?ip is missing}"
local stat=1
if validate_ipv4 "$ip"; then
stat=0
else
stat=$(validate_ipv6 "$ip")
fi
return $stat
}
# 校验字符串格式
# 标志位:
# -min-length - 最小长度
@@ -203,32 +264,22 @@ validate_string() {
break
;;
-*)
LOG_E "unrecognized flag $1"
error "unrecognized flag $1"
return 1
;;
*)
break
string="$1"
;;
esac
shift
done
if [ "$#" -gt 1 ]; then
LOG_E "too many arguments provided"
return 2
elif [ "$#" -eq 0 ]; then
LOG_W "missing string"
return 1
else
string=$1
fi
if [[ "$min_length" -ge 0 ]] && [[ "${#string}" -lt "$min_length" ]]; then
LOG_I "string length is less than $min_length"
info "string length is less than $min_length"
return 1
fi
if [[ "$max_length" -ge 0 ]] && [[ "${#string}" -gt "$max_length" ]]; then
LOG_I "string length is great than $max_length"
info "string length is great than $max_length"
return 1
fi
}
}
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Ver: 1.0 by Endial Fang (endial@126.com)
#
# 版本管理函数库
# 加载依赖项
source "/opt/colovu/lib/liblog.sh"
# 函数列表
# 获取语义化版本
# 参数:
# $1 - 版本字符串
# $2 - 版本部分,1 为 major2 为 minor3 为 patch
# 返回值:
# 版本数组,包含 majorminor 和 patch
get_sematic_version () {
local version="${1:?version is required}"
local section="${2:?section is required}"
local -a version_sections
# 用于解析版本的正则表达式:x.y.z
local -r regex='([0-9]+)(\.([0-9]+)(\.([0-9]+))?)?'
if [[ "$version" =~ $regex ]]; then
local i=1
local j=1
local n=${#BASH_REMATCH[*]}
while [[ $i -lt $n ]]; do
if [[ -n "${BASH_REMATCH[$i]}" ]] && [[ "${BASH_REMATCH[$i]:0:1}" != '.' ]]; then
version_sections[j]="${BASH_REMATCH[$i]}"
((j++))
fi
((i++))
done
local number_regex='^[0-9]+$'
if [[ "$section" =~ $number_regex ]] && (( section > 0 )) && (( section <= 3 )); then
echo "${version_sections[$section]}"
return
else
error "Section allowed values are: 1, 2, and 3"
return 1
fi
fi
}
+87 -67
View File
@@ -1,9 +1,17 @@
#!/bin/sh
# Ver: 1.1 by Endial Fang (endial@126.com)
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
# 定义错误处理函数,添加错误位置信息
error() {
local error_location="$1"
local error_message="$2"
echo "Error at $error_location: $error_message" >&2
exit 1
}
print_usage() {
echo "Usage: download_pkg <COMMAND> <PACKAGE-NAME> \"<URLS>\" [OPTIONS]"
echo ""
@@ -35,41 +43,44 @@ check_pgp() {
local name_asc=${1:?missing asc file name}
local name=${2:?missing file name}
local keys="${3:?missing key id}"
GNUPGHOME="$(mktemp -d)"
if which gpg >/dev/null 2>&1; then
for key in $keys; do
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "${key}" ||
gpg --batch --keyserver pgp.mit.edu --recv-keys "${key}" ||
gpg --batch --keyserver keys.gnupg.net --recv-keys "${key}" ||
gpg --batch --keyserver keyserver.pgp.com --recv-keys "${key}";
done
gpg --batch --verify "$name_asc" "$name"
command -v gpgconf > /dev/null && gpgconf --kill all
fi
if which gpg >/dev/null 2>&1; then
local key_servers=("pgp.mit.edu" "keys.gnupg.net" "keyserver.pgp.com" "ha.pool.sks-keyservers.net")
for key in $keys; do
for server in "${key_servers[@]}"; do
if gpg --batch --keyserver "$server" --recv-keys "${key}" --timeout 10; then
break
fi
done
done
gpg --batch --verify "$name_asc" "$name" || error "PGP verification" "PGP verification failed"
command -v gpgconf > /dev/null && gpgconf --kill all
fi
}
# 获取并解析参数
ARGS=$(getopt -o g:s:h -l "checkpgp:,checksum:,help" -n "download-pkg" -- "$@")
if [ $? -ne 0 ];
then
exit 1
parsed_args=$(getopt -o g:s:h -l "checkpgp:,checksum:,help" -n "download-pkg" -- "$@")
if [ $? -ne 0 ]; then
error "Parameter parsing" "Failed to parse command line arguments."
print_usage
exit 1
fi
eval set -- "$ARGS";
eval set -- "$parsed_args";
while true; do
case "$1" in
-g|--checkpgp)
shift
if [ -n "$1" ]; then
PACKAGE_KEYS=$1
package_keys=$1
shift
fi
;;
-s|--checksum)
shift
if [ -n "$1" ]; then
PACKAGE_SHA256=$1
package_sha256=$1
shift
fi
;;
@@ -88,7 +99,7 @@ done
case "$1" in
download|install|unpack) ;;
*)
error "Unrecognized command: $1"
error "Command validation" "Unrecognized command: $1"
print_usage
exit 1
;;
@@ -96,40 +107,50 @@ esac
# 检测输入参数是否足够,需要至少提供软件包名称 及 下载路径
if [ $# -lt 3 ]; then
error "Parameter validation" "Insufficient parameters. Please provide package name and URLs."
print_usage
exit 1
fi
INSTALL_ROOT=/usr/local
CACHE_ROOT=/tmp
install_root=/usr/local
cache_root=/tmp
PACKAGE="$2"
PACKAGE_URLS=$3
package="$2"
package_urls=$3
cd $INSTALL_ROOT
echo "Downloading $PACKAGE package"
for url in $PACKAGE_URLS; do
echo "Try $url/$PACKAGE"
if wget -O "$CACHE_ROOT/$PACKAGE" "$url/$PACKAGE" && [ -s "$CACHE_ROOT/$PACKAGE" ]; then
if [ -n "${PACKAGE_KEYS:-}" ]; then
wget -O "$CACHE_ROOT/$PACKAGE.asc" "$url/$PACKAGE.asc" || wget -O "$CACHE_ROOT/$PACKAGE.asc" "$url/$PACKAGE.sign" || :
if [ ! -e "$CACHE_ROOT/$PACKAGE.asc" ]; then
exit 1
# 检查缓存目录中是否已存在该软件包
if [ -e "$cache_root/$package" ]; then
echo "Package already exists in cache: $cache_root/$package"
else
cd $install_root
echo "Downloading $package package"
for url in $package_urls; do
echo "Try $url/$package"
if wget -T 10 -O "$cache_root/$package" "$url/$package" && [ -s "$cache_root/$package" ]; then
if [ -n "${package_keys:-}" ]; then
wget -T 10 -O "$cache_root/$package.asc" "$url/$package.asc" || wget -T 10 -O "$cache_root/$package.asc" "$url/$package.sign" || :
if [ ! -e "$cache_root/$package.asc" ]; then
error "PGP signature download" "Failed to download PGP signature file."
exit 1
fi
fi
break
else
echo "Failed to download from $url/$package"
fi
fi
break
fi
done
if [ -n "${PACKAGE_SHA256:-}" ]; then
echo "Verifying package integrity"
echo "$PACKAGE_SHA256 *$CACHE_ROOT/$PACKAGE" | sha256sum -c -
done
fi
if [ -e "$CACHE_ROOT/$PACKAGE.asc" ]; then
echo "Verifying package with PGP"
check_pgp "$CACHE_ROOT/$PACKAGE.asc" "$CACHE_ROOT/$PACKAGE" "$PACKAGE_KEYS"
if [ -n "${package_sha256:-}" ]; then
echo "Verifying package integrity"
if ! echo "$package_sha256 *$cache_root/$package" | sha256sum -c -; then
error "SHA256 verification" "SHA256 verification failed"
fi
fi
if [ -e "$cache_root/$package.asc" ]; then
echo "Verifying package with PGP"
check_pgp "$cache_root/$package.asc" "$cache_root/$package" "$package_keys"
fi
# If the tarball has too many files, it can trigger a bug
@@ -137,27 +158,26 @@ fi
# to workaround it. As the overhead is too big (~40 MB), it is not added by
# default. Source: https://github.com/coreos/bugs/issues/1095
# 安装或解压软件
case "$1" in
download)
echo "Download success: $CACHE_ROOT/$PACKAGE"
;;
install)
echo "Installing $PACKAGE"
cp $CACHE_ROOT/$PACKAGE /usr/local/sbin/
;;
unpack)
if ! tar -taf $CACHE_ROOT/$PACKAGE >/dev/null 2>&1; then
echo "Invalid or corrupt '$PACKAGE' package."
exit 1
fi
echo "Unpacking $PACKAGE to $CACHE_ROOT"
cd $CACHE_ROOT
if which bsdtar >/dev/null 2>&1; then
bsdtar -xf $CACHE_ROOT/$PACKAGE
else
tar --no-same-owner -xaf $CACHE_ROOT/$PACKAGE
fi
;;
download)
echo "Download success: $cache_root/$package"
;;
install)
echo "Installing $package"
cp $cache_root/$package /usr/local/sbin/
;;
unpack)
if ! tar -taf $cache_root/$package >/dev/null 2>&1; then
error "Package integrity check" "Invalid or corrupt '$package' package."
exit 1
fi
echo "Unpacking $package to $cache_root"
cd $cache_root
if which bsdtar >/dev/null 2>&1; then
bsdtar -xf $cache_root/$package
else
tar --no-same-owner -xaf $cache_root/$package
fi
;;
esac
+17 -6
View File
@@ -1,9 +1,15 @@
#!/bin/sh
# Ver: 1.0 by Endial Fang (endial@126.com)
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
# 检查用户权限
if [ "$EUID" -ne 0 ]; then
echo "Error: This script must be run as root."
exit 1
fi
print_usage() {
echo "Usage: install_pkg <PACKAGE-NAME>"
echo ""
@@ -13,7 +19,7 @@ print_usage() {
echo " -h, --help Show this help message and exit."
echo ""
echo "Examples:"
echo " - Unpack package"
echo " - Install bash & curl"
echo " \$ install_pkg bash curl"
echo ""
}
@@ -23,12 +29,17 @@ if [ $# -lt 1 ]; then
exit 1
fi
case "$1" in
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
exit 0
;;
esac
*)
break
;;
esac
done
retry=0
max=2
@@ -36,7 +47,6 @@ until [ $retry -gt $max ]; do
set +e
(
apk update --no-cache
apk upgrade --no-cache
apk add --no-cache "$@"
)
CODE=$?
@@ -45,6 +55,7 @@ until [ $retry -gt $max ]; do
break
fi
if [ $retry -eq $max ]; then
echo "Failed to install packages after $max retries."
exit $CODE
fi
echo "APK failed, retrying"
+27 -3
View File
@@ -1,7 +1,31 @@
#!/bin/sh
# Ver: 1.0 by Endial Fang (endial@126.com)
#!/bin/bash
# Ver: 1.2 by Endial Fang (endial@126.com)
#
# 此脚本用于根据传入的参数选择对应的 apk 源配置文件并复制到指定目录
# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行)
set -eux
cp /etc/apk/repositories.${1:-default} /etc/apk/repositories
# 检查是否有足够的权限
if [ "$EUID" -ne 0 ]; then
echo "Error: This script must be run as root."
exit 1
fi
# 获取用户传入的参数,若未传入则使用默认值 "default"
source_name=${1:-default}
# 定义源文件路径
source_file="/etc/apk/repositories.${source_name}"
# 检查源文件是否存在
if [ ! -f "$source_file" ]; then
echo "Error: Source file $source_file does not exist."
exit 1
fi
# 定义目标文件路径
target_file="/etc/apk/repositories"
# 复制源文件到目标文件
cp "$source_file" "$target_file"
echo "Successfully selected apt source: $source_name"