From 3f6a9b395ae8980855681375b5bd6c3c8e815694 Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 21 Jul 2026 19:58:19 +0100 Subject: [PATCH] fix(install): repair Docker bind-mount stub dirs where a config file belongs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../docker/docker_config_to_container.sh | 12 ++++-- scripts/function/file/copy_file.sh | 38 ++++++++++++++++++ .../source/files/arrays/function_manifest.sh | 39 +++++++++++++++++++ scripts/webui/webui_install_image.sh | 6 +++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/scripts/config/docker/docker_config_to_container.sh b/scripts/config/docker/docker_config_to_container.sh index 7f78715..7098dfe 100755 --- a/scripts/config/docker/docker_config_to_container.sh +++ b/scripts/config/docker/docker_config_to_container.sh @@ -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 diff --git a/scripts/function/file/copy_file.sh b/scripts/function/file/copy_file.sh index d82ef30..b7f85fb 100755 --- a/scripts/function/file/copy_file.sh +++ b/scripts/function/file/copy_file.sh @@ -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 .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 +} diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 88197c2..2ba6287 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -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 "$@"; } diff --git a/scripts/webui/webui_install_image.sh b/scripts/webui/webui_install_image.sh index 3f415f8..eef741d 100755 --- a/scripts/webui/webui_install_image.sh +++ b/scripts/webui/webui_install_image.sh @@ -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"