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>
127 lines
5.9 KiB
Bash
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
|
|
}
|