diff --git a/docs/guide/install-and-use.md b/docs/guide/install-and-use.md index 52b8794..4330d39 100644 --- a/docs/guide/install-and-use.md +++ b/docs/guide/install-and-use.md @@ -26,7 +26,25 @@ login), installs LibrePortal, and prints the WebUI address + a generated passwor ### Put data where you want it (separate disks, external drives) -LibrePortal uses **three independent roots**, each can be its own path/disk: +**The installer just asks.** If it finds a second drive, it offers it for app +data before installing anything: + +``` +Where should app data live? + + 1) This disk (default) 911.9G 808.4G free + 2) /mnt/bigdisk 3.6T 3.6T free + + LibrePortal itself stays on this disk either way — only app data moves. + +Choose [1]: +``` + +It picks a subdirectory on the drive you choose, never the mount point itself, +and skips the question entirely when there is nothing else to choose, when you +passed `--containers-dir`, or when running unattended. + +The three roots below are still there for scripted installs: | Flag (default) | Holds | Owner | |---|---|---| @@ -57,8 +75,22 @@ Notes: / one folder per backup location ``` -The locations are chosen at install and fixed afterward (changing them is a -deliberate reinstall, not a setting — this is part of the security model). +The three roots are chosen at install and fixed afterward — changing *them* is a +deliberate reinstall, not a setting, and that is part of the security model. + +What is **not** fixed is where each app's data lives. You can register extra +drives at any time and place apps on them individually: + +```bash +libreportal storage scan # what else could hold app data +libreportal storage add /mnt/bigdisk # register it (must be an empty directory) +libreportal app move nextcloud bigdisk +``` + +New apps follow `CFG_STORAGE_DEFAULT` (General → Basic), so one setting sends +everything to the big disk without touching each app. An app whose drive is not +mounted refuses to start rather than being rebuilt empty on the bare mount +point. ## Update diff --git a/init.sh b/init.sh index 0b4b2f7..6c2cea2 100755 --- a/init.sh +++ b/init.sh @@ -188,6 +188,96 @@ libreportalDerivePaths() { } libreportalDerivePaths +# Interactive disk picker for app data. +# +# The --containers-dir flag has existed for a while, but someone running the +# curl|bash installer never learns it exists — so a box with a 4 TB disk sitting +# next to a small system SSD quietly put everything on the SSD. This asks, once, +# in the shape an OS installer asks it. +# +# Deliberately narrow: +# * app data only. The control plane is ~20 MB and never grows; the thing +# worth placing is the data. Offering three roots would be three questions +# to answer badly. +# * skipped entirely when unattended, when --containers-dir was passed, when +# there is no TTY, or when there is nothing else to choose. A prompt with +# one answer is not a question. +# * a SUBDIRECTORY on the chosen disk, never its mount point — that keeps the +# "root only ever takes an empty directory" rule intact and leaves anything +# already on the disk alone. +# +# Self-contained (findmnt only), like the rest of init.sh: scripts/ is not +# necessarily loadable this early. +initPickContainersDir() +{ + [[ "$init_unattended_mode" == true ]] && return 0 + [[ -n "${LP_CONTAINERS_DIR_EXPLICIT:-}" ]] && return 0 + [[ -t 0 && -t 1 ]] || return 0 + command -v findmnt >/dev/null 2>&1 || return 0 + + local sys_dev; sys_dev=$(stat -c '%d' -- / 2>/dev/null) + local -a paths=() labels=() + local line kv val target source fstype size avail dev + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + target=""; source=""; fstype=""; size=""; avail="" + for kv in TARGET SOURCE FSTYPE SIZE AVAIL; do + val="${line#*${kv}=\"}" + [[ "$val" == "$line" ]] && continue + val="${val%%\"*}" + case "$kv" in + TARGET) target="$val" ;; SOURCE) source="$val" ;; + FSTYPE) fstype="$val" ;; SIZE) size="$val" ;; AVAIL) avail="$val" ;; + esac + done + [[ -z "$target" ]] && continue + case "$target" in + /|/boot|/boot/*|/efi|/proc*|/sys*|/dev*|/run*|/snap*|/var/snap/*|/tmp) continue ;; + esac + case "$fstype" in + squashfs|overlay|overlay2|aufs|tmpfs|devtmpfs|ramfs|iso9660|udf|vfat|exfat|ntfs|ntfs3|msdos|fuseblk|"") continue ;; + esac + dev=$(stat -c '%d' -- "$target" 2>/dev/null) + [[ -n "$dev" && "$dev" == "$sys_dev" ]] && continue # same disk as / — no gain + paths+=("$target") + labels+=("$(printf '%-24s %-8s %s free' "$target" "${size:-?}" "${avail:-?}")") + done < <(findmnt -Pno TARGET,SOURCE,FSTYPE,SIZE,AVAIL 2>/dev/null) + + (( ${#paths[@]} )) || return 0 + + local root_size root_avail + root_size=$(findmnt -no SIZE --target / 2>/dev/null | tail -1) + root_avail=$(findmnt -no AVAIL --target / 2>/dev/null | tail -1) + + echo "" + isHeader "Where should app data live?" + echo "" + printf ' %s %-24s %-8s %s free\n' "1)" "This disk (default)" "${root_size:-?}" "${root_avail:-?}" + local i + for i in "${!paths[@]}"; do + printf ' %s %s\n' "$((i + 2)))" "${labels[$i]}" + done + echo "" + echo " LibrePortal itself stays on this disk either way — only app data moves." + echo "" + + local choice="" + isQuestion "Choose [1]:" + read -r choice + echo "" + [[ -z "$choice" || "$choice" == "1" ]] && return 0 + if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 2 || choice > ${#paths[@]} + 1 )); then + isNotice "Not a listed option — using this disk." + return 0 + fi + + local chosen="${paths[$((choice - 2))]}" + LP_CONTAINERS_DIR="${chosen%/}/libreportal-containers" + isSuccessful "App data will live in $LP_CONTAINERS_DIR" + return 0 +} + # 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). @@ -276,7 +366,7 @@ for ((i=1; i<=$#; i++)); do # 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++)) ;; + --containers-dir=*) LP_CONTAINERS_DIR="${!i#*=}"; LP_CONTAINERS_DIR_EXPLICIT=1; ((init_shift_count++)) ;; --backups-dir=*) LP_BACKUPS_DIR="${!i#*=}"; ((init_shift_count++)) ;; --manager-user=*) LP_MANAGER_USER="${!i#*=}"; ((init_shift_count++)) ;; --allow-home) init_allow_home=true; ((init_shift_count++)) ;; @@ -1910,6 +2000,12 @@ if [[ $EUID -ne 0 ]]; then exit 1 else if [[ "$param1" == "init" ]]; then + # Ask where app data goes before anything is validated or created. + # No-op when unattended, when --containers-dir was passed, or when + # there is only one possible answer. + initPickContainersDir + libreportalDerivePaths + # Validate the chosen install roots before creating/baking anything. libreportalValidatePaths