A manager-run 'update apply' refreshes code/apps/WebUI but CANNOT rewrite the root-owned footprint (helpers/wrapper/uninstall/unit/sudoers) — that immutability is the de-sudo boundary. Previously a release that changed those would silently leave them stale. Make it explicit: - init.sh: footprint_version=1 constant, baked at install into /usr/local/lib/libreportal/.footprint_version (root:root 0644) by initRootHelpers. Bump it whenever a root component changes. - make_release.sh: publishes footprint_version in latest.json. - fetch.sh: lpInstalledFootprintVersion (marker) + lpReleaseLatestFootprint (manifest). - check_update.sh: 'update apply' REFUSES when the release's footprint_version exceeds the installed one, directing to a root re-install (which fetches + re-bakes everything atomically). No half-applied updates. - webui_system_update.sh: badge sets footprint_update_needed + clears can_update so the WebUI won't offer a one-click apply for a footprint-bumping release. - docs/DEVELOPMENT.md: the bump rule + the footprint exception explained. Verified: manifest carries footprint_version; drift decision correct both ways (no marker/older -> needs re-install; equal -> no drift). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
248 lines
8.9 KiB
Bash
Executable File
248 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# webuiRunUpdate — non-interactive LibrePortal update for the WebUI.
|
|
#
|
|
# Invoked as a task via `libreportal update apply` (see cli_update_commands.sh),
|
|
# so it runs under the task processor with stdin closed and
|
|
# LIBREPORTAL_NONINTERACTIVE=1. It must therefore never block on a prompt:
|
|
# every decision is resolved up front from config, and it bails out cleanly
|
|
# (non-zero) instead of asking a question.
|
|
#
|
|
# Flow: guard -> forced update check -> if behind, run the proven file update
|
|
# (gitPerformUpdate) -> redeploy the portal so the new WebUI is live ->
|
|
# regenerate WebUI data and refresh the out-of-date status.
|
|
webuiRunUpdate()
|
|
{
|
|
isHeader "LibrePortal Update"
|
|
|
|
sourceCheckFiles;
|
|
|
|
local install_mode="${CFG_INSTALL_MODE:-git}"
|
|
local git_updates="${CFG_GIT_UPDATES:-true}"
|
|
|
|
if [[ "$install_mode" == "local" ]]; then
|
|
isError "This is a local installation — updates are managed manually, nothing to pull."
|
|
return 1
|
|
fi
|
|
if [[ "$git_updates" != "true" ]]; then
|
|
isError "Updates are disabled (CFG_GIT_UPDATES). Enable them to update from the WebUI."
|
|
return 1
|
|
fi
|
|
|
|
# Release mode: version-compare the channel's latest against the local VERSION
|
|
# and, if newer, fetch + verify the new tarball and redeploy. No config backup
|
|
# dance — lpFetchRelease replaces only the install tree; configs/logs live in
|
|
# the separate system tree.
|
|
if [[ "$install_mode" == "release" ]]; then
|
|
webuiSystemUpdateCheck "force"
|
|
local cur lat
|
|
cur=$(tr -d ' \t\n\r' < "$script_dir/VERSION" 2>/dev/null)
|
|
lat=$(lpReleaseLatestVersion 2>/dev/null)
|
|
if [[ -z "$lat" ]]; then
|
|
isError "Could not reach the release server ($(lpReleaseBaseUrl))."
|
|
return 1
|
|
fi
|
|
if ! lpVersionGt "$lat" "$cur"; then
|
|
isSuccessful "LibrePortal is already up to date (v${cur})."
|
|
return 0
|
|
fi
|
|
# If the release bumps the root-owned footprint (helpers/wrapper/unit), a
|
|
# manager-run apply can't install it — those are deliberately not
|
|
# manager-writable. Require a root re-install, which fetches + re-bakes
|
|
# everything atomically.
|
|
local inst_fp rel_fp
|
|
inst_fp=$(lpInstalledFootprintVersion 2>/dev/null)
|
|
rel_fp=$(lpReleaseLatestFootprint 2>/dev/null)
|
|
if [[ -n "$rel_fp" && "${rel_fp:-0}" -gt "${inst_fp:-0}" ]]; then
|
|
isError "Update v${lat} changes system components — it must be applied as root, not from the WebUI."
|
|
isNotice "Run on the host: curl -fsSL $(lpReleaseBaseUrl)/install.sh | sudo bash"
|
|
isNotice "(or, from the install tree: sudo ${script_dir}/init.sh --unattended init ... )"
|
|
return 1
|
|
fi
|
|
isNotice "Update found — v${cur} → v${lat}. Fetching the verified release..."
|
|
lpFetchRelease "$lat" || { isError "Release fetch failed — install unchanged."; return 1; }
|
|
isNotice "Redeploying LibrePortal with the new version..."
|
|
dockerInstallApp "libreportal"
|
|
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
|
webuiSystemUpdateCheck "force"
|
|
isSuccessful "LibrePortal has been updated to v${lat}."
|
|
return 0
|
|
fi
|
|
|
|
# Credential guard — gitReset()/gitCheckGitDetails() would otherwise prompt
|
|
# for missing git details and hang the task forever. Fail fast with a clear
|
|
# message instead.
|
|
if [[ "$install_mode" == "git" ]]; then
|
|
if [[ -z "$CFG_GIT_USER" || "$CFG_GIT_USER" == "changeme" ]]; then
|
|
isError "Git credentials are not configured. Run the 'libreportal' command once on the host, then retry."
|
|
return 1
|
|
fi
|
|
if [[ "$CFG_GIT_USER" != "empty" && ( -z "$CFG_GIT_KEY" || "$CFG_GIT_KEY" == "changeme" ) ]]; then
|
|
isError "Git access token is not configured. Run the 'libreportal' command once on the host, then retry."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
cd "$script_dir" || { isError "Cannot access the install directory ($script_dir)."; return 1; }
|
|
runAsManager git config core.fileMode false
|
|
|
|
# Force a fresh fetch + status write so the decision below (and the badge)
|
|
# reflect reality right now, not a stale throttled snapshot.
|
|
webuiSystemUpdateCheck "force"
|
|
|
|
local branch behind
|
|
branch=$(runAsManager git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
[[ -z "$branch" || "$branch" == "HEAD" ]] && branch="main"
|
|
behind=$(runAsManager git -C "$script_dir" rev-list --count "HEAD..refs/remotes/origin/$branch" 2>/dev/null)
|
|
[[ -z "$behind" ]] && behind=0
|
|
|
|
if [[ "$behind" -eq 0 ]]; then
|
|
isSuccessful "LibrePortal is already up to date."
|
|
return 0
|
|
fi
|
|
|
|
isNotice "Update found — $behind commit(s) behind origin/$branch. Updating now..."
|
|
|
|
# Proven file-level update: back up configs/logs, re-clone, restore.
|
|
gitPerformUpdate;
|
|
|
|
# Redeploy the portal from the freshly pulled source so the new WebUI goes
|
|
# live. This is exactly what `libreportal app install libreportal` does, so
|
|
# it's already safe to run non-interactively. It restarts the container; the
|
|
# task processor runs on the host, so this task survives the restart.
|
|
isNotice "Redeploying LibrePortal with the new version..."
|
|
dockerInstallApp "libreportal"
|
|
|
|
# Regenerate WebUI data (new version, configs, etc.) and clear the
|
|
# out-of-date flag.
|
|
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
|
|
webuiSystemUpdateCheck "force"
|
|
|
|
isSuccessful "LibrePortal has been updated."
|
|
}
|
|
|
|
checkUpdates()
|
|
{
|
|
local param1="$1"
|
|
|
|
sourceCheckFiles;
|
|
|
|
# Skip Git updates if installation mode is local or Git updates are disabled
|
|
if [[ $CFG_INSTALL_MODE == "local" ]]; then
|
|
isNotice "Local installation detected - Git updates are disabled."
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
startLoad;
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if [[ $CFG_GIT_UPDATES == "true" ]]; then
|
|
isHeader "Checking for Updates"
|
|
|
|
# Ask user if they want to check for updates
|
|
while true; do
|
|
if [[ $CFG_GIT_UPDATES == "false" ]]; then
|
|
isQuestion "Would you like to check for updates? (y/n): "
|
|
read -p "" check_updates_choice
|
|
elif [[ $CFG_GIT_UPDATES == "true" ]]; then
|
|
check_updates_choice=y
|
|
fi
|
|
case $check_updates_choice in
|
|
[yY])
|
|
# DNS Query Test with Quad9
|
|
isNotice "Testing internet DNS, please wait..."
|
|
if command -v dig >/dev/null 2>&1; then
|
|
dns_check_cmd="dig +short +time=3 +tries=1 @9.9.9.9 quad9.net"
|
|
elif command -v getent >/dev/null 2>&1; then
|
|
dns_check_cmd="getent hosts quad9.net"
|
|
else
|
|
dns_check_cmd="ping -c 1 -W 3 9.9.9.9"
|
|
fi
|
|
if $dns_check_cmd >/dev/null 2>&1; then
|
|
isSuccessful "Internet DNS is working."
|
|
else
|
|
isError "Internet DNS is not working."
|
|
exit 1
|
|
fi
|
|
|
|
cd "$script_dir" || { isError " Cannot navigate to the repository directory"; exit 1; }
|
|
|
|
# Update Git to ignore changes in file permissions
|
|
runAsManager git config core.fileMode false
|
|
# Update Git with email address
|
|
runAsManager git config --global user.name "$CFG_INSTALL_NAME"
|
|
runAsManager git config --global user.email "noreply@${CFG_INSTALL_NAME,,}.libreportal.local"
|
|
|
|
# Check if there are edited (modified) files
|
|
if git status --porcelain | grep -q "^ M"; then
|
|
isNotice "There are uncommitted changes in the repository."
|
|
while true; do
|
|
isQuestion "Do you want to discard these changes and update the repository? (y/n): "
|
|
read -p "" customupdatesfound
|
|
case $customupdatesfound in
|
|
[yY])
|
|
remove_changes=true
|
|
gitCheckForUpdate;
|
|
gitCheckConfigs;
|
|
fixPermissionsBeforeStart "" "update";
|
|
sourceCheckFiles;
|
|
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
isSuccessful "Starting/Restarting LibrePortal"
|
|
startLoad;
|
|
fi
|
|
|
|
;;
|
|
[nN])
|
|
isNotice "Custom changes will be kept, continuing..."
|
|
remove_changes=false
|
|
gitCheckForUpdate;
|
|
gitCheckConfigs;
|
|
fixPermissionsBeforeStart "" "update";
|
|
sourceCheckFiles;
|
|
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
startLoad;
|
|
fi
|
|
|
|
;;
|
|
*)
|
|
isNotice "Please provide a valid input (y or n)."
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
# Make sure an update happens after custom code check
|
|
if [[ $update_done != "true" ]]; then
|
|
gitCheckForUpdate;
|
|
|
|
gitCheckConfigs;
|
|
fixPermissionsBeforeStart "" "update";
|
|
sourceCheckFiles;
|
|
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
isSuccessful "Starting/Restarting LibrePortal"
|
|
startLoad;
|
|
fi
|
|
fi
|
|
;;
|
|
[nN])
|
|
echo ""
|
|
isNotice "Skipping update check. Starting application..."
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
startLoad;
|
|
fi
|
|
;;
|
|
*)
|
|
isNotice "Please enter y or n."
|
|
;;
|
|
esac
|
|
done
|
|
else
|
|
if [[ $init_run_flag == "true" ]]; then
|
|
startLoad;
|
|
fi
|
|
fi
|
|
}
|