feat(storage): registry helper, fitness checks, CLI and the Disks view
Phase 1 of docs/roadmap/storage-locations.md — locations can now exist. Nothing places an app on one yet; that is phase 2. libreportal-storage is the only writer of the root-owned registry, and its admission rules are what make adding a location safe: absolute and canonical (a symlinked path is refused), outside the protected system set, non-nesting with any known root in either direction, and EMPTY — or already carrying our marker, which is the adopt case for a drive that already holds app data. Root only ever chowns an empty directory, so acceptance cannot hand away anything that existed. The parent must also not be manager-writable, which is what closes the validate-then-chown race; /mnt and /srv qualify, a path inside the manager's home does not. The fitness checks answer a different question — "will app data actually work here" — and grade rather than refuse. Only checks 1-5 (filesystem type, mount options, ownership, sub-UID range, write/read-back) can block. Reboot persistence and removability warn, because both describe supported setups and start-up is already gated by the marker test. The ownership probe had to move into the root helper. For a candidate the directory is not ours yet — a fresh /mnt/disk is root-owned 0755 — so an unprivileged probe could only ever report "cannot create a directory here", which says nothing about the filesystem. Verified against a real loopback ext4: it now reports ownership, sub-UID and read-back cleanly. The Disks view unions the registry with attached hardware, registry first. Verified on the live box that pulling a drive leaves its row in place as not-attached, naming the app stranded on it, rather than the row silently disappearing at exactly the moment someone needs it. Also registers storage_scripts with both loaders, adds the CLI category (auto-dispatched by cli_initialize.sh), and bumps footprint_version to 6 for the new root helper and the widened sudoers allowlist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ac4c11b5e9
commit
0de7f40f5d
111
scripts/cli/commands/storage/cli_storage_commands.sh
Normal file
111
scripts/cli/commands/storage/cli_storage_commands.sh
Normal file
@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Dispatched automatically by cli_initialize.sh (category -> cliHandleStorageCommands).
|
||||
#
|
||||
# Mirrors `libreportal backup location …`. The mutating verbs go through the
|
||||
# root-owned libreportal-storage helper, which is the only writer of the storage
|
||||
# registry — see scripts/system/libreportal-storage.
|
||||
|
||||
cliHandleStorageCommands()
|
||||
{
|
||||
local action="$initial_command2"
|
||||
local arg1="$initial_command3"
|
||||
local arg2="$initial_command4"
|
||||
|
||||
case "$action" in
|
||||
""|help)
|
||||
cliShowStorageHelp
|
||||
;;
|
||||
|
||||
list)
|
||||
storageList
|
||||
;;
|
||||
|
||||
scan)
|
||||
storageScan
|
||||
;;
|
||||
|
||||
add)
|
||||
if [[ -z "$arg1" ]]; then
|
||||
isNotice "Usage: storage add <path> [name]"
|
||||
cliShowStorageHelp
|
||||
return 1
|
||||
fi
|
||||
storageAdd "$arg1" "$arg2" >/dev/null
|
||||
;;
|
||||
|
||||
remove)
|
||||
if [[ -z "$arg1" ]]; then
|
||||
isNotice "Usage: storage remove <id|path>"
|
||||
cliShowStorageHelp
|
||||
return 1
|
||||
fi
|
||||
storageRemove "$arg1"
|
||||
;;
|
||||
|
||||
check)
|
||||
local sev check msg rc=0
|
||||
if [[ -n "$arg1" ]]; then
|
||||
isHeader "Checking storage location $arg1"
|
||||
while IFS=$'\t' read -r sev check msg; do
|
||||
[[ -z "$sev" ]] && continue
|
||||
case "$sev" in
|
||||
refuse) isError "$check: $msg" ;;
|
||||
warn) isNotice "$check: $msg" ;;
|
||||
*) isNotice "$check: $msg" ;;
|
||||
esac
|
||||
done < <(storageCheckLocation "$arg1")
|
||||
else
|
||||
local id state path
|
||||
while IFS=$'\t' read -r id state path; do
|
||||
[[ -z "$id" ]] && continue
|
||||
isHeader "Location $id — $path ($state)"
|
||||
while IFS=$'\t' read -r sev check msg; do
|
||||
[[ -z "$sev" ]] && continue
|
||||
case "$sev" in
|
||||
refuse) isError "$check: $msg" ;;
|
||||
warn) isNotice "$check: $msg" ;;
|
||||
*) isNotice "$check: $msg" ;;
|
||||
esac
|
||||
done < <(storageCheckLocation "$id")
|
||||
done < <(runStorage verify 2>/dev/null)
|
||||
[[ -z "$(runStorage verify 2>/dev/null)" ]] && \
|
||||
isNotice "No storage locations registered — everything lives on $(primaryRoot)."
|
||||
fi
|
||||
return $rc
|
||||
;;
|
||||
|
||||
apps)
|
||||
local root
|
||||
if [[ -n "$arg1" ]]; then
|
||||
root=$(storageLocationPath "$arg1") || { isError "No such location: $arg1"; return 1; }
|
||||
isHeader "Apps on $(storageLocationName "$root") ($root)"
|
||||
storageAppsOnRoot "$root"
|
||||
else
|
||||
local id state path
|
||||
isHeader "Apps by location"
|
||||
echo "default ($(primaryRoot)):"
|
||||
storageAppsOnRoot "$(primaryRoot)" | sed 's/^/ /'
|
||||
while IFS=$'\t' read -r id state path; do
|
||||
[[ -z "$id" ]] && continue
|
||||
echo "$(storageLocationName "$path") ($path) [$state]:"
|
||||
if [[ "$state" == "ok" ]]; then
|
||||
storageAppsOnRoot "$path" | sed 's/^/ /'
|
||||
else
|
||||
echo " (drive not mounted — apps here will not start)"
|
||||
fi
|
||||
done < <(runStorage verify 2>/dev/null)
|
||||
fi
|
||||
;;
|
||||
|
||||
disks)
|
||||
storageDisks
|
||||
;;
|
||||
|
||||
*)
|
||||
isNotice "Invalid storage action: $action"
|
||||
cliShowStorageHelp
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
33
scripts/cli/commands/storage/cli_storage_header.sh
Normal file
33
scripts/cli/commands/storage/cli_storage_header.sh
Normal file
@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
cliShowStorageHelp()
|
||||
{
|
||||
isHeader "LibrePortal Storage Commands"
|
||||
echo "storage list"
|
||||
echo " Every registered storage location: name, path, mount state, apps, free space."
|
||||
echo ""
|
||||
echo "storage scan"
|
||||
echo " Filesystems on this host that could hold app data, with a verdict for each."
|
||||
echo ""
|
||||
echo "storage add <path> [name]"
|
||||
echo " Register a location. The directory must be EMPTY (or already be a"
|
||||
echo " LibrePortal location), and must not sit inside a protected system path."
|
||||
echo " Root only ever takes ownership of an empty directory — that is what"
|
||||
echo " makes adding one safe."
|
||||
echo ""
|
||||
echo "storage remove <id|path>"
|
||||
echo " Unregister a location. Refused while any app still lives on it, and"
|
||||
echo " refused when its drive is not mounted (that would orphan those apps)."
|
||||
echo ""
|
||||
echo "storage check [id]"
|
||||
echo " Re-run the fitness checks for one location, or all of them."
|
||||
echo ""
|
||||
echo "storage apps [id]"
|
||||
echo " Which apps live on a location."
|
||||
echo ""
|
||||
echo "storage disks"
|
||||
echo " One row per filesystem, showing what LibrePortal uses it for."
|
||||
echo ""
|
||||
isNotice "A location whose drive is absent is reported, never silently skipped:"
|
||||
isNotice "apps stored there refuse to start rather than being rebuilt empty."
|
||||
}
|
||||
@ -23,6 +23,7 @@ files_libreportal_app=(
|
||||
"${source_scripts[@]}"
|
||||
"${ssh_scripts[@]}"
|
||||
"${start_scripts[@]}"
|
||||
"${storage_scripts[@]}"
|
||||
"${task_scripts[@]}"
|
||||
"${update_scripts[@]}"
|
||||
"${validation_scripts[@]}"
|
||||
|
||||
@ -47,6 +47,8 @@ cli_scripts=(
|
||||
"cli/commands/setup/cli_setup_header.sh"
|
||||
"cli/commands/ssh/cli_ssh_commands.sh"
|
||||
"cli/commands/ssh/cli_ssh_header.sh"
|
||||
"cli/commands/storage/cli_storage_commands.sh"
|
||||
"cli/commands/storage/cli_storage_header.sh"
|
||||
"cli/commands/system/cli_system_commands.sh"
|
||||
"cli/commands/system/cli_system_header.sh"
|
||||
"cli/commands/update/cli_update_commands.sh"
|
||||
|
||||
@ -10,6 +10,7 @@ crontab_scripts=(
|
||||
"crontab/crontab_install.sh"
|
||||
"crontab/crontab_refresh.sh"
|
||||
"crontab/crontab_setup.sh"
|
||||
"crontab/system/crontab_boot_app_reconcile.sh"
|
||||
"crontab/system/crontab_setup_system_info_updater.sh"
|
||||
|
||||
)
|
||||
|
||||
@ -29,6 +29,7 @@ source_scripts=(
|
||||
"source/files/arrays/files_source.sh"
|
||||
"source/files/arrays/files_ssh.sh"
|
||||
"source/files/arrays/files_start.sh"
|
||||
"source/files/arrays/files_storage.sh"
|
||||
"source/files/arrays/files_task.sh"
|
||||
"source/files/arrays/files_update.sh"
|
||||
"source/files/arrays/files_validation.sh"
|
||||
|
||||
12
scripts/source/files/arrays/files_storage.sh
Normal file
12
scripts/source/files/arrays/files_storage.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This file is auto-generated by generate_arrays.sh
|
||||
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
|
||||
|
||||
storage_scripts=(
|
||||
"storage/storage_checks.sh"
|
||||
"storage/storage_disks.sh"
|
||||
"storage/storage_locations.sh"
|
||||
"storage/storage_scan.sh"
|
||||
|
||||
)
|
||||
@ -338,6 +338,7 @@ declare -gA LP_FN_MAP=(
|
||||
[cliHandleRestoreCommands]="cli/commands/restore/cli_restore_commands.sh"
|
||||
[cliHandleSetupCommands]="cli/commands/setup/cli_setup_commands.sh"
|
||||
[cliHandleSshCommands]="cli/commands/ssh/cli_ssh_commands.sh"
|
||||
[cliHandleStorageCommands]="cli/commands/storage/cli_storage_commands.sh"
|
||||
[cliHandleSystemCommands]="cli/commands/system/cli_system_commands.sh"
|
||||
[cliHandleUpdateCommands]="cli/commands/update/cli_update_commands.sh"
|
||||
[cliHandleUpdaterCommands]="cli/commands/updater/cli_updater_commands.sh"
|
||||
@ -364,6 +365,7 @@ declare -gA LP_FN_MAP=(
|
||||
[cliShowRestoreHelp]="cli/commands/restore/cli_restore_header.sh"
|
||||
[cliShowSetupHelp]="cli/commands/setup/cli_setup_header.sh"
|
||||
[cliShowSshHelp]="cli/commands/ssh/cli_ssh_header.sh"
|
||||
[cliShowStorageHelp]="cli/commands/storage/cli_storage_header.sh"
|
||||
[cliShowSystemHelp]="cli/commands/system/cli_system_header.sh"
|
||||
[cliShowUpdateHelp]="cli/commands/update/cli_update_header.sh"
|
||||
[cliShowUpdaterHelp]="cli/commands/updater/cli_updater_header.sh"
|
||||
@ -928,6 +930,7 @@ declare -gA LP_FN_MAP=(
|
||||
[_runRootHelper]="docker/command/run_privileged.sh"
|
||||
[runSocket]="docker/command/run_privileged.sh"
|
||||
[runSshAccess]="docker/command/run_privileged.sh"
|
||||
[runStorage]="docker/command/run_privileged.sh"
|
||||
[runSvc]="docker/command/run_privileged.sh"
|
||||
[runSystem]="docker/command/run_privileged.sh"
|
||||
[runTask]="task/crontab_task_processor.sh"
|
||||
@ -959,6 +962,7 @@ declare -gA LP_FN_MAP=(
|
||||
[setupWizardTerminal]="checks/first_install.sh"
|
||||
[showInstructions]="menu/message/instructions.sh"
|
||||
[sourceBackupLocations]="backup/locations/location_loader.sh"
|
||||
[sourceStorageLocations]="storage/storage_locations.sh"
|
||||
[sshRemote]="network/ssh/ssh.sh"
|
||||
[stalwart_apply_port_access]="stalwart/scripts/stalwart_install_hooks.sh"
|
||||
[stalwart_cli]="stalwart/scripts/stalwart_install_hooks.sh"
|
||||
@ -999,6 +1003,34 @@ declare -gA LP_FN_MAP=(
|
||||
[_stoatWriteSecrets]="stoat/scripts/stoat_install_hooks.sh"
|
||||
[_stoatWriteUrlFiles]="stoat/scripts/stoat_install_hooks.sh"
|
||||
[stopCrowdsec]="crowdsec/crowdsec.sh"
|
||||
[storageAdd]="storage/storage_locations.sh"
|
||||
[storageAppsOnRoot]="storage/storage_locations.sh"
|
||||
[_storageCheckDistinct]="storage/storage_checks.sh"
|
||||
[_storageCheckEncryption]="storage/storage_checks.sh"
|
||||
[_storageCheckFsType]="storage/storage_checks.sh"
|
||||
[storageCheckLocation]="storage/storage_checks.sh"
|
||||
[_storageCheckMountOpts]="storage/storage_checks.sh"
|
||||
[_storageCheckOwnership]="storage/storage_checks.sh"
|
||||
[storageCheckPath]="storage/storage_checks.sh"
|
||||
[_storageCheckPersistence]="storage/storage_checks.sh"
|
||||
[_storageCheckRemovable]="storage/storage_checks.sh"
|
||||
[_storageCheckSharedWithBackup]="storage/storage_checks.sh"
|
||||
[_storageCheckSpace]="storage/storage_checks.sh"
|
||||
[storageDisks]="storage/storage_disks.sh"
|
||||
[storageDisksData]="storage/storage_disks.sh"
|
||||
[_storageEmit]="storage/storage_checks.sh"
|
||||
[_storageFreeHuman]="storage/storage_locations.sh"
|
||||
[storageList]="storage/storage_locations.sh"
|
||||
[storageLocationConfig]="storage/storage_locations.sh"
|
||||
[storageLocationDir]="storage/storage_locations.sh"
|
||||
[storageLocationsDir]="storage/storage_locations.sh"
|
||||
[_storageProbeDir]="storage/storage_checks.sh"
|
||||
[_storageRefreshWebui]="storage/storage_locations.sh"
|
||||
[storageRemove]="storage/storage_locations.sh"
|
||||
[storageScan]="storage/storage_scan.sh"
|
||||
[storageScanCandidates]="storage/storage_scan.sh"
|
||||
[_storageSkipTarget]="storage/storage_scan.sh"
|
||||
[_storageWriteLocationConfig]="storage/storage_locations.sh"
|
||||
[switchMigrateBackupApps]="docker/type_switcher/swap_docker_type.sh"
|
||||
[switchMigrateRestoreApps]="docker/type_switcher/swap_docker_type.sh"
|
||||
[tagsManagerGetTagContent]="config/tags/manager/tags_manager_content.sh"
|
||||
@ -1522,6 +1554,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[cliHandleRestoreCommands]="scripts"
|
||||
[cliHandleSetupCommands]="scripts"
|
||||
[cliHandleSshCommands]="scripts"
|
||||
[cliHandleStorageCommands]="scripts"
|
||||
[cliHandleSystemCommands]="scripts"
|
||||
[cliHandleUpdateCommands]="scripts"
|
||||
[cliHandleUpdaterCommands]="scripts"
|
||||
@ -1548,6 +1581,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[cliShowRestoreHelp]="scripts"
|
||||
[cliShowSetupHelp]="scripts"
|
||||
[cliShowSshHelp]="scripts"
|
||||
[cliShowStorageHelp]="scripts"
|
||||
[cliShowSystemHelp]="scripts"
|
||||
[cliShowUpdateHelp]="scripts"
|
||||
[cliShowUpdaterHelp]="scripts"
|
||||
@ -2112,6 +2146,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[_runRootHelper]="scripts"
|
||||
[runSocket]="scripts"
|
||||
[runSshAccess]="scripts"
|
||||
[runStorage]="scripts"
|
||||
[runSvc]="scripts"
|
||||
[runSystem]="scripts"
|
||||
[runTask]="scripts"
|
||||
@ -2143,6 +2178,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[setupWizardTerminal]="scripts"
|
||||
[showInstructions]="scripts"
|
||||
[sourceBackupLocations]="scripts"
|
||||
[sourceStorageLocations]="scripts"
|
||||
[sshRemote]="scripts"
|
||||
[stalwart_apply_port_access]="containers"
|
||||
[stalwart_cli]="containers"
|
||||
@ -2183,6 +2219,34 @@ declare -gA LP_FN_ROOT=(
|
||||
[_stoatWriteSecrets]="containers"
|
||||
[_stoatWriteUrlFiles]="containers"
|
||||
[stopCrowdsec]="containers"
|
||||
[storageAdd]="scripts"
|
||||
[storageAppsOnRoot]="scripts"
|
||||
[_storageCheckDistinct]="scripts"
|
||||
[_storageCheckEncryption]="scripts"
|
||||
[_storageCheckFsType]="scripts"
|
||||
[storageCheckLocation]="scripts"
|
||||
[_storageCheckMountOpts]="scripts"
|
||||
[_storageCheckOwnership]="scripts"
|
||||
[storageCheckPath]="scripts"
|
||||
[_storageCheckPersistence]="scripts"
|
||||
[_storageCheckRemovable]="scripts"
|
||||
[_storageCheckSharedWithBackup]="scripts"
|
||||
[_storageCheckSpace]="scripts"
|
||||
[storageDisks]="scripts"
|
||||
[storageDisksData]="scripts"
|
||||
[_storageEmit]="scripts"
|
||||
[_storageFreeHuman]="scripts"
|
||||
[storageList]="scripts"
|
||||
[storageLocationConfig]="scripts"
|
||||
[storageLocationDir]="scripts"
|
||||
[storageLocationsDir]="scripts"
|
||||
[_storageProbeDir]="scripts"
|
||||
[_storageRefreshWebui]="scripts"
|
||||
[storageRemove]="scripts"
|
||||
[storageScan]="scripts"
|
||||
[storageScanCandidates]="scripts"
|
||||
[_storageSkipTarget]="scripts"
|
||||
[_storageWriteLocationConfig]="scripts"
|
||||
[switchMigrateBackupApps]="scripts"
|
||||
[switchMigrateRestoreApps]="scripts"
|
||||
[tagsManagerGetTagContent]="scripts"
|
||||
@ -2392,6 +2456,7 @@ LP_EAGER_FILES=(
|
||||
"scripts:migrate/migrate_url_rewrite.sh"
|
||||
"containers:nextcloud/scripts/nextcloud_upgrade_hooks.sh"
|
||||
"scripts:source/artifacts.sh"
|
||||
"scripts:storage/storage_scan.sh"
|
||||
"scripts:task/crontab_check_processor.sh"
|
||||
"scripts:task/crontab_task_processor.sh"
|
||||
"scripts:validation/validate_config.sh"
|
||||
@ -2742,6 +2807,7 @@ cliHandleResetCommands() { unset -f cliHandleResetCommands; __lpAutoload "${inst
|
||||
cliHandleRestoreCommands() { unset -f cliHandleRestoreCommands; __lpAutoload "${install_scripts_dir}cli/commands/restore/cli_restore_commands.sh"; cliHandleRestoreCommands "$@"; }
|
||||
cliHandleSetupCommands() { unset -f cliHandleSetupCommands; __lpAutoload "${install_scripts_dir}cli/commands/setup/cli_setup_commands.sh"; cliHandleSetupCommands "$@"; }
|
||||
cliHandleSshCommands() { unset -f cliHandleSshCommands; __lpAutoload "${install_scripts_dir}cli/commands/ssh/cli_ssh_commands.sh"; cliHandleSshCommands "$@"; }
|
||||
cliHandleStorageCommands() { unset -f cliHandleStorageCommands; __lpAutoload "${install_scripts_dir}cli/commands/storage/cli_storage_commands.sh"; cliHandleStorageCommands "$@"; }
|
||||
cliHandleSystemCommands() { unset -f cliHandleSystemCommands; __lpAutoload "${install_scripts_dir}cli/commands/system/cli_system_commands.sh"; cliHandleSystemCommands "$@"; }
|
||||
cliHandleUpdateCommands() { unset -f cliHandleUpdateCommands; __lpAutoload "${install_scripts_dir}cli/commands/update/cli_update_commands.sh"; cliHandleUpdateCommands "$@"; }
|
||||
cliHandleUpdaterCommands() { unset -f cliHandleUpdaterCommands; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; cliHandleUpdaterCommands "$@"; }
|
||||
@ -2768,6 +2834,7 @@ cliShowResetHelp() { unset -f cliShowResetHelp; __lpAutoload "${install_scripts_
|
||||
cliShowRestoreHelp() { unset -f cliShowRestoreHelp; __lpAutoload "${install_scripts_dir}cli/commands/restore/cli_restore_header.sh"; cliShowRestoreHelp "$@"; }
|
||||
cliShowSetupHelp() { unset -f cliShowSetupHelp; __lpAutoload "${install_scripts_dir}cli/commands/setup/cli_setup_header.sh"; cliShowSetupHelp "$@"; }
|
||||
cliShowSshHelp() { unset -f cliShowSshHelp; __lpAutoload "${install_scripts_dir}cli/commands/ssh/cli_ssh_header.sh"; cliShowSshHelp "$@"; }
|
||||
cliShowStorageHelp() { unset -f cliShowStorageHelp; __lpAutoload "${install_scripts_dir}cli/commands/storage/cli_storage_header.sh"; cliShowStorageHelp "$@"; }
|
||||
cliShowSystemHelp() { unset -f cliShowSystemHelp; __lpAutoload "${install_scripts_dir}cli/commands/system/cli_system_header.sh"; cliShowSystemHelp "$@"; }
|
||||
cliShowUpdateHelp() { unset -f cliShowUpdateHelp; __lpAutoload "${install_scripts_dir}cli/commands/update/cli_update_header.sh"; cliShowUpdateHelp "$@"; }
|
||||
cliShowUpdaterHelp() { unset -f cliShowUpdaterHelp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_header.sh"; cliShowUpdaterHelp "$@"; }
|
||||
@ -3332,6 +3399,7 @@ runResolv() { unset -f runResolv; __lpAutoload "${install_scripts_dir}docker/com
|
||||
_runRootHelper() { unset -f _runRootHelper; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; _runRootHelper "$@"; }
|
||||
runSocket() { unset -f runSocket; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runSocket "$@"; }
|
||||
runSshAccess() { unset -f runSshAccess; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runSshAccess "$@"; }
|
||||
runStorage() { unset -f runStorage; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runStorage "$@"; }
|
||||
runSvc() { unset -f runSvc; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runSvc "$@"; }
|
||||
runSystem() { unset -f runSystem; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runSystem "$@"; }
|
||||
runTask() { unset -f runTask; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; runTask "$@"; }
|
||||
@ -3363,6 +3431,7 @@ setupWizardReset() { unset -f setupWizardReset; __lpAutoload "${install_scripts_
|
||||
setupWizardTerminal() { unset -f setupWizardTerminal; __lpAutoload "${install_scripts_dir}checks/first_install.sh"; setupWizardTerminal "$@"; }
|
||||
showInstructions() { unset -f showInstructions; __lpAutoload "${install_scripts_dir}menu/message/instructions.sh"; showInstructions "$@"; }
|
||||
sourceBackupLocations() { unset -f sourceBackupLocations; __lpAutoload "${install_scripts_dir}backup/locations/location_loader.sh"; sourceBackupLocations "$@"; }
|
||||
sourceStorageLocations() { unset -f sourceStorageLocations; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; sourceStorageLocations "$@"; }
|
||||
sshRemote() { unset -f sshRemote; __lpAutoload "${install_scripts_dir}network/ssh/ssh.sh"; sshRemote "$@"; }
|
||||
stalwart_apply_port_access() { unset -f stalwart_apply_port_access; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_apply_port_access "$@"; }
|
||||
stalwart_cli() { unset -f stalwart_cli; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_cli "$@"; }
|
||||
@ -3403,6 +3472,34 @@ _stoatSetDisabled() { unset -f _stoatSetDisabled; __lpAutoload "${install_contai
|
||||
_stoatWriteSecrets() { unset -f _stoatWriteSecrets; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatWriteSecrets "$@"; }
|
||||
_stoatWriteUrlFiles() { unset -f _stoatWriteUrlFiles; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatWriteUrlFiles "$@"; }
|
||||
stopCrowdsec() { unset -f stopCrowdsec; __lpAutoload "${install_containers_dir}crowdsec/crowdsec.sh"; stopCrowdsec "$@"; }
|
||||
storageAdd() { unset -f storageAdd; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageAdd "$@"; }
|
||||
storageAppsOnRoot() { unset -f storageAppsOnRoot; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageAppsOnRoot "$@"; }
|
||||
_storageCheckDistinct() { unset -f _storageCheckDistinct; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckDistinct "$@"; }
|
||||
_storageCheckEncryption() { unset -f _storageCheckEncryption; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckEncryption "$@"; }
|
||||
_storageCheckFsType() { unset -f _storageCheckFsType; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckFsType "$@"; }
|
||||
storageCheckLocation() { unset -f storageCheckLocation; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; storageCheckLocation "$@"; }
|
||||
_storageCheckMountOpts() { unset -f _storageCheckMountOpts; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckMountOpts "$@"; }
|
||||
_storageCheckOwnership() { unset -f _storageCheckOwnership; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckOwnership "$@"; }
|
||||
storageCheckPath() { unset -f storageCheckPath; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; storageCheckPath "$@"; }
|
||||
_storageCheckPersistence() { unset -f _storageCheckPersistence; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckPersistence "$@"; }
|
||||
_storageCheckRemovable() { unset -f _storageCheckRemovable; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckRemovable "$@"; }
|
||||
_storageCheckSharedWithBackup() { unset -f _storageCheckSharedWithBackup; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckSharedWithBackup "$@"; }
|
||||
_storageCheckSpace() { unset -f _storageCheckSpace; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageCheckSpace "$@"; }
|
||||
storageDisks() { unset -f storageDisks; __lpAutoload "${install_scripts_dir}storage/storage_disks.sh"; storageDisks "$@"; }
|
||||
storageDisksData() { unset -f storageDisksData; __lpAutoload "${install_scripts_dir}storage/storage_disks.sh"; storageDisksData "$@"; }
|
||||
_storageEmit() { unset -f _storageEmit; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageEmit "$@"; }
|
||||
_storageFreeHuman() { unset -f _storageFreeHuman; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageFreeHuman "$@"; }
|
||||
storageList() { unset -f storageList; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageList "$@"; }
|
||||
storageLocationConfig() { unset -f storageLocationConfig; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationConfig "$@"; }
|
||||
storageLocationDir() { unset -f storageLocationDir; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationDir "$@"; }
|
||||
storageLocationsDir() { unset -f storageLocationsDir; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationsDir "$@"; }
|
||||
_storageProbeDir() { unset -f _storageProbeDir; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageProbeDir "$@"; }
|
||||
_storageRefreshWebui() { unset -f _storageRefreshWebui; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageRefreshWebui "$@"; }
|
||||
storageRemove() { unset -f storageRemove; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageRemove "$@"; }
|
||||
storageScan() { unset -f storageScan; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; storageScan "$@"; }
|
||||
storageScanCandidates() { unset -f storageScanCandidates; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; storageScanCandidates "$@"; }
|
||||
_storageSkipTarget() { unset -f _storageSkipTarget; __lpAutoload "${install_scripts_dir}storage/storage_scan.sh"; _storageSkipTarget "$@"; }
|
||||
_storageWriteLocationConfig() { unset -f _storageWriteLocationConfig; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageWriteLocationConfig "$@"; }
|
||||
switchMigrateBackupApps() { unset -f switchMigrateBackupApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateBackupApps "$@"; }
|
||||
switchMigrateRestoreApps() { unset -f switchMigrateRestoreApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateRestoreApps "$@"; }
|
||||
tagsManagerGetTagContent() { unset -f tagsManagerGetTagContent; __lpAutoload "${install_scripts_dir}config/tags/manager/tags_manager_content.sh"; tagsManagerGetTagContent "$@"; }
|
||||
|
||||
@ -23,6 +23,7 @@ files_libreportal_cli=(
|
||||
"${source_scripts[@]}"
|
||||
"${ssh_scripts[@]}"
|
||||
"${start_scripts[@]}"
|
||||
"${storage_scripts[@]}"
|
||||
"${task_scripts[@]}"
|
||||
"${update_scripts[@]}"
|
||||
"${validation_scripts[@]}"
|
||||
|
||||
168
scripts/storage/storage_disks.sh
Normal file
168
scripts/storage/storage_disks.sh
Normal file
@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
# The Disks view — one row per filesystem, showing what LibrePortal does with it.
|
||||
#
|
||||
# This is the answer to "storage location" and "backup location" being two
|
||||
# competing top-level nouns: the device becomes the organising concept and the
|
||||
# registries become ROLES on it. See §7.1 of docs/roadmap/storage-locations.md.
|
||||
#
|
||||
# The union is the part that matters more than the enrichment. A registered
|
||||
# location whose drive is unplugged does not appear in findmnt or lsblk at all —
|
||||
# and that is precisely when someone looks at this page. So rows come from the
|
||||
# REGISTRY first and attached hardware second: a missing device still renders,
|
||||
# marked not-attached, naming the apps stranded on it. A row vanishing when the
|
||||
# disk is pulled is the one failure this view cannot have.
|
||||
|
||||
# Emit one TSV record per row:
|
||||
# <key> <mount> <source> <fstype> <size> <free> <removable> <state> <roles> <apps>
|
||||
#
|
||||
# key fs UUID when known, else the mount point (device names reorder across
|
||||
# reboots and would scramble the table)
|
||||
# state ok | not-attached
|
||||
# roles comma-separated: system, primary, storage:<id>, backups:<idx>
|
||||
# apps comma-separated app slugs on this filesystem ("-" when none/unknown)
|
||||
storageDisksData()
|
||||
{
|
||||
local -A row_mount=() row_source=() row_fstype=() row_size=() row_free=()
|
||||
local -A row_rm=() row_state=() row_roles=() row_apps=() row_order=()
|
||||
local order=0
|
||||
|
||||
_addRole() {
|
||||
local k="$1" r="$2"
|
||||
if [[ -z "${row_roles[$k]:-}" ]]; then row_roles["$k"]="$r"
|
||||
elif [[ ",${row_roles[$k]}," != *",$r,"* ]]; then row_roles["$k"]="${row_roles[$k]},$r"; fi
|
||||
}
|
||||
|
||||
# ---- 1. attached filesystems -------------------------------------------
|
||||
local target source fstype size avail uuid key line kv val
|
||||
if command -v findmnt >/dev/null 2>&1; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
target=""; source=""; fstype=""; size=""; avail=""; uuid=""
|
||||
for kv in TARGET SOURCE FSTYPE SIZE AVAIL UUID; 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" ;; UUID) uuid="$val" ;;
|
||||
esac
|
||||
done
|
||||
[[ -z "$target" ]] && continue
|
||||
_storageSkipTarget "$target" && continue
|
||||
[[ "$fstype" =~ ^($_STORAGE_SKIP_FSTYPES)$ ]] && continue
|
||||
|
||||
key="${uuid:-$target}"
|
||||
[[ -n "${row_mount[$key]:-}" ]] && continue
|
||||
row_order["$key"]=$((order++))
|
||||
row_mount["$key"]="$target"; row_source["$key"]="$source"
|
||||
row_fstype["$key"]="$fstype"; row_size["$key"]="$size"
|
||||
row_free["$key"]="$avail"; row_state["$key"]="ok"
|
||||
row_rm["$key"]=0
|
||||
if [[ "$source" == /dev/* ]] && command -v lsblk >/dev/null 2>&1; then
|
||||
local r h
|
||||
r=$(lsblk -no RM "$source" 2>/dev/null | head -1 | tr -d ' ')
|
||||
h=$(lsblk -no HOTPLUG "$source" 2>/dev/null | head -1 | tr -d ' ')
|
||||
[[ "$r" == "1" || "$h" == "1" ]] && row_rm["$key"]=1
|
||||
fi
|
||||
done < <(findmnt -Pno TARGET,SOURCE,FSTYPE,SIZE,AVAIL,UUID 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Map a path to the row that holds it (by mount point, longest match wins).
|
||||
_rowFor() {
|
||||
local p="${1%/}" best="" best_len=0 k m
|
||||
for k in "${!row_mount[@]}"; do
|
||||
m="${row_mount[$k]%/}"; [[ -z "$m" ]] && m="/"
|
||||
if [[ "$p" == "$m" || "$p" == "$m/"* || "$m" == "/" ]]; then
|
||||
(( ${#m} >= best_len )) && { best="$k"; best_len=${#m}; }
|
||||
fi
|
||||
done
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
# ---- 2. roles ------------------------------------------------------------
|
||||
local k
|
||||
k=$(_rowFor "/"); [[ -n "$k" ]] && _addRole "$k" "system"
|
||||
k=$(_rowFor "$(primaryRoot)"); [[ -n "$k" ]] && _addRole "$k" "primary"
|
||||
|
||||
# Backup locations
|
||||
if declare -f resticEnabledLocations >/dev/null 2>&1 \
|
||||
&& declare -f backupLocationResolvedPath >/dev/null 2>&1; then
|
||||
local idx bpath probe
|
||||
while IFS= read -r idx; do
|
||||
[[ -z "$idx" ]] && continue
|
||||
bpath=$(backupLocationResolvedPath "$idx" 2>/dev/null); bpath="${bpath%/}"
|
||||
[[ -z "$bpath" ]] && continue
|
||||
probe=$(_storageProbeDir "$bpath")
|
||||
k=$(_rowFor "$probe"); [[ -n "$k" ]] && _addRole "$k" "backups:$idx"
|
||||
done < <(resticEnabledLocations 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ---- 3. registered storage locations, INCLUDING absent ones -------------
|
||||
local _id _path _rest
|
||||
if [[ -r "$lp_storage_registry" ]]; then
|
||||
while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do
|
||||
[[ -z "$_path" || "$_id" == \#* ]] && continue
|
||||
_path="${_path%/}"
|
||||
if storageRootAvailable "$_path"; then
|
||||
k=$(_rowFor "$_path")
|
||||
[[ -n "$k" ]] && { _addRole "$k" "storage:$_id"; row_apps["$k"]="$(storageAppsOnRoot "$_path" | paste -sd, -)"; }
|
||||
else
|
||||
# Not attached: synthesise the row from the registry so it still
|
||||
# appears, and name what is stranded on it.
|
||||
key="absent:$_id"
|
||||
row_order["$key"]=$((order++))
|
||||
row_mount["$key"]="$_path"; row_source["$key"]="-"
|
||||
row_fstype["$key"]="-"; row_size["$key"]="-"
|
||||
row_free["$key"]="-"; row_rm["$key"]=0
|
||||
row_state["$key"]="not-attached"
|
||||
_addRole "$key" "storage:$_id"
|
||||
local stranded=""
|
||||
while IFS=$'\t' read -r _s _r; do
|
||||
[[ "${_r%/}" == "$_path" ]] && stranded+="${stranded:+,}$_s"
|
||||
done < <(cat "$(storageIndexFile)" 2>/dev/null)
|
||||
row_apps["$key"]="${stranded:--}"
|
||||
fi
|
||||
done < "$lp_storage_registry"
|
||||
fi
|
||||
|
||||
# Apps on the primary root
|
||||
k=$(_rowFor "$(primaryRoot)")
|
||||
[[ -n "$k" && -z "${row_apps[$k]:-}" ]] && row_apps["$k"]="$(storageAppsOnRoot "$(primaryRoot)" | paste -sd, -)"
|
||||
|
||||
# ---- 4. emit, in discovery order ----------------------------------------
|
||||
local sorted
|
||||
sorted=$(for k in "${!row_order[@]}"; do printf '%s\t%s\n' "${row_order[$k]}" "$k"; done | sort -n | cut -f2)
|
||||
while IFS= read -r k; do
|
||||
[[ -z "$k" ]] && continue
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$k" "${row_mount[$k]}" "${row_source[$k]}" "${row_fstype[$k]}" \
|
||||
"${row_size[$k]}" "${row_free[$k]}" "${row_rm[$k]}" \
|
||||
"${row_state[$k]}" "${row_roles[$k]:--}" "${row_apps[$k]:--}"
|
||||
done <<< "$sorted"
|
||||
|
||||
unset -f _addRole _rowFor
|
||||
}
|
||||
|
||||
# Human-readable Disks view.
|
||||
storageDisks()
|
||||
{
|
||||
local key mount source fstype size free rm_flag state roles apps
|
||||
isHeader "Disks"
|
||||
printf '%-24s %-9s %-8s %-8s %-22s %-14s %s\n' \
|
||||
"MOUNT" "FS" "SIZE" "FREE" "ROLES" "STATE" "APPS"
|
||||
|
||||
while IFS=$'\t' read -r key mount source fstype size free rm_flag state roles apps; do
|
||||
[[ -z "$key" ]] && continue
|
||||
local shown_state="$state"
|
||||
[[ "$rm_flag" == "1" ]] && shown_state="$state, removable"
|
||||
printf '%-24s %-9s %-8s %-8s %-22s %-14s %s\n' \
|
||||
"$mount" "$fstype" "$size" "$free" "$roles" "$shown_state" "$apps"
|
||||
done < <(storageDisksData)
|
||||
|
||||
echo ""
|
||||
isNotice "A location shown as 'not-attached' is registered but its drive is not mounted."
|
||||
isNotice "Apps listed against it will refuse to start until it is back — deliberately,"
|
||||
isNotice "so they are never rebuilt empty on the bare mount point."
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user