diff --git a/misc/tools.func b/misc/tools.func index da0f954a3..cdc10e369 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -8665,3 +8665,654 @@ EOF $STD apt update return 0 } + +# ------------------------------------------------------------------------------ +# Get latest GitLab release version. +# Usage: get_latest_gitlab_release "owner/repo" [strip_v] +# ------------------------------------------------------------------------------ +get_latest_gitlab_release() { + local repo="$1" + local strip_v="${2:-true}" + + local repo_encoded + repo_encoded=$(printf '%s' "$repo" | sed 's|/|%2F|g') + + local header=() + [[ -n "${GITLAB_TOKEN:-}" ]] && header=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN") + + local temp_file + temp_file=$(mktemp) + + local http_code + http_code=$(curl --connect-timeout 10 --max-time 30 -sSL \ + -w "%{http_code}" -o "$temp_file" \ + "${header[@]}" \ + "https://gitlab.com/api/v4/projects/$repo_encoded/releases?per_page=1&order_by=released_at&sort=desc" 2>/dev/null) || true + + if [[ "$http_code" != "200" ]]; then + rm -f "$temp_file" + msg_warn "GitLab API call failed for ${repo} (HTTP ${http_code})" + return 22 + fi + + local version + version=$(jq -r '.[0].tag_name // empty' "$temp_file") + rm -f "$temp_file" + + if [[ -z "$version" ]]; then + msg_error "Could not determine latest version for ${repo}" + return 250 + fi + + if [[ "$strip_v" == "true" ]]; then + [[ "$version" =~ ^v[0-9] ]] && version="${version:1}" + fi + + echo "$version" +} + +# ------------------------------------------------------------------------------ +# Checks for new GitLab release (latest tag). +# +# Description: +# - Queries the GitLab API for the latest release tag +# - Compares it to a local cached version (~/.) +# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0 +# +# Usage: +# if check_for_gl_release "myapp" "owner/repo" [optional] "v1.2.3"; then +# # trigger update... +# fi +# exit 0 +# } (end of update_script not from the function) +# +# Notes: +# - Requires `jq` (auto-installed if missing) +# - Supports GITLAB_TOKEN env var for private/rate-limited repos +# - Does not modify anything, only checks version state +# ------------------------------------------------------------------------------ +check_for_gl_release() { + local app="$1" + local source="$2" + local pinned_version_in="${3:-}" # optional + local pin_reason="${4:-}" # optional reason shown to user + local app_lc="${app,,}" + local current_file="$HOME/.${app_lc}" + + msg_info "Checking for update: ${app}" + + # DNS check + if ! getent hosts gitlab.com >/dev/null 2>&1; then + msg_error "Network error: cannot resolve gitlab.com" + return 6 + fi + + ensure_dependencies jq + + local repo_encoded + repo_encoded=$(printf '%s' "$repo" | sed 's|/|%2F|g') + echo "$source" | sed 's|/|%2F|g') + + local header=() + [[ -n "${GITLAB_TOKEN:-}" ]] && header=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN") + + local releases_json="" http_code="" + + # For pinned versions, try to fetch the specific release tag first + if [[ -n "$pinned_version_in" ]]; then + local pinned_encoded="${pinned_version_in//\//%2F}" + http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gl_check.json \ + "${header[@]}" \ + "https://gitlab.com/api/v4/projects/$repo_encoded/releases/$pinned_encoded" 2>/dev/null) || true + if [[ "$http_code" == "200" ]] && [[ -s /tmp/gl_check.json ]]; then + releases_json="[$(/dev/null) || true + + if [[ "$http_code" == "200" ]] && [[ -s /tmp/gl_check.json ]]; then + releases_json=$(/dev/null) + if ((${#legacy_files[@]} == 1)); then + current="$(<"${legacy_files[0]}")" + echo "${current#v}" >"$current_file" + rm -f "${legacy_files[0]}" + fi + fi + if [[ "$current" =~ ^v[0-9] ]]; then + current="${current:1}" + fi + + # Pinned version handling + if [[ -n "$pinned_version_in" ]]; then + local pin_clean + if [[ "$pinned_version_in" =~ ^v[0-9] ]]; then + pin_clean="${pinned_version_in:1}" + else + pin_clean="$pinned_version_in" + fi + local match_raw="" + for i in "${!clean_tags[@]}"; do + if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then + match_raw="${raw_tags[$i]}" + break + fi + done + + if [[ -z "$match_raw" ]]; then + msg_error "Pinned version ${pinned_version_in} not found upstream" + return 250 + fi + + if [[ "$current" != "$pin_clean" ]]; then + CHECK_UPDATE_RELEASE="$match_raw" + msg_ok "Update available: ${app} ${current:-not installed} → ${pin_clean}" + return 0 + fi + + if [[ -n "$pin_reason" ]]; then + msg_ok "No update available: ${app} (${current}) - update held back: ${pin_reason}" + else + msg_ok "No update available: ${app} (${current}) - update temporarily held back due to issues with newer releases" + fi + return 1 + fi + + # No pinning → use latest + if [[ -z "$current" || "$current" != "$latest_clean" ]]; then + CHECK_UPDATE_RELEASE="$latest_raw" + msg_ok "Update available: ${app} ${current:-not installed} → ${latest_clean}" + return 0 + fi + + msg_ok "No update available: ${app} (${latest_clean})" + return 1 +} + +function fetch_and_deploy_gl_release() { + local app="$1" + local repo="$2" + local mode="${3:-tarball}" + local version="${var_appversion:-${4:-latest}}" + local target="${5:-/opt/$app}" + local asset_pattern="${6:-}" + + if [[ -z "$app" ]]; then + app="${repo##*/}" + if [[ -z "$app" ]]; then + msg_error "fetch_and_deploy_gl_release requires app name or valid repo" + return 1 + fi + fi + + local app_lc=$(echo "${app,,}" | tr -d ' ') + local version_file="$HOME/.${app_lc}" + + local api_timeout="--connect-timeout 10 --max-time 60" + local download_timeout="--connect-timeout 15 --max-time 900" + + local current_version="" + [[ -f "$version_file" ]] && current_version=$(<"$version_file") + + ensure_dependencies jq + + local repo_encoded + repo_encoded=$(printf '%s' "$repo" | sed 's|/|%2F|g') + echo "$repo" | sed 's|/|%2F|g') + + local api_base="https://gitlab.com/api/v4/projects/$repo_encoded/releases" + local api_url + if [[ "$version" != "latest" ]]; then + api_url="$api_base/$version" + else + api_url="$api_base?per_page=1&order_by=released_at&sort=desc" + fi + + local header=() + [[ -n "${GITLAB_TOKEN:-}" ]] && header=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN") + + local max_retries=3 retry_delay=2 attempt=1 success=false http_code + + while ((attempt <= max_retries)); do + http_code=$(curl $api_timeout -sSL -w "%{http_code}" -o /tmp/gl_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true + if [[ "$http_code" == "200" ]]; then + success=true + break + elif [[ "$http_code" == "429" ]]; then + if ((attempt < max_retries)); then + msg_warn "GitLab API rate limit hit, retrying in ${retry_delay}s... (attempt $attempt/$max_retries)" + sleep "$retry_delay" + retry_delay=$((retry_delay * 2)) + fi + else + sleep "$retry_delay" + fi + ((attempt++)) + done + + if ! $success; then + if [[ "$http_code" == "401" ]]; then + msg_error "GitLab API authentication failed (HTTP 401)." + if [[ -n "${GITLAB_TOKEN:-}" ]]; then + msg_error "Your GITLAB_TOKEN appears to be invalid or expired." + else + msg_error "The repository may require authentication. Try: export GITLAB_TOKEN=\"glpat-your_token\"" + fi + elif [[ "$http_code" == "404" ]]; then + msg_error "GitLab project or release not found (HTTP 404)." + msg_error "Ensure '$repo' is correct and the project is accessible." + elif [[ "$http_code" == "429" ]]; then + msg_error "GitLab API rate limit exceeded (HTTP 429)." + msg_error "To increase the limit, export a GitLab token before running the script:" + msg_error " export GITLAB_TOKEN=\"glpat-your_token_here\"" + elif [[ "$http_code" == "000" || -z "$http_code" ]]; then + msg_error "GitLab API connection failed (no response)." + msg_error "Check your network/DNS: curl -sSL https://gitlab.com/api/v4/version" + else + msg_error "Failed to fetch release metadata (HTTP $http_code)" + fi + return 1 + fi + + local json tag_name + json=$(/dev/null || uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" + + local assets url_match="" + assets=$(_gl_asset_urls "$json") + + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in + $asset_pattern) + url_match="$u" + break + ;; + esac + done + fi + + if [[ -z "$url_match" ]]; then + for u in $assets; do + if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then + url_match="$u" + break + fi + done + fi + + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + + if [[ -z "$url_match" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "binary" "$asset_pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + assets=$(_gl_asset_urls "$json") + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in $asset_pattern) + url_match="$u" + break + ;; + esac + done + fi + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]] && url_match="$u" && break + done + fi + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + fi + fi + + if [[ -z "$url_match" ]]; then + msg_error "No suitable .deb asset found for $app" + rm -rf "$tmpdir" + return 1 + fi + + filename="${url_match##*/}" + curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$url_match" || { + msg_error "Download failed: $url_match" + rm -rf "$tmpdir" + return 1 + } + + chmod 644 "$tmpdir/$filename" + local dpkg_opts="" + [[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold" + [[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew" + DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || { + SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || { + msg_error "Both apt and dpkg installation failed" + rm -rf "$tmpdir" + return 1 + } + } + + ### Prebuild Mode ### + elif [[ "$mode" == "prebuild" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + if [[ -z "$asset_url" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "prebuild" "$pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in $pattern) + asset_url="$u" + break + ;; + esac + done + fi + fi + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + local unpack_tmp + unpack_tmp=$(mktemp -d) + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + if [[ "$filename" == *.zip ]]; then + ensure_dependencies unzip + unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || { + msg_error "Failed to extract ZIP archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + elif [[ "$filename" == *.tar.* || "$filename" == *.tgz || "$filename" == *.txz ]]; then + tar --no-same-owner -xf "$tmpdir/$filename" -C "$unpack_tmp" || { + msg_error "Failed to extract TAR archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unsupported archive format: $filename" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + + local top_entries inner_dir + top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) + if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then + inner_dir="$top_entries" + shopt -s dotglob nullglob + if compgen -G "$inner_dir/*" >/dev/null; then + cp -r "$inner_dir"/* "$target/" || { + msg_error "Failed to copy contents from $inner_dir to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Inner directory is empty: $inner_dir" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + else + shopt -s dotglob nullglob + if compgen -G "$unpack_tmp/*" >/dev/null; then + cp -r "$unpack_tmp"/* "$target/" || { + msg_error "Failed to copy contents to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unpacked archive is empty" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + fi + + ### Singlefile Mode ### + elif [[ "$mode" == "singlefile" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + if [[ -z "$asset_url" ]]; then + local fallback_json + if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "singlefile" "$pattern" "$tag_name"); then + json="$fallback_json" + tag_name=$(echo "$json" | jq -r '.tag_name // empty') + [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" + msg_info "Fetching GitLab release: $app ($version)" + for u in $(_gl_asset_urls "$json"); do + filename_candidate="${u##*/}" + case "$filename_candidate" in $pattern) + asset_url="$u" + break + ;; + esac + done + fi + fi + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + mkdir -p "$target" + + local use_filename="${USE_ORIGINAL_FILENAME:-false}" + local target_file="$app" + [[ "$use_filename" == "true" ]] && target_file="$filename" + + curl $download_timeout -fsSL "${header[@]}" -o "$target/$target_file" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then + chmod +x "$target/$target_file" + fi + + else + msg_error "Unknown mode: $mode" + rm -rf "$tmpdir" + return 1 + fi + + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" + rm -rf "$tmpdir" +}