#!/bin/bash # # Catalog sources — the ordered list of app-catalog URLs the App Center browses. # --------------------------------------------------------------------------- # Source #1 is ALWAYS the official LibrePortal catalog, synthesized live from # CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL so it can't be edited or removed and # always tracks the release host. Extra (third-party) sources are stored as a # small JSON array in their OWN file under the live configs dir and are treated # as UNVERIFIED (the operator added the URL themselves). # # Priority = list order: the official catalog is first, then extras in the order # they were added. When the same app slug is offered by more than one source the # highest-priority source that has it is the default (the Add dialog can override). # # IMPORTANT SCOPE: this list governs APP BROWSE + APP ADD only. LibrePortal's own # updates and hotfixes still resolve from the official CFG_RELEASE_BASE_URL alone # (lpFetchIndex is untouched) — a third-party catalog can never become a channel # for system updates. # Its own file, next to the other live config (manager-owned control plane). catalogSourcesFile() { echo "${configs_dir%/}/catalog/sources.json"; } # The extra (non-official) sources as a JSON array; [] when none/absent/invalid. catalogExtraSourcesJson() { local f; f="$(catalogSourcesFile)" command -v jq >/dev/null 2>&1 || { echo '[]'; return 0; } if [[ -s "$f" ]] && jq empty "$f" 2>/dev/null; then jq -c 'if type=="array" then . else [] end' "$f" 2>/dev/null || echo '[]' else echo '[]' fi } # The full ordered source list as a JSON array. Each entry: # { idx, name, base, channel, enabled, official, trust } # idx 1 = official (synthesized from CFG); 2..N = extras in file order. 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, enabled: true, official: true, trust: "official" } ] + ( $extras | to_entries | map( { idx: (.key + 2), name: (.value.name // .value.base), base: (.value.base // ""), channel: (.value.channel // "stable"), enabled: (if (.value | has("enabled")) then (.value.enabled == true) else true end), official: false, trust: "community" } ) )' } # One ENABLED source per line (compact JSON), in priority order — for `while read`. catalogEnabledSources() { catalogSourcesJson | jq -c '.[] | select(.enabled)'; } # Human-readable listing for the CLI. catalogSourceList() { catalogSourcesJson | jq -r '.[] | "[\(.idx)] \(.name) — \(.base)/\(.channel) " + "(\(if .official then "official ✓" else "unverified" end), " + "\(if .enabled then "on" else "off" end))"' } # Fetch a THIRD-PARTY catalog index. Deliberately does NOT verify against the # official key and does NOT touch the shared anti-rollback serial — community # sources are UNVERIFIED (their payloads are still sha256-pinned at add time by # the pin carried in this very 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 (manager-owned file; write via the manager funnel) ----------- # catalogSourceAdd [name] [channel] catalogSourceAdd() { local url="$1" name="$2" channel="${3:-stable}" if [[ ! "$url" =~ ^https?://[^[:space:]/]+ ]]; then isError "catalog: source URL must be http(s)://…"; return 1 fi url="${url%/}" [[ -z "$name" ]] && name="$url" # Refuse a duplicate base+channel (including the official one). if catalogSourcesJson | jq -e --arg b "$url" --arg c "$channel" 'any(.[]; .base==$b and .channel==$c)' >/dev/null 2>&1; then isNotice "catalog: '$url' ($channel) is already a source."; return 0 fi local f dir extras new f="$(catalogSourcesFile)"; dir="$(dirname "$f")" runInstallOp mkdir -p "$dir" 2>/dev/null || true extras="$(catalogExtraSourcesJson)" new="$(jq -c --arg n "$name" --arg b "$url" --arg c "$channel" \ '. + [ { name: $n, base: $b, channel: $c, enabled: true } ]' <<<"$extras")" || { isError "catalog: failed to build the new source list."; return 1; } printf '%s\n' "$new" | runInstallWrite "$f" isSuccessful "catalog: added source '$name' ($url/$channel)." } # catalogSourceRemove (idx 1 = official, cannot be removed) catalogSourceRemove() { local idx="$1" [[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: must be a number."; return 1; } (( idx == 1 )) && { isError "catalog: source 1 is the official catalog and can't be removed."; return 1; } local f extras count pos new f="$(catalogSourcesFile)"; extras="$(catalogExtraSourcesJson)" count="$(jq 'length' <<<"$extras")" pos=$(( idx - 2 )) if (( pos < 0 || pos >= count )); then isError "catalog: no source with idx $idx."; return 1; fi new="$(jq -c --argjson i "$pos" 'del(.[$i])' <<<"$extras")" printf '%s\n' "$new" | runInstallWrite "$f" isSuccessful "catalog: removed source $idx." } # catalogSourceToggle catalogSourceToggle() { local idx="$1" on="$2" [[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: must be a number."; return 1; } (( idx == 1 )) && { isError "catalog: the official catalog is always enabled."; return 1; } [[ "$on" == "true" || "$on" == "false" ]] || { isError "catalog: toggle value must be true|false."; return 1; } local f extras count pos new f="$(catalogSourcesFile)"; extras="$(catalogExtraSourcesJson)" count="$(jq 'length' <<<"$extras")" pos=$(( idx - 2 )) if (( pos < 0 || pos >= count )); then isError "catalog: no source with idx $idx."; return 1; fi new="$(jq -c --argjson i "$pos" --argjson v "$on" '.[$i].enabled=$v' <<<"$extras")" printf '%s\n' "$new" | runInstallWrite "$f" isSuccessful "catalog: source $idx $([[ "$on" == "true" ]] && echo enabled || echo disabled)." } # Re-run the App Center browse scan (so a source change reflects immediately # instead of waiting for the next `updater check`). 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 }