feat(install): custom, root-baked install locations (phase 3)
Make the three roots selectable at install and bake them into the CLI wrapper (the last /docker-hardcoded consumer). - init.sh: --system-dir= / --containers-dir= / --backups-dir= flags (=form keeps the single-token shift logic), plus --allow-home; LP_*_DIR env also honored. Re-derives paths after flag parsing. - libreportalValidatePaths (run only in the install flow): each root must be a non-root absolute path outside protected system trees; the three must not nest (except the legacy /docker compat layout); a containers/backups root inside a human home is refused unless --allow-home (rootless o+x traversal = privacy trade-off). The root helpers re-check at runtime (defence in depth). - CLI wrapper: a baked bootstrap (the same __ROOT__ placeholder mechanism as the helpers) exports LP_*_DIR and derives docker_dir/configs_dir/script_dir; every /docker literal in the heredoc now resolves from those at runtime. init.sh seds the placeholders into the root-owned wrapper after writing it. The scoped sudoers needs no change (it references only the fixed helper paths + system binaries, never a data root). Custom locations verified end-to-end: generate+bake the wrapper with /mnt/* roots → syntax OK, no placeholders left, paths resolve. Live box untouched (wrapper/helpers only change on reinstall). Phase 3b (external-drive guards) + phase 4 (verify) follow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
492e62b6d0
commit
38e531ed6e
110
init.sh
110
init.sh
@ -15,6 +15,15 @@
|
||||
# of tearing them down — so a following reinstall rebuilds
|
||||
# the WebUI image from cache (fast) instead of from
|
||||
# scratch. (No effect on install.)
|
||||
# --system-dir=PATH Install root for the control plane (configs/logs/install/
|
||||
# db). Default /libreportal-system. (Also: LP_SYSTEM_DIR.)
|
||||
# --containers-dir=PATH Root for live app data. Default /libreportal-containers.
|
||||
# (Also: LP_CONTAINERS_DIR.) Put on its own disk if wanted.
|
||||
# --backups-dir=PATH Root for backup repos. Default /libreportal-backups.
|
||||
# (Also: LP_BACKUPS_DIR.) Point at a separate disk/mount.
|
||||
# --allow-home Permit a containers/backups root inside /home/<user>
|
||||
# (needs rootless o+x traversal of that home — a privacy
|
||||
# trade-off; refused without this flag).
|
||||
#
|
||||
# Examples:
|
||||
# ./init.sh --random-password --local init
|
||||
@ -97,6 +106,7 @@ init_unattended_mode=false
|
||||
init_skip_os_update=false
|
||||
init_skip_prereqs=false
|
||||
init_skip_docker_images=false
|
||||
init_allow_home=false
|
||||
|
||||
install_param="init"
|
||||
sudo_user_name=libreportal
|
||||
@ -152,6 +162,46 @@ libreportalDerivePaths() {
|
||||
}
|
||||
libreportalDerivePaths
|
||||
|
||||
# Validate the chosen roots before anything is created/baked. Called from the
|
||||
# install flow only (NOT at source time — the CLI sources init.sh too). Aborts on
|
||||
# an unsafe choice; the root helpers also re-check at runtime (defence in depth).
|
||||
libreportalValidatePaths() {
|
||||
local pair name d
|
||||
for pair in "system:$LP_SYSTEM_DIR" "containers:$LP_CONTAINERS_DIR" "backups:$LP_BACKUPS_DIR"; do
|
||||
name="${pair%%:*}"; d="${pair#*:}"
|
||||
case "$d" in
|
||||
""|/) isError "The $name root must be a non-root absolute path (got '$d')."; exit 1 ;;
|
||||
/usr|/usr/*|/etc|/etc/*|/bin|/bin/*|/sbin|/sbin/*|/lib|/lib/*|/lib64|/lib64/*|/boot|/boot/*|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/run|/run/*|/root|/root/*)
|
||||
isError "Refusing $name root '$d' — inside a protected system path."; exit 1 ;;
|
||||
/*) ;; # absolute, allowed
|
||||
*) isError "The $name root must be an absolute path (got '$d')."; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
# The three must not nest inside one another — EXCEPT the legacy single-tree
|
||||
# compat layout (/docker + /docker/{containers,backups}), which nests by design.
|
||||
if [[ "$LP_SYSTEM_DIR" != "/docker" ]]; then
|
||||
local a b
|
||||
for a in "$LP_SYSTEM_DIR" "$LP_CONTAINERS_DIR" "$LP_BACKUPS_DIR"; do
|
||||
for b in "$LP_SYSTEM_DIR" "$LP_CONTAINERS_DIR" "$LP_BACKUPS_DIR"; do
|
||||
[[ "$a" == "$b" ]] && continue
|
||||
if [[ "$a" == "$b"/* ]]; then
|
||||
isError "Roots must not nest: '$a' is inside '$b'."; exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
# containers/backups inside a human home need the rootless user to traverse it
|
||||
# (o+x up the chain) — a privacy trade-off. Require an explicit opt-in.
|
||||
for pair in "containers:$LP_CONTAINERS_DIR" "backups:$LP_BACKUPS_DIR"; do
|
||||
name="${pair%%:*}"; d="${pair#*:}"
|
||||
if [[ "$d" == /home/* && "$init_allow_home" != "true" ]]; then
|
||||
isError "The $name root '$d' is inside a user home; the rootless container user would need o+x traversal of that home (a privacy trade-off)."
|
||||
isNotice "Re-run with --allow-home to accept, or choose a dedicated path/mount."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Parse flags
|
||||
init_shift_count=0
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
@ -183,9 +233,20 @@ for ((i=1; i<=$#; i++)); do
|
||||
init_skip_docker_images=true
|
||||
((init_shift_count++))
|
||||
;;
|
||||
# Relocatable roots (=form keeps the single-token shift logic). Validated
|
||||
# by libreportalValidatePaths before any folder is created. Can also be set
|
||||
# via the LP_*_DIR environment.
|
||||
--system-dir=*) LP_SYSTEM_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||
--containers-dir=*) LP_CONTAINERS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||
--backups-dir=*) LP_BACKUPS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||
--allow-home) init_allow_home=true; ((init_shift_count++)) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Re-derive after flags (flags/env set LP_*_DIR; libreportalDerivePaths keeps any
|
||||
# value already set and only fills the rest).
|
||||
libreportalDerivePaths
|
||||
|
||||
# Shift parsed flags to get positional parameters
|
||||
if [ $init_shift_count -gt 0 ]; then
|
||||
for ((i=1; i<=init_shift_count; i++)); do
|
||||
@ -1057,6 +1118,18 @@ if [ "$CURRENT_USER" != "$CHECK_USER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Relocatable roots (baked into this wrapper at install by init.sh) --------
|
||||
# An unbaked copy keeps the "__" sentinel, which no real absolute path has.
|
||||
# Exported so start.sh (and paths.sh) inherit them authoritatively.
|
||||
LP_SYSTEM_DIR="__SYSTEM_DIR__"; [[ "$LP_SYSTEM_DIR" == *"__"* ]] && LP_SYSTEM_DIR="/libreportal-system"
|
||||
LP_CONTAINERS_DIR="__CONTAINERS_DIR__"; [[ "$LP_CONTAINERS_DIR" == *"__"* ]] && LP_CONTAINERS_DIR="/libreportal-containers"
|
||||
LP_BACKUPS_DIR="__BACKUPS_DIR__"; [[ "$LP_BACKUPS_DIR" == *"__"* ]] && LP_BACKUPS_DIR="/libreportal-backups"
|
||||
export LP_SYSTEM_DIR LP_CONTAINERS_DIR LP_BACKUPS_DIR
|
||||
docker_dir="$LP_SYSTEM_DIR"
|
||||
configs_dir="$LP_SYSTEM_DIR/configs"
|
||||
script_dir="$LP_SYSTEM_DIR/install"
|
||||
install_scripts_dir="$script_dir/scripts"
|
||||
|
||||
command1="${1:-empty}"
|
||||
command2="${2:-empty}"
|
||||
command3="${3:-empty}"
|
||||
@ -1082,7 +1155,7 @@ reset_git_config() {
|
||||
# Helper function to load config files in the libreportal command
|
||||
commandReloadConfigs() {
|
||||
# Load new structure config files only
|
||||
for category_dir in /docker/configs/*; do
|
||||
for category_dir in "$configs_dir"/*; do
|
||||
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
||||
for config_file in "$category_dir"/*; do
|
||||
local should_load=true
|
||||
@ -1104,7 +1177,7 @@ commandReloadConfigs() {
|
||||
commandUpdateConfigOption() {
|
||||
local config_option="$1"
|
||||
local config_value="$2"
|
||||
for category_dir in /docker/configs/*; do
|
||||
for category_dir in "$configs_dir"/*; do
|
||||
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
||||
for config_file in "$category_dir"/*; do
|
||||
if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then
|
||||
@ -1254,8 +1327,8 @@ setup_repo() {
|
||||
}
|
||||
|
||||
sync_configs_from_install() {
|
||||
local src="/docker/install/configs"
|
||||
local dst="/docker/configs"
|
||||
local src="$script_dir/configs"
|
||||
local dst="$configs_dir"
|
||||
if [ ! -d "$src" ]; then
|
||||
echo "ERROR: $src missing — clone broken."
|
||||
return 1
|
||||
@ -1266,7 +1339,7 @@ sync_configs_from_install() {
|
||||
echo "ERROR: Failed to sync configs from $src to $dst."
|
||||
return 1
|
||||
fi
|
||||
chown -R libreportal:libreportal "$dst"
|
||||
chown -R "$CHECK_USER:$CHECK_USER" "$dst"
|
||||
if [ ! -f "$dst/general/general_install" ]; then
|
||||
echo "ERROR: $dst/general/general_install missing after sync."
|
||||
return 1
|
||||
@ -1279,12 +1352,12 @@ sync_configs_from_install() {
|
||||
}
|
||||
|
||||
clone_repo() {
|
||||
rm -rf /docker/install
|
||||
rm -rf "$script_dir"
|
||||
local clone_url
|
||||
if [ "$CFG_GIT_USER" != "empty" ]; then
|
||||
for clone_url in "$AUTH_HTTPS_REPO_URL" "$AUTH_HTTP_REPO_URL"; do
|
||||
if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then
|
||||
sudo cp -f /docker/install/init.sh /root/
|
||||
if git clone -q "$clone_url" "$script_dir" 2>/dev/null; then
|
||||
sudo cp -f "$script_dir/init.sh" /root/
|
||||
sync_configs_from_install || return 1
|
||||
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
||||
return 0
|
||||
@ -1294,8 +1367,8 @@ clone_repo() {
|
||||
return 1
|
||||
fi
|
||||
for clone_url in "https://${CLEAN_GIT_URL}.git" "http://${CLEAN_GIT_URL}.git"; do
|
||||
if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then
|
||||
sudo cp -f /docker/install/init.sh /root/
|
||||
if git clone -q "$clone_url" "$script_dir" 2>/dev/null; then
|
||||
sudo cp -f "$script_dir/init.sh" /root/
|
||||
sync_configs_from_install || return 1
|
||||
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
||||
return 0
|
||||
@ -1316,18 +1389,24 @@ clone_and_install() {
|
||||
clone_repo;
|
||||
}
|
||||
|
||||
cd /docker/
|
||||
cd "$docker_dir/" 2>/dev/null || cd /
|
||||
if [[ $command1 == "reset" ]]; then
|
||||
clone_and_install
|
||||
elif [ -f "/docker/install/start.sh" ]; then
|
||||
chmod 0755 /docker/install/*
|
||||
cd /docker/install
|
||||
elif [ -f "$script_dir/start.sh" ]; then
|
||||
chmod 0755 "$script_dir"/*
|
||||
cd "$script_dir"
|
||||
./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9"
|
||||
else
|
||||
clone_and_install
|
||||
fi
|
||||
# LibrePortal Command End
|
||||
EOF
|
||||
# Bake the three roots into the (root-owned) wrapper, same as the helpers.
|
||||
sudo sed -i \
|
||||
-e "s#__SYSTEM_DIR__#${LP_SYSTEM_DIR}#g" \
|
||||
-e "s#__CONTAINERS_DIR__#${LP_CONTAINERS_DIR}#g" \
|
||||
-e "s#__BACKUPS_DIR__#${LP_BACKUPS_DIR}#g" \
|
||||
"$command_script"
|
||||
sudo chmod +x $command_script
|
||||
sudo chown root:root $command_script
|
||||
# Put it on $PATH via a symlink (replaces any older real file at this path).
|
||||
@ -1546,6 +1625,9 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
else
|
||||
if [[ "$param1" == "init" ]]; then
|
||||
# Validate the chosen install roots before creating/baking anything.
|
||||
libreportalValidatePaths
|
||||
|
||||
# Always check existing config first
|
||||
initCheckConfigs
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user