fix(install): repair Docker bind-mount stub dirs where a config file belongs

Docker materialises a missing bind-mount source as an empty directory when a
container starts. The WebUI compose mounts ./libreportal.config as a file, so a
container start before the config landed left a directory at that path — and it
was self-perpetuating:

  - copyFolder's tar extract aborted the whole source copy with
    "libreportal/libreportal.config: Cannot open: File exists" (exit 2)
  - dockerConfigSetupToContainer guards on [ ! -f ], which a directory fails, so
    copyFile dropped the real config INSIDE the stub
  - the closing -e / -r sanity checks both pass on a directory

The installer then reported success while libreportal-service crash-looped on
EISDIR reading /app/libreportal.config, leaving the WebUI unreachable.

Add repairStubDirForFile: promote a same-named file out of the stub, drop the
directory, and report if the path still isn't a regular file. Call it before the
WebUI source copy and before the per-app config copy (covers every app, not just
the WebUI), and tighten the closing existence check from -e to -f so a stub can
never pass validation again.

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-07-21 19:58:19 +01:00
parent 1af73143c3
commit 3f6a9b395a
4 changed files with 92 additions and 3 deletions

View File

@ -40,6 +40,11 @@ dockerConfigSetupToContainer()
mv "$temp_file" "$target_file"
}
# The compose file bind-mounts this config as a FILE. If the container ever
# started before it landed, Docker left a directory here — clear it before the
# copy, or the config gets written inside it and the app boots on a directory.
repairStubDirForFile "$target_path/$config_file" "$silent_flag"
if [ ! -f "$target_path/$config_file" ]; then
if [ "$silent_flag" == "loud" ]; then
isNotice "Copying config file to '$target_path/$config_file'..."
@ -68,9 +73,10 @@ dockerConfigSetupToContainer()
fixConfigPermissions $silent_flag $app_name;
# Check if the file exists
if [ ! -e "$target_path/$config_file" ]; then
isError "File $target_path/$config_file does not exist"
# Must be a regular file, not just present: a Docker-created bind-mount
# directory satisfies -e and -r but makes the container exit on boot.
if [ ! -f "$target_path/$config_file" ]; then
isError "File $target_path/$config_file does not exist, or is not a regular file"
fi
# Check if the user has read permission on target_path/config_file

View File

@ -26,3 +26,41 @@ copyFile()
$op cp $flags_full "$file" "$save_dir" >/dev/null 2>&1
fi
}
# Docker materialises a missing bind-mount source as an empty DIRECTORY when the
# container starts. Where the mount is a FILE (an app's <app>.config), that
# directory is self-perpetuating: tar extraction fails with "Cannot open: File
# exists", `cp` drops the real file INSIDE it, `-e`/`-r` checks still pass, and
# the container reads a directory (EISDIR) and exits on boot.
# Restore the regular file: promote a same-named file from inside the stub if the
# copy already landed there, then drop the stub. Returns non-zero if the path is
# still a directory afterwards.
repairStubDirForFile()
{
local target="$1"
local silent_flag="${2:-silent}"
[[ -d "$target" ]] || return 0
local op="runInstallOp"
[[ "$target" == "$containers_dir"* || "$target" == "${LP_CONTAINERS_DIR:-/libreportal-containers}"/* ]] && op="runFileOp"
local name; name=$(basename "$target")
local staged="$target.stub-repair.$$"
if [[ -f "$target/$name" ]]; then
$op cp -f "$target/$name" "$staged" >/dev/null 2>&1
$op rm -rf "$target" >/dev/null 2>&1
$op mv "$staged" "$target" >/dev/null 2>&1
else
$op rm -rf "$target" >/dev/null 2>&1
fi
if [[ -d "$target" ]]; then
isNotice "Could not repair '$target' — it is a directory where a file is required."
return 1
fi
[[ "$silent_flag" == "loud" ]] && isNotice "Repaired '$name' (Docker had created it as a bind-mount directory)."
return 0
}

View File

@ -761,6 +761,7 @@ declare -gA LP_FN_MAP=(
[repairDirectoryStructure]="task/crontab_check_processor.sh"
[repairFileSystem]="task/crontab_check_processor.sh"
[repairPermissions]="task/crontab_check_processor.sh"
[repairStubDirForFile]="function/file/copy_file.sh"
[repairSystemIssues]="task/crontab_check_processor.sh"
[repairSystemService]="task/crontab_check_processor.sh"
[repairTaskSystem]="task/crontab_check_processor.sh"
@ -911,11 +912,23 @@ declare -gA LP_FN_MAP=(
[updateDockerSudoPassword]="docker/update_docker_sudo_pass.sh"
[updateFileOwnership]="function/permission/ownership/file.sh"
[updateHostIPToWhitelist]="config/utils/update_whitelist.sh"
[updaterAllServiceImages]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterApplyAll]="cli/commands/updater/cli_updater_commands.sh"
[updaterApplyApp]="cli/commands/updater/cli_updater_commands.sh"
[updaterClassifyTag]="webui/data/generators/updater/webui_updater_scan.sh"
[_updaterCleanImageRef]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterComposePull]="cli/commands/updater/cli_updater_commands.sh"
[updaterDisplayVersion]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterInspectLocal]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterLastUpdateFrom]="cli/commands/updater/cli_updater_commands.sh"
[updaterPrimaryImage]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterRecordHistory]="cli/commands/updater/cli_updater_commands.sh"
[updaterRefDigest]="cli/commands/updater/cli_updater_commands.sh"
[updaterRegistryDigest]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterRepoTag]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterRollbackApp]="cli/commands/updater/cli_updater_commands.sh"
[updaterSetAnchorRef]="cli/commands/updater/cli_updater_commands.sh"
[updaterTagOf]="webui/data/generators/updater/webui_updater_scan.sh"
[updateTaskFields]="task/crontab_task_processor.sh"
[userExists]="function/checks/user_exists.sh"
[validateContainerHealth]="task/crontab_check_processor.sh"
@ -1753,6 +1766,7 @@ declare -gA LP_FN_ROOT=(
[repairDirectoryStructure]="scripts"
[repairFileSystem]="scripts"
[repairPermissions]="scripts"
[repairStubDirForFile]="scripts"
[repairSystemIssues]="scripts"
[repairSystemService]="scripts"
[repairTaskSystem]="scripts"
@ -1903,11 +1917,23 @@ declare -gA LP_FN_ROOT=(
[updateDockerSudoPassword]="scripts"
[updateFileOwnership]="scripts"
[updateHostIPToWhitelist]="scripts"
[updaterAllServiceImages]="scripts"
[updaterApplyAll]="scripts"
[updaterApplyApp]="scripts"
[updaterClassifyTag]="scripts"
[_updaterCleanImageRef]="scripts"
[updaterComposePull]="scripts"
[updaterDisplayVersion]="scripts"
[updaterInspectLocal]="scripts"
[updaterLastUpdateFrom]="scripts"
[updaterPrimaryImage]="scripts"
[updaterRecordHistory]="scripts"
[updaterRefDigest]="scripts"
[updaterRegistryDigest]="scripts"
[updaterRepoTag]="scripts"
[updaterRollbackApp]="scripts"
[updaterSetAnchorRef]="scripts"
[updaterTagOf]="scripts"
[updateTaskFields]="scripts"
[userExists]="scripts"
[validateContainerHealth]="scripts"
@ -2778,6 +2804,7 @@ removeEmptyLineAtFileEnd() { unset -f removeEmptyLineAtFileEnd; __lpAutoload "${
repairDirectoryStructure() { unset -f repairDirectoryStructure; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairDirectoryStructure "$@"; }
repairFileSystem() { unset -f repairFileSystem; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairFileSystem "$@"; }
repairPermissions() { unset -f repairPermissions; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairPermissions "$@"; }
repairStubDirForFile() { unset -f repairStubDirForFile; __lpAutoload "${install_scripts_dir}function/file/copy_file.sh"; repairStubDirForFile "$@"; }
repairSystemIssues() { unset -f repairSystemIssues; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairSystemIssues "$@"; }
repairSystemService() { unset -f repairSystemService; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairSystemService "$@"; }
repairTaskSystem() { unset -f repairTaskSystem; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; repairTaskSystem "$@"; }
@ -2928,11 +2955,23 @@ updateDockerNetworkSubnet() { unset -f updateDockerNetworkSubnet; __lpAutoload "
updateDockerSudoPassword() { unset -f updateDockerSudoPassword; __lpAutoload "${install_scripts_dir}docker/update_docker_sudo_pass.sh"; updateDockerSudoPassword "$@"; }
updateFileOwnership() { unset -f updateFileOwnership; __lpAutoload "${install_scripts_dir}function/permission/ownership/file.sh"; updateFileOwnership "$@"; }
updateHostIPToWhitelist() { unset -f updateHostIPToWhitelist; __lpAutoload "${install_scripts_dir}config/utils/update_whitelist.sh"; updateHostIPToWhitelist "$@"; }
updaterAllServiceImages() { unset -f updaterAllServiceImages; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterAllServiceImages "$@"; }
updaterApplyAll() { unset -f updaterApplyAll; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterApplyAll "$@"; }
updaterApplyApp() { unset -f updaterApplyApp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterApplyApp "$@"; }
updaterClassifyTag() { unset -f updaterClassifyTag; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterClassifyTag "$@"; }
_updaterCleanImageRef() { unset -f _updaterCleanImageRef; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; _updaterCleanImageRef "$@"; }
updaterComposePull() { unset -f updaterComposePull; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterComposePull "$@"; }
updaterDisplayVersion() { unset -f updaterDisplayVersion; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterDisplayVersion "$@"; }
updaterInspectLocal() { unset -f updaterInspectLocal; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterInspectLocal "$@"; }
updaterLastUpdateFrom() { unset -f updaterLastUpdateFrom; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterLastUpdateFrom "$@"; }
updaterPrimaryImage() { unset -f updaterPrimaryImage; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterPrimaryImage "$@"; }
updaterRecordHistory() { unset -f updaterRecordHistory; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterRecordHistory "$@"; }
updaterRefDigest() { unset -f updaterRefDigest; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterRefDigest "$@"; }
updaterRegistryDigest() { unset -f updaterRegistryDigest; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterRegistryDigest "$@"; }
updaterRepoTag() { unset -f updaterRepoTag; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterRepoTag "$@"; }
updaterRollbackApp() { unset -f updaterRollbackApp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterRollbackApp "$@"; }
updaterSetAnchorRef() { unset -f updaterSetAnchorRef; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterSetAnchorRef "$@"; }
updaterTagOf() { unset -f updaterTagOf; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterTagOf "$@"; }
updateTaskFields() { unset -f updateTaskFields; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; updateTaskFields "$@"; }
userExists() { unset -f userExists; __lpAutoload "${install_scripts_dir}function/checks/user_exists.sh"; userExists "$@"; }
validateContainerHealth() { unset -f validateContainerHealth; __lpAutoload "${install_scripts_dir}task/crontab_check_processor.sh"; validateContainerHealth "$@"; }

View File

@ -13,6 +13,12 @@ installLibrePortalImageWebUI()
# Establish traversal + containers-root ownership FIRST.
fixFolderPermissions
# A previous run that started the container before its config landed leaves
# libreportal.config as a Docker-created directory. The tar copy below can't
# extract a file over a directory ("Cannot open: File exists") and aborts the
# whole source copy, so clear the stub first.
repairStubDirForFile "$containers_dir/libreportal/libreportal.config" "loud"
local result; result=$(copyFolder "$install_containers_dir/libreportal" "$containers_dir" "$sudo_user_name")
checkSuccess "Copy the LibrePortal to the containers folder"