feat(install): ask where app data goes, like an OS installer
--containers-dir has existed for a while and nobody running the curl|bash
installer ever learned it existed, so a box with a 4 TB disk beside a
small system SSD quietly put everything on the SSD. The installer now
asks, once, in the shape an OS installer asks it.
Deliberately narrow, because a question you answer badly is worse than no
question:
* app data only. The control plane is ~20 MB and never grows; the thing
worth placing is the data. Offering all three roots would be three
questions nobody can answer at that moment.
* a SUBDIRECTORY on the chosen disk, never its mount point — that keeps
the "root only ever takes an empty directory" rule intact and leaves
whatever is already on the disk alone.
* candidates exclude anything on the same filesystem as /, since placing
data there gains nothing.
It stays out of the way: skipped when unattended, when --containers-dir
was passed, when there is no TTY, and when the scan finds nothing else —
a prompt with one possible answer is not a question. Self-contained
(findmnt only), since scripts/ is not necessarily loadable that early.
Verified all six paths under a pty: default, valid pick, out-of-range,
non-numeric, no TTY, unattended, explicit flag, and no candidates.
Docs updated: the installer prompt is shown, and the section that said
locations are fixed after install now explains what actually is fixed
(the three roots) versus what isn't (per-app placement, storage add, and
CFG_STORAGE_DEFAULT).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
49c463a996
commit
8d41b98497
@ -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)
|
### 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 |
|
| Flag (default) | Holds | Owner |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@ -57,8 +75,22 @@ Notes:
|
|||||||
<backups-dir>/ one folder per backup location
|
<backups-dir>/ one folder per backup location
|
||||||
```
|
```
|
||||||
|
|
||||||
The locations are chosen at install and fixed afterward (changing them is a
|
The three roots are chosen at install and fixed afterward — changing *them* is a
|
||||||
deliberate reinstall, not a setting — this is part of the security model).
|
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
|
## Update
|
||||||
|
|
||||||
|
|||||||
98
init.sh
98
init.sh
@ -188,6 +188,96 @@ libreportalDerivePaths() {
|
|||||||
}
|
}
|
||||||
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
|
# 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
|
# 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).
|
# 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
|
# by libreportalValidatePaths before any folder is created. Can also be set
|
||||||
# via the LP_*_DIR environment.
|
# via the LP_*_DIR environment.
|
||||||
--system-dir=*) LP_SYSTEM_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
--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++)) ;;
|
--backups-dir=*) LP_BACKUPS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||||
--manager-user=*) LP_MANAGER_USER="${!i#*=}"; ((init_shift_count++)) ;;
|
--manager-user=*) LP_MANAGER_USER="${!i#*=}"; ((init_shift_count++)) ;;
|
||||||
--allow-home) init_allow_home=true; ((init_shift_count++)) ;;
|
--allow-home) init_allow_home=true; ((init_shift_count++)) ;;
|
||||||
@ -1910,6 +2000,12 @@ if [[ $EUID -ne 0 ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
if [[ "$param1" == "init" ]]; then
|
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.
|
# Validate the chosen install roots before creating/baking anything.
|
||||||
libreportalValidatePaths
|
libreportalValidatePaths
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user