Files
Pulse/scripts/lib/http.sh
2025-12-13 15:44:51 +00:00

285 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# HTTP helpers for Pulse shell scripts (downloads, API calls, GitHub queries).
set -euo pipefail
# Ensure common library is loaded when sourced directly.
if ! declare -F common::log_info >/dev/null 2>&1; then
# shellcheck disable=SC1091
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
fi
declare -g HTTP_DEFAULT_RETRIES="${HTTP_DEFAULT_RETRIES:-3}"
declare -g HTTP_DEFAULT_BACKOFF="${HTTP_DEFAULT_BACKOFF:-1 3 5}"
# http::detect_download_tool
# Emits the preferred download tool (curl/wget) or fails if neither exist.
http::detect_download_tool() {
if command -v curl >/dev/null 2>&1; then
printf 'curl\n'
return 0
fi
if command -v wget >/dev/null 2>&1; then
printf 'wget\n'
return 0
fi
return 1
}
# http::download --url URL --output PATH [--insecure] [--quiet] [--header "Name: value"]
# [--retries N] [--backoff "1 3 5"]
# Downloads the specified URL to PATH using curl or wget.
http::download() {
local url=""
local output=""
local insecure=false
local quiet=false
local retries="${HTTP_DEFAULT_RETRIES}"
local backoff="${HTTP_DEFAULT_BACKOFF}"
local -a headers=()
while (($#)); do
case "$1" in
--url)
url="$2"
shift 2
;;
--output)
output="$2"
shift 2
;;
--insecure)
insecure=true
shift
;;
--quiet)
quiet=true
shift
;;
--header)
headers+=("$2")
shift 2
;;
--retries)
retries="$2"
shift 2
;;
--backoff)
backoff="$2"
shift 2
;;
*)
common::log_warn "Unknown flag for http::download: $1"
shift
;;
esac
done
if [[ -z "${url}" || -z "${output}" ]]; then
common::fail "http::download requires --url and --output"
fi
if common::is_dry_run; then
common::log_info "[dry-run] Download ${url} -> ${output}"
return 0
fi
local tool
if ! tool="$(http::detect_download_tool)"; then
common::fail "No download tool available (install curl or wget)"
fi
mkdir -p "$(dirname "${output}")"
local -a cmd
if [[ "${tool}" == "curl" ]]; then
cmd=(curl -fL --connect-timeout 15)
if [[ "${quiet}" == true ]]; then
cmd+=(-sS)
else
cmd+=(--progress-bar)
fi
if [[ "${insecure}" == true ]]; then
cmd+=(-k)
fi
local header
for header in "${headers[@]}"; do
cmd+=(-H "${header}")
done
cmd+=(-o "${output}" "${url}")
else
cmd=(wget --tries=3)
if [[ "${quiet}" == true ]]; then
cmd+=(-q)
else
cmd+=(--progress=bar:force)
fi
if [[ "${insecure}" == true ]]; then
cmd+=(--no-check-certificate)
fi
local header
for header in "${headers[@]}"; do
cmd+=(--header="${header}")
done
cmd+=(-O "${output}" "${url}")
fi
local -a run_args=(--label "download ${url}")
[[ -n "${retries}" ]] && run_args+=(--retries "${retries}")
[[ -n "${backoff}" ]] && run_args+=(--backoff "${backoff}")
common::run "${run_args[@]}" -- "${cmd[@]}"
}
# http::api_call --url URL [--method METHOD] [--token TOKEN] [--bearer TOKEN]
# [--body DATA] [--header "Name: value"] [--insecure]
# Performs an API request and prints the response body.
http::api_call() {
local url=""
local method="GET"
local token=""
local bearer=""
local body=""
local insecure=false
local -a headers=()
while (($#)); do
case "$1" in
--url)
url="$2"
shift 2
;;
--method)
method="$2"
shift 2
;;
--token)
token="$2"
shift 2
;;
--bearer)
bearer="$2"
shift 2
;;
--body)
body="$2"
shift 2
;;
--header)
headers+=("$2")
shift 2
;;
--insecure)
insecure=true
shift
;;
*)
common::log_warn "Unknown flag for http::api_call: $1"
shift
;;
esac
done
if [[ -z "${url}" ]]; then
common::fail "http::api_call requires --url"
fi
if common::is_dry_run; then
common::log_info "[dry-run] API ${method} ${url}"
return 0
fi
local tool
if ! tool="$(http::detect_download_tool)"; then
common::fail "No HTTP client available (install curl or wget)"
fi
local -a cmd
if [[ "${tool}" == "curl" ]]; then
cmd=(curl -fsSL)
[[ "${insecure}" == true ]] && cmd+=(-k)
[[ -n "${method}" ]] && cmd+=(-X "${method}")
if [[ -n "${body}" ]]; then
cmd+=(-d "${body}")
fi
if [[ -n "${token}" ]]; then
headers+=("X-API-Token: ${token}")
fi
if [[ -n "${bearer}" ]]; then
headers+=("Authorization: Bearer ${bearer}")
fi
local header
for header in "${headers[@]}"; do
cmd+=(-H "${header}")
done
cmd+=("${url}")
else
cmd=(wget -qO-)
[[ "${insecure}" == true ]] && cmd+=(--no-check-certificate)
[[ -n "${method}" ]] && cmd+=(--method="${method}")
if [[ -n "${body}" ]]; then
cmd+=(--body-data="${body}")
fi
if [[ -n "${token}" ]]; then
cmd+=(--header="X-API-Token: ${token}")
fi
if [[ -n "${bearer}" ]]; then
cmd+=(--header="Authorization: Bearer ${bearer}")
fi
local header
for header in "${headers[@]}"; do
cmd+=(--header="${header}")
done
cmd+=("${url}")
fi
common::run_capture --label "api ${method} ${url}" -- "${cmd[@]}"
}
# http::get_github_latest_release owner/repo
# Echoes the latest release tag for a GitHub repository.
http::get_github_latest_release() {
local repo="${1:-}"
if [[ -z "${repo}" ]]; then
common::fail "http::get_github_latest_release requires owner/repo argument"
fi
local response
response="$(http::api_call --url "https://api.github.com/repos/${repo}/releases/latest" --header "Accept: application/vnd.github+json" 2>/dev/null || true)"
if [[ -z "${response}" ]]; then
return 1
fi
if [[ "${response}" =~ \"tag_name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
printf '%s\n' "${BASH_REMATCH[1]}"
return 0
fi
if [[ "${response}" =~ \"name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
printf '%s\n' "${BASH_REMATCH[1]}"
return 0
fi
common::log_warn "Unable to parse GitHub release tag for ${repo}"
return 1
}
# http::parse_bool value
# Parses truthy/falsy strings and prints canonical true/false.
http::parse_bool() {
local input="${1:-}"
local lowered="${input,,}"
case "${lowered}" in
1|true|yes|y|on)
printf 'true\n'
return 0
;;
0|false|no|n|off|"")
printf 'false\n'
return 0
;;
esac
return 1
}