Files
hykocx 15c42e1f24 feat(lib): add require_disk_space helper and enforce pre-flight disk checks
- add `require_disk_space` function to lib/common.sh with dedup logic for shared filesystems
- gate cloudpanel, coolify, pterodactyl installs behind a 2–3 GB disk check
- gate uptime-kuma, proxmox update, harden, update-server, and server-benchmark behind 300–1024 MB disk checks
- fail early with a clear error before apt installs or config writes can leave the system in a partial state
2026-05-12 22:39:19 -04:00

295 lines
11 KiB
Bash

#!/bin/bash
# LXS - Common library
# Sourced by lxs.sh and all sub-scripts. Provides colors, UI helpers, loggers,
# spinner, OS guards, and shared utilities (public IP, password generation,
# apt lock wait, root re-exec, apt non-interactive setup).
# Repo: https://git.hyko.cx/hykocx/lxs
# ═══════════════════════════════════════════════════════════════════════════
# Colors — minimal palette: red, cyan, white (+ gray as a white shade)
# ═══════════════════════════════════════════════════════════════════════════
RED='\033[38;2;255;64;64m' # errors, destructive actions
CYAN='\033[38;2;0;229;255m' # accents, titles, OK, info
WHITE='\033[38;2;240;240;240m' # primary text
GRAY='\033[38;2;140;140;140m' # secondary text, comments, separators
NC='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'
# Legacy aliases — older sub-scripts still reference these. Map onto CYAN
# so the palette stays at red/cyan/white without breaking them.
YELLOW="$CYAN"
GREEN="$CYAN"
MAGENTA="$CYAN"
PURPLE="$CYAN"
# ═══════════════════════════════════════════════════════════════════════════
# UI helpers — title + horizontal rule, no closed box.
#
# Layout:
# show_box_top "TITLE" ["RIGHT_TAG"] → TITLE [ RIGHT ]
# ─────────────────────────────────
# show_box_mid "SECTION" → ─ SECTION ─────────────────────
# show_box_bottom → ───────────────────────────────
# show_separator → ─── (light gray divider)
# show_menu_item "01" "LABEL" "desc" → [01] LABEL // desc
# show_prompt → >
# ═══════════════════════════════════════════════════════════════════════════
_lxs_term_cols() {
local cols
cols=$(tput cols 2>/dev/null || echo 80)
[ "$cols" -gt 100 ] && cols=100
[ "$cols" -lt 60 ] && cols=60
echo "$cols"
}
show_box_top() {
local title="$1" right="${2:-}"
local cols pad_len pad
cols=$(_lxs_term_cols)
if [ -n "$right" ]; then
pad_len=$(( cols - 2 - ${#title} - ${#right} - 4 ))
[ "$pad_len" -lt 1 ] && pad_len=1
pad=$(printf ' %.0s' $(seq 1 "$pad_len"))
printf " ${CYAN}${BOLD}%s${NC}%s${GRAY}[ ${CYAN}%s${GRAY} ]${NC}\n" \
"$title" "$pad" "$right"
else
printf " ${CYAN}${BOLD}%s${NC}\n" "$title"
fi
_lxs_hr "$cols"
}
show_box_mid() {
local title="$1"
local cols fill_len fill
cols=$(_lxs_term_cols)
fill_len=$(( cols - ${#title} - 4 ))
[ "$fill_len" -lt 2 ] && fill_len=2
fill=$(printf '─%.0s' $(seq 1 "$fill_len"))
printf "${GRAY}${CYAN}${BOLD}%s${NC} ${GRAY}%s${NC}\n" "$title" "$fill"
}
show_box_bottom() {
_lxs_hr "$(_lxs_term_cols)"
}
show_separator() {
_lxs_hr "$(_lxs_term_cols)"
}
_lxs_hr() {
local cols=$1 fill
fill=$(printf '─%.0s' $(seq 1 "$cols"))
printf "${GRAY}%s${NC}\n" "$fill"
}
show_title() {
local title="$1"
local subtitle="${2:-}"
echo ""
if [ -n "$subtitle" ]; then
show_box_top "$title" "$subtitle"
else
show_box_top "$title"
fi
}
# Render a menu line. Pass "exit" as 4th arg to color the key red.
# show_menu_item "01" "APPLICATIONS" "deploy stacks"
# show_menu_item "00" "EXIT" "quit shell" exit
show_menu_item() {
local key="$1" label="$2" desc="${3:-}" kind="${4:-}"
local key_color="${CYAN}"
[ "$kind" = "exit" ] && key_color="${RED}"
if [ -n "$desc" ]; then
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%-18s${NC} ${GRAY}// %s${NC}\n" \
"$key" "$label" "$desc"
else
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%s${NC}\n" "$key" "$label"
fi
}
show_prompt() {
echo -e -n " ${CYAN}>${NC} "
}
info() { echo -e "${CYAN}[..]${NC} $*"; }
ok() { echo -e "${CYAN}[OK]${NC} $*"; }
warn() { echo -e "${CYAN}[!!]${NC} $*"; }
err() { echo -e "${RED}[KO]${NC} $*" >&2; }
# ═══════════════════════════════════════════════════════════════════════════
# Spinner — runs a shell command, redirects output to a log file, shows
# a spinner with a label, prints ✓/✗ on completion. Returns the command's
# exit code.
#
# Usage:
# run_spinner "Installing foo..." "apt-get install -y foo"
#
# Log file: ${LXS_LOG_FILE:-/tmp/lxs.log}
# ═══════════════════════════════════════════════════════════════════════════
run_spinner() {
local message=$1
shift
local command="$*"
local spinstr='|/-\'
local log="${LXS_LOG_FILE:-/tmp/lxs.log}"
echo -e "${CYAN}[..] ${message}${NC}"
eval "$command" > "$log" 2>&1 &
local pid=$!
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf "\r${CYAN}[%c.]${NC} ${message}" "$spinstr"
spinstr=$temp${spinstr%"$temp"}
sleep 0.15
done
wait "$pid"
local exit_code=$?
if [ $exit_code -eq 0 ]; then
printf "\r${CYAN}[OK]${NC} ${message}\n"
else
printf "\r${RED}[KO]${NC} ${message}\n"
fi
return $exit_code
}
# ═══════════════════════════════════════════════════════════════════════════
# Guards
# ═══════════════════════════════════════════════════════════════════════════
require_debian_ubuntu() {
if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
err "This script supports Debian/Ubuntu only."
return 1
fi
}
# Verify enough free disk space before doing apt installs or writing config.
# Fails early with a clear message so we don't get half-installed packages or
# files truncated mid-write when the disk is full.
#
# Usage:
# require_disk_space # 500MB on /var (apt cache + lists)
# require_disk_space 1024 # 1024MB on /var
# require_disk_space 1024 /var /opt # 1024MB on each listed path
#
# Duplicate filesystems (e.g. /var on the same fs as /) are checked once.
require_disk_space() {
local min_mb=${1:-500}
shift 2>/dev/null || true
local paths=("$@")
[ ${#paths[@]} -eq 0 ] && paths=(/var)
local path avail fs seen=" " rc=0
for path in "${paths[@]}"; do
fs=$(df -P "$path" 2>/dev/null | awk 'NR==2 {print $1}')
if [ -z "$fs" ]; then
err "Cannot read disk usage for ${path}"
rc=1
continue
fi
[[ "$seen" == *" $fs "* ]] && continue
seen="$seen$fs "
avail=$(df -Pm "$path" 2>/dev/null | awk 'NR==2 {print $4}')
if [ "${avail:-0}" -lt "$min_mb" ]; then
err "Not enough disk space on ${path} (${fs}): ${avail}MB free, ${min_mb}MB required."
rc=1
fi
done
return $rc
}
# Re-exec the current script via sudo if not already root. Preserves env and
# arguments. Call near the top of a sub-script:
# require_root "$0" "$@"
require_root() {
[ "$EUID" -eq 0 ] && return 0
local self=$1
shift
exec sudo -E "$self" "$@"
}
# ═══════════════════════════════════════════════════════════════════════════
# Network / random / apt helpers
# ═══════════════════════════════════════════════════════════════════════════
get_public_ip() {
local ip=""
ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) || \
ip=$(curl -s --max-time 5 icanhazip.com 2>/dev/null) || \
ip=$(curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) || \
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
echo "$ip"
}
# Generate a random alphanumeric string. Default length: 32.
generate_password() {
local length=${1:-32}
LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | head -c "$length"
}
# Configure apt/debconf for fully non-interactive runs. Safe to call multiple
# times. No-op when apt is absent.
apt_noninteractive() {
command -v apt >/dev/null 2>&1 || return 0
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a
export NEEDRESTART_SUSPEND=1
echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true
}
# Add a UFW allow rule, but only if UFW is installed AND active. No-op
# otherwise — so app installers can declare the ports they need without
# forcing UFW on hosts that don't use it.
#
# Usage:
# ufw_allow 8000/tcp "Coolify dashboard"
# ufw_allow 80/tcp
ufw_allow() {
command -v ufw >/dev/null 2>&1 || return 0
ufw status 2>/dev/null | grep -q "Status: active" || return 0
local rule=$1 comment=${2:-}
if [ -n "$comment" ]; then
ufw allow "$rule" comment "$comment" >/dev/null
else
ufw allow "$rule" >/dev/null
fi
ok "UFW :: allowed ${rule}${comment:+ (${comment})}"
}
# Wait for other apt/dpkg processes to release their locks. Up to 120s.
wait_for_apt() {
command -v apt >/dev/null 2>&1 || return 0
local max_wait=120
local wait_count=0
local lock_detected=false
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \
fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \
fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
[ "$lock_detected" = false ] && lock_detected=true && \
echo -e "${YELLOW}[!!] Waiting for other package manager to finish...${NC}"
wait_count=$((wait_count + 1))
[ $wait_count -ge $max_wait ] && \
echo -e "${RED}[KO] Timeout waiting for package manager${NC}" && return 1
printf "\r${GRAY}[..] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait
sleep 1
done
[ "$lock_detected" = true ] && \
echo -e "\n${GREEN}[OK] Package manager is now available${NC}"
return 0
}