LibrePortal/scripts/catalog/catalog_sources.sh
librelad abf3a65c88 refactor(catalog): move sources into a config file (CFG_CATALOG_1..9), generate from it
Per the intended model: catalog sources should live in the config place (like
domains), and registry_catalog.json should be a purely GENERATED artifact
derived from them — not the source of truth. Replaces the earlier
$docker_dir/catalog/sources.json store.

- New configs/general/general_catalogs — CFG_CATALOG_1..9, one catalog base URL
  per slot ("url" or "url|channel"), domains-style. Official stays pinned as
  source #1 (derived from CFG_RELEASE_BASE_URL, not listed here). Slot N → source
  idx N+1 (stable id for the Add picker / `app add --source`).
- catalog_sources.sh now reads/writes those CFG vars (via updateConfigOption)
  instead of a JSON file; dropped catalogSourcesFile + the enable/disable toggle
  (presence = enabled; remove = clear the slot).
- configUpdateBatch regenerates registry_catalog.json when a CFG_CATALOG_* key
  changed — so pressing Save in the WebUI rebuilds the browse data.
- webuiRegistryCatalogScan is unchanged (still iterates catalogEnabledSources).

Verified: CFG_CATALOG_1/2 → sources at idx 2/3, empty slots skipped, url|channel
parsed, official pinned at idx 1.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:01:22 +01:00

127 lines
5.9 KiB
Bash

#!/bin/bash
#
# Catalog sources — the ordered list of app-catalog URLs the App Center browses.
# ---------------------------------------------------------------------------
# SOURCE OF TRUTH = the CFG_CATALOG_1..9 config vars in configs/general/
# general_catalogs (edited from the WebUI Catalog Sources block, like domains).
# registry_catalog.json is a GENERATED artifact derived from these + the official
# source — rebuilt by webuiRegistryCatalogScan on config save + `updater check`.
#
# Source #1 is ALWAYS the official catalog, synthesized live from
# CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL (pinned, NOT in general_catalogs).
# Each extra slot CFG_CATALOG_<N> maps to source idx <N+1> (a stable id the Add
# dialog / `app add --source` can reference) and is UNVERIFIED third-party.
#
# Priority = official first, then slots 1..9 ascending; the highest-priority
# source offering an app is the default (Add dialog can pick another).
#
# SCOPE: browse + add only. System updates/hotfixes still resolve from the
# official CFG_RELEASE_BASE_URL alone (lpFetchIndex untouched) — a third-party
# catalog can never become a system-update channel.
CATALOG_MAX_SLOTS=9
# Extra (third-party) sources as a JSON array [{idx,name,base,channel}] in slot
# order, skipping empty slots. A slot value is "url" or "url|channel".
catalogExtraSourcesJson() {
command -v jq >/dev/null 2>&1 || { echo '[]'; return 0; }
local arr='[]' n var val url channel host
for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do
var="CFG_CATALOG_${n}"; val="${!var:-}"
[[ -z "$val" ]] && continue
url="${val%%|*}"
channel="stable"; [[ "$val" == *"|"* ]] && channel="${val##*|}"
[[ "$url" =~ ^https?://[^[:space:]/]+ ]] || continue
url="${url%/}"
host="${url#*://}"; host="${host%%/*}"
arr="$(jq -c --argjson i "$(( n + 1 ))" --arg name "$host" --arg base "$url" --arg ch "$channel" \
'. + [ { idx:$i, name:$name, base:$base, channel:$ch } ]' <<<"$arr")"
done
printf '%s' "$arr"
}
# The full ordered source list as a JSON array. Each: {idx,name,base,channel,official,trust}.
catalogSourcesJson() {
local off_base off_channel extras
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
off_channel="$(lpReleaseChannel)"
extras="$(catalogExtraSourcesJson)"
jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson extras "$extras" '
[ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel,
official: true, trust: "official" } ]
+ ( $extras | map( . + { official: false, trust: "community" } ) )'
}
# One source per line (compact JSON), priority order — for `while read`. Every
# configured source is active (empty slots are skipped upstream).
catalogEnabledSources() { catalogSourcesJson | jq -c '.[]'; }
# Human-readable listing for the CLI.
catalogSourceList() {
catalogSourcesJson | jq -r '.[] |
"[\(.idx)] \(.name) — \(.base)/\(.channel) (\(if .official then "official ✓" else "unverified" end))"'
}
# Fetch a THIRD-PARTY catalog index. Deliberately NOT verified against the
# official key and does NOT touch the anti-rollback serial — community sources
# are UNVERIFIED (their payloads are still sha256-pinned at add time by the pin
# carried in this index). Echoes the index JSON; non-zero on failure.
catalogFetchCommunityIndex() {
local base="$1" channel="$2" tmp idx json
base="${base%/}"
[[ -n "$base" && -n "$channel" ]] || return 1
command -v jq >/dev/null 2>&1 || return 1
[[ -n "$(_lpFetchTool)" ]] || return 1
tmp="$(mktemp -d)"; idx="$tmp/index.json"
if ! _lpDownload "$base/$channel/index.json" "$idx" 2>/dev/null; then rm -rf "$tmp"; return 1; fi
json="$(cat "$idx")"; rm -rf "$tmp"
jq empty <<<"$json" 2>/dev/null || return 1
printf '%s' "$json"
}
# --- Mutations: write the CFG_CATALOG_<N> vars via the config system ---------
# catalogSourceAdd <url> [channel] — into the first free slot.
catalogSourceAdd() {
local url="$1" channel="$2"
if [[ ! "$url" =~ ^https?://[^[:space:]/]+ ]]; then
isError "catalog: source URL must be http(s)://…"; return 1
fi
url="${url%/}"
local stored="$url"; [[ -n "$channel" && "$channel" != "stable" ]] && stored="$url|$channel"
if catalogSourcesJson | jq -e --arg b "$url" 'any(.[]; .base==$b)' >/dev/null 2>&1; then
isNotice "catalog: '$url' is already a source."; return 0
fi
local n var
for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do
var="CFG_CATALOG_${n}"
if [[ -z "${!var:-}" ]]; then
updateConfigOption "$var" "$stored" >/dev/null || { isError "catalog: failed to write $var."; return 1; }
isSuccessful "catalog: added source '$url' (slot $n)."
catalogRefresh
return 0
fi
done
isError "catalog: all $CATALOG_MAX_SLOTS catalog slots are full — remove one first."; return 1
}
# catalogSourceRemove <idx> — idx 1 = official (fixed); idx N+1 = CFG_CATALOG_N.
catalogSourceRemove() {
local idx="$1"
[[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: <idx> must be a number."; return 1; }
(( idx == 1 )) && { isError "catalog: source 1 is the official catalog and can't be removed."; return 1; }
local slot=$(( idx - 1 )) var="CFG_CATALOG_$(( idx - 1 ))"
(( slot < 1 || slot > CATALOG_MAX_SLOTS )) && { isError "catalog: no source with idx $idx."; return 1; }
[[ -z "${!var:-}" ]] && { isError "catalog: no source with idx $idx."; return 1; }
updateConfigOption "$var" "" >/dev/null || { isError "catalog: failed to clear $var."; return 1; }
isSuccessful "catalog: removed source $idx."
catalogRefresh
}
# Re-run the App Center browse scan so a source change reflects immediately.
catalogRefresh() {
declare -F webuiRegistryCatalogScan >/dev/null 2>&1 || \
source "${install_scripts_dir}webui/data/generators/apps/webui_registry_scan.sh" 2>/dev/null
declare -F webuiRegistryCatalogScan >/dev/null 2>&1 && webuiRegistryCatalogScan
}