feat: add shared library (lib/common.sh)
17 functions: logging (info/warn/error/success/step/phase_header), env management (load_env/save_env_var/require_vars), SSH wrappers (ssh_exec/ssh_check/scp_to), API wrappers (gitea_api/gitea_backup_api/ github_api), template rendering, and polling (wait_for_http/wait_for_ssh). All logs go to stderr, JSON data to stdout. Shellcheck clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
290
lib/common.sh
Normal file
290
lib/common.sh
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# lib/common.sh — Shared functions for Gitea Migration Toolkit
|
||||||
|
# Source this file in every script: source "$(dirname "$0")/lib/common.sh"
|
||||||
|
# =============================================================================
|
||||||
|
# This file defines functions only. No side effects at source time.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Colors (only if stderr is a terminal)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ -t 2 ]]; then
|
||||||
|
_C_RESET='\033[0m'
|
||||||
|
_C_RED='\033[0;31m'
|
||||||
|
_C_GREEN='\033[0;32m'
|
||||||
|
_C_YELLOW='\033[0;33m'
|
||||||
|
_C_BLUE='\033[0;34m'
|
||||||
|
_C_BOLD='\033[1m'
|
||||||
|
else
|
||||||
|
_C_RESET='' _C_RED='' _C_GREEN='' _C_YELLOW='' _C_BLUE='' _C_BOLD=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logging — all output to stderr so stdout stays clean for data (JSON, etc.)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
printf '%b[INFO]%b %s\n' "$_C_BLUE" "$_C_RESET" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
printf '%b[WARN]%b %s\n' "$_C_YELLOW" "$_C_RESET" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
printf '%b[ERROR]%b %s\n' "$_C_RED" "$_C_RESET" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
printf '%b[OK]%b %s\n' "$_C_GREEN" "$_C_RESET" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_step() {
|
||||||
|
local step_num="$1"; shift
|
||||||
|
printf ' %b[%s]%b %s\n' "$_C_BOLD" "$step_num" "$_C_RESET" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
phase_header() {
|
||||||
|
local num="$1" name="$2"
|
||||||
|
printf '\n%b=== Phase %s: %s ===%b\n\n' "$_C_BOLD" "$num" "$name" "$_C_RESET" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Environment
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Resolve the project root (directory containing .env / .env.example)
|
||||||
|
_project_root() {
|
||||||
|
local dir
|
||||||
|
dir="$(cd "$(dirname "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")/.." && pwd)"
|
||||||
|
# Walk up until we find .env or .env.example
|
||||||
|
while [[ "$dir" != "/" ]]; do
|
||||||
|
if [[ -f "$dir/.env" ]] || [[ -f "$dir/.env.example" ]]; then
|
||||||
|
printf '%s' "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
dir="$(dirname "$dir")"
|
||||||
|
done
|
||||||
|
# Fallback: script's parent dir
|
||||||
|
cd "$(dirname "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")/.." && pwd
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_file
|
||||||
|
env_file="$(_project_root)/.env"
|
||||||
|
if [[ ! -f "$env_file" ]]; then
|
||||||
|
log_error ".env file not found at $env_file"
|
||||||
|
log_error "Copy .env.example to .env and populate values."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
save_env_var() {
|
||||||
|
local key="$1" value="$2"
|
||||||
|
local env_file
|
||||||
|
env_file="$(_project_root)/.env"
|
||||||
|
|
||||||
|
if [[ ! -f "$env_file" ]]; then
|
||||||
|
log_error ".env file not found at $env_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape special characters in value for sed
|
||||||
|
local escaped_value
|
||||||
|
escaped_value=$(printf '%s' "$value" | sed 's/[&/\]/\\&/g')
|
||||||
|
|
||||||
|
if grep -q "^${key}=" "$env_file"; then
|
||||||
|
# Replace existing line — match KEY= followed by anything (value + optional comment)
|
||||||
|
sed -i.bak "s|^${key}=.*|${key}=${escaped_value}|" "$env_file"
|
||||||
|
rm -f "${env_file}.bak"
|
||||||
|
else
|
||||||
|
# Append new line
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also export it in current shell
|
||||||
|
export "${key}=${value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_vars() {
|
||||||
|
local var
|
||||||
|
for var in "$@"; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
log_error "Missing required var: $var"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SSH
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ssh_exec() {
|
||||||
|
local host_key="$1"; shift
|
||||||
|
local cmd="$*"
|
||||||
|
|
||||||
|
local ip_var="${host_key}_IP"
|
||||||
|
local user_var="${host_key}_SSH_USER"
|
||||||
|
local port_var="${host_key}_SSH_PORT"
|
||||||
|
|
||||||
|
local ip="${!ip_var:-}"
|
||||||
|
local user="${!user_var:-}"
|
||||||
|
local port="${!port_var:-22}"
|
||||||
|
|
||||||
|
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
||||||
|
log_error "SSH config incomplete for $host_key: ${ip_var}=${ip:-<empty>}, ${user_var}=${user:-<empty>}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh -o ConnectTimeout=10 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-p "$port" \
|
||||||
|
"${user}@${ip}" \
|
||||||
|
"$cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_check() {
|
||||||
|
local host_key="$1"
|
||||||
|
ssh_exec "$host_key" "true" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
scp_to() {
|
||||||
|
local host_key="$1" src="$2" dst="$3"
|
||||||
|
|
||||||
|
local ip_var="${host_key}_IP"
|
||||||
|
local user_var="${host_key}_SSH_USER"
|
||||||
|
local port_var="${host_key}_SSH_PORT"
|
||||||
|
|
||||||
|
local ip="${!ip_var:-}"
|
||||||
|
local user="${!user_var:-}"
|
||||||
|
local port="${!port_var:-22}"
|
||||||
|
|
||||||
|
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
||||||
|
log_error "SCP config incomplete for $host_key"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
scp -o ConnectTimeout=10 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-P "$port" \
|
||||||
|
"$src" "${user}@${ip}:${dst}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# API wrappers — return JSON on stdout, logs go to stderr
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_api_call() {
|
||||||
|
local base_url="$1" token="$2" method="$3" path="$4" data="${5:-}"
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
local tmpfile
|
||||||
|
tmpfile=$(mktemp)
|
||||||
|
|
||||||
|
local -a curl_args=(
|
||||||
|
-s
|
||||||
|
-X "$method"
|
||||||
|
-H "Authorization: token ${token}"
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
-H "Accept: application/json"
|
||||||
|
-o "$tmpfile"
|
||||||
|
-w "%{http_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
curl_args+=(-d "$data")
|
||||||
|
fi
|
||||||
|
|
||||||
|
http_code=$(curl "${curl_args[@]}" "${base_url}${path}") || {
|
||||||
|
log_error "curl failed for $method ${base_url}${path}"
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$http_code" -ge 400 ]]; then
|
||||||
|
log_error "API $method ${path} returned HTTP ${http_code}"
|
||||||
|
log_error "Response: $(cat "$tmpfile")"
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$tmpfile"
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_api() {
|
||||||
|
local method="$1" path="$2" data="${3:-}"
|
||||||
|
_api_call "${GITEA_INTERNAL_URL}/api/v1" "${GITEA_ADMIN_TOKEN}" "$method" "$path" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_backup_api() {
|
||||||
|
local method="$1" path="$2" data="${3:-}"
|
||||||
|
_api_call "${GITEA_BACKUP_INTERNAL_URL}/api/v1" "${GITEA_BACKUP_ADMIN_TOKEN}" "$method" "$path" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
github_api() {
|
||||||
|
local method="$1" path="$2" data="${3:-}"
|
||||||
|
_api_call "https://api.github.com" "${GITHUB_TOKEN}" "$method" "$path" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Templates
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
render_template() {
|
||||||
|
local src="$1" dest="$2"
|
||||||
|
|
||||||
|
if [[ ! -f "$src" ]]; then
|
||||||
|
log_error "Template not found: $src"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
envsubst < "$src" > "$dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Polling / waiting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
wait_for_http() {
|
||||||
|
local url="$1" max_secs="${2:-60}"
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
log_info "Waiting for HTTP 200 at ${url} (timeout: ${max_secs}s)..."
|
||||||
|
while [[ $elapsed -lt $max_secs ]]; do
|
||||||
|
if curl -sf -o /dev/null "$url" 2>/dev/null; then
|
||||||
|
log_success "HTTP 200 at ${url} (after ${elapsed}s)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
log_error "Timeout waiting for ${url} after ${max_secs}s"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_ssh() {
|
||||||
|
local host_key="$1" max_secs="${2:-60}"
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
log_info "Waiting for SSH to ${host_key} (timeout: ${max_secs}s)..."
|
||||||
|
while [[ $elapsed -lt $max_secs ]]; do
|
||||||
|
if ssh_check "$host_key"; then
|
||||||
|
log_success "SSH to ${host_key} connected (after ${elapsed}s)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
log_error "Timeout waiting for SSH to ${host_key} after ${max_secs}s"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user