Files
SODEW/sodew-bash-main/libs/main.sh
T
2026-08-18 09:40:08 +02:00

362 lines
10 KiB
Bash

# Remove a single trailing slash from a string.
# $1 (s): input string.
function strTrimSlash() {
local s="${1-}"
s="${s%/}"
printf '%s' "$s"
}
# Generates a random password with a specified length and character set.
#
# $1 (length): length of the password (defaults to 32 characters).
# $2 (charset): string of characters to use for generating the password (defaults to "A-Za-z0-9@#$%^*_+=[]{}:").
function strRandom() {
local length="${1:-32}"
local charset="${2:-"A-Za-z0-9_.-"}"
LC_ALL=C tr -dc "$charset" < /dev/urandom | head -c "$length"
}
# Format a number with thousands separators (space-separated groups).
# $1 (number): the number to format.
function numFormat() {
printf '%s\n' "${1-}" | sed ':a;s/\B[0-9]\{3\}\>/ &/;ta'
}
# Check if a value exists in an array passed as remaining arguments.
# $1 (needle): value to search for.
# $2 (items): array elements to search in (passed as individual arguments).
function arrayContains() {
local needle="${1-}"
shift || true
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
# Return success if $value exists as full line in multiline $list
function listContains() {
local value="${1-}"
local list="${2-}"
[[ -n "$value" ]] || return 1
grep -Fxq -- "$value" <<<"$list"
}
# Calculate the difference in seconds between two datetime strings.
# $1 (from): start datetime string (accepted by date -d).
# $2 (to): end datetime string (accepted by date -d).
function timeDiff() {
local from="${1-}"
local to="${2-}"
local t1 t2
[[ -n "$from" ]] || { appError "time_from not specified"; return 1; }
[[ -n "$to" ]] || { appError "time_to not specified"; return 1; }
t1="$(date -d "$from" +%s)" || { appError "invalid time_from: $from"; return 1; }
t2="$(date -d "$to" +%s)" || { appError "invalid time_to: $to"; return 1; }
printf '%s\n' "$(( t2 - t1 ))"
}
# Normalize a domain name: lowercase, strip whitespace and trailing dot, then IDN-encode.
# $1 (domain): raw domain string to normalize.
function domainPrepare() {
local domain="${1-}"
domain="${domain,,}"
domain="${domain//[[:space:]]/}"
domain="${domain%.}"
LC_ALL=C.UTF-8 idn2 <<<"$domain"
}
# Checks if the given string is a valid domain.
#
# $1 (domain): a string representing the domain.
function domainValidate() {
local domain="${1-}"
(( ${#domain} >= 4 )) || { appError "Domain name is too short: $domain"; return 1; }
(( ${#domain} <= 253 )) || { appError "Domain name is too long: $domain"; return 1; }
[[ "$domain" =~ ^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$ ]] || { appError "Invalid domain name format: $domain"; return 1; }
}
# Validate a domain name and reject reserved names.
# $1 (domain): domain name to check.
function domainCheck() {
local domain="${1-}"
local reserved=("root" "user")
arrayContains "$domain" "${reserved[@]}" && { appError "Reserved domain name: $domain"; return 1; }
domainValidate "$domain"
}
# Generate username and groupname from domain
function domainToUser() {
local domain="${1-}"
local base hash
base="$(printf '%s' "$domain" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9-]+/-/g; s/-{2,}/-/g; s/^-//; s/-$//')"
hash="$(printf '%s' "$domain$hostSalt" \
| sha256sum \
| awk '{print substr($1,1,8)}')"
printf '%s' "${base:0:21}-${hash}"
}
function domainToUid() {
local domain="${1-}"
local username uid
if [[ -z "$domain" ]]; then
appError "Domain not specified"
return 1
fi
username=$(domainToUser "$domain")
if [[ -z "$username" ]]; then
appError "Cannot compute username for: $domain"
return 1
fi
uid=$(id -u "$username" 2>/dev/null)
if [[ -z "$uid" ]]; then
appError "No such user: $username"
return 1
fi
if [[ "$uid" == "0" ]]; then
appError "Refusing to return root UID for: $domain"
return 1
fi
printf '%s' "$uid"
}
function domainToGid() {
local domain="${1-}"
local username gid
if [[ -z "$domain" ]]; then
appError "Domain not specified"
return 1
fi
username=$(domainToUser "$domain")
if [[ -z "$username" ]]; then
appError "Cannot compute username for: $domain"
return 1
fi
gid=$(id -g "$username" 2>/dev/null)
if [[ -z "$gid" ]]; then
appError "No such user: $username"
return 1
fi
if [[ "$gid" == "0" ]]; then
appError "Refusing to return root GID for: $domain"
return 1
fi
printf '%s' "$gid"
}
# Generate random string from domain
function domainToRandom() {
local domain="${1-}"
local maxLen="${2:-256}"
local tailLen="${3:-8}"
local base tail baseLen
base="$(printf '%s' "$domain" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/_/g; s/_{2,}/_/g; s/^_//; s/_$//')"
tail="$(strRandom "$tailLen" 'a-z0-9')" || return 1
baseLen=$(( maxLen - tailLen - 1 ))
(( baseLen < 1 )) && baseLen=1
printf '%s' "${base:0:baseLen}_${tail}"
}
# Run a command and capture its stdout and stderr into named variables.
# $1 (outVar): name of the variable to receive stdout.
# $2 (errVar): name of the variable to receive stderr.
# $3 (cmd): command and arguments to execute.
function run() {
local -n __runOut="$1"
local -n __runError="$2"
shift 2
local stdoutTmp stderrTmp status
stdoutTmp=$(mktemp) || return 1
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
"$@" >"$stdoutTmp" 2>"$stderrTmp"
status=$?
__runOut=$(<"$stdoutTmp")
__runError=$(<"$stderrTmp")
rm -f "$stdoutTmp" "$stderrTmp"
return "$status"
}
# Run command, discard stdout
function runError() {
local -n __runErrorErr="$1"
shift || true
local __runErrorOutput __runErrorError status
run __runErrorOutput __runErrorError "$@"
status=$?
if [[ -n "$__runErrorError" ]]; then
__runErrorErr="$__runErrorError"
else
__runErrorErr="$__runErrorOutput"
fi
return "$status"
}
# Run a command, discarding output, and print an error message on failure.
# $1 (cmd): command and arguments to execute.
function runFail() {
local __runFailErr
runError __runFailErr "$@"
local status=$?
[[ $status -eq 0 ]] || appError "$__runFailErr"
return $status
}
# Run a command and discard all stdout and stderr output.
# $1 (cmd): command and arguments to execute.
function runSilent() {
local output error
run output error "$@"
return $?
}
# Run a shell command string (with operator support) and capture stdout and stderr into named variables.
# $1 (outVar): name of the variable to receive stdout.
# $2 (errVar): name of the variable to receive stderr.
# $3 (cmd): command and arguments, including shell operators (|, &&, ;, etc.).
function runShell() {
local -n __out="$1"
local -n __err="$2"
shift 2
local stdoutTmp stderrTmp status
stdoutTmp=$(mktemp) || return 1
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
# Arguments: > >> < | || & && ; ( ) convert to operators.
local cmd="" arg
for arg in "$@"; do
if [[ "$arg" =~ ^([0-9]?>|[0-9]?>>|[0-9]?<|\||\|\||&&|;|\(|\))$ ]]; then
cmd+=" $arg"
else
cmd+=" $(printf '%q' "$arg")"
fi
done
(
set -o pipefail
eval -- "$cmd"
) >"$stdoutTmp" 2>"$stderrTmp"
status=$?
__out=$(<"$stdoutTmp")
__err=$(<"$stderrTmp")
rm -f "$stdoutTmp" "$stderrTmp"
return "$status"
}
# Converts notation (28Gi, 512Mi, 1Ki, 4Gb, ...) to bytes; returns -1 for empty input.
function sizeToBytes() {
local v="$1"
case "$v" in
*Ti) echo $(( ${v%Ti} * 1099511627776 )) ;;
*Gi) echo $(( ${v%Gi} * 1073741824 )) ;;
*Mi) echo $(( ${v%Mi} * 1048576 )) ;;
*Ki) echo $(( ${v%Ki} * 1024 )) ;;
*Tb) echo $(( ${v%Tb} * 1000000000000 )) ;;
*Gb) echo $(( ${v%Gb} * 1000000000 )) ;;
*Mb) echo $(( ${v%Mb} * 1000000 )) ;;
*Kb) echo $(( ${v%Kb} * 1000 )) ;;
"") echo -1 ;;
*) echo "$v" ;;
esac
}
#=========================================================================
# Comment out the first non-empty, non-commented line in the domains list file.
# Prints the domain name on success, returns 1 if no domain available.
function domainsListMark() {
local file="$1"
local line domain num=0 lineNum=0
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
while IFS= read -r line; do
(( num++ ))
[[ -z "$line" || "$line" == \#* ]] && continue
domain="$line"
lineNum=$num
break
done < "$file"
[[ $lineNum -eq 0 ]] && return 1
sed -i "${lineNum}s/^/#/" "$file"
printf '%s\n' "$domain"
}
# Remove the '#' prefix from the specified domain line in the domains list file.
function domainsListUnmark() {
local file="$1"
local domain="$2"
local line num=0 lineNum=0
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
while IFS= read -r line; do
(( num++ ))
[[ "$line" == "#${domain}" ]] && { lineNum=$num; break; }
done < "$file"
[[ $lineNum -eq 0 ]] && { appError "Domain not marked in list: $domain"; return 1; }
sed -i "${lineNum}s/^#//" "$file"
}
# Sends an email with the specified subject and body.
#
# $1 (mailSubject): string containing the subject of the email.
# $2 (mailBody): string containing the body of the email.
function emailSend() {
local mailSubject="$1"
local mailBody="$2"
[[ -n "$mailSubject" ]] || { appError "Subject not specified"; return 1; }
[[ -n "$mailBody" ]] || { appError "Body email not specified"; return 1; }
local result
result=$(printf 'Subject:%s\nFrom:%s\nTo:%s\n\n%s' "$mailSubject" "$mailFrom" "$mailTo" "$mailBody" | msmtp "$mailTo" 2>&1)
[[ $? -eq 0 ]] || { appError "$result"; return 1; }
}