fix(rootless): apps/categories/config/system generators write as container owner

The remaining WebUI generators built JSON into a temp file inside the
output dir then placed it with mv/sudo mv + a createTouch that can't re-own,
so in rootless they produced root/libreportal-owned data and 'touch:
Permission denied' spam. Two problems: the temp lived in the (now
dockerinstall-owned) output dir, which the cron updater — running as
libreportal — can't write; and the final file landed wrong-owned.

Move each temp to mktemp (/tmp, writable by whoever runs the updater) and
place the result via runFileWrite (writes as the container owner:
dockerinstall in rootless, manager in rooted), dropping the redundant
createTouch; convert the dir mkdirs to runFileOp. Covers apps
(services/config/tools/app_status/gluetun/config_patch), categories
(app/config-categories/field-mappings), config (configs.json) and system
(info/memory/disk/update). The logs file is handled by the now mode-aware
createFolders + createTouch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 14:07:46 +01:00
parent bd4887f889
commit ed9697cdc0
14 changed files with 39 additions and 60 deletions

View File

@ -40,7 +40,7 @@ webuiUpdateAppStatus() {
done < "$output_file"
# Replace the original file with the updated one
mv "$temp_file" "$output_file"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
if [[ "$found_app" == true ]]; then
isSuccessful "Updated $app_name status to: $is_installed"

View File

@ -40,8 +40,8 @@ webuiGenerateLibrePortalConfig() {
isNotice "Generating LibrePortal apps.json from config files..."
[[ -n "$specific_app" ]] && isNotice "Filtering to specific app: $specific_app"
mkdir -p "$(dirname "$output_file")"
local temp_file="${output_file}.tmp.$$"
runFileOp mkdir -p "$(dirname "$output_file")"
local temp_file="$(mktemp)"
cat > "$temp_file" << 'EOF'
{
@ -163,7 +163,7 @@ EOF
fi
if [[ -n "$icon_file" ]]; then
local icons_apps_dir="${containers_dir}libreportal/frontend/icons/apps"
mkdir -p "$icons_apps_dir"
runFileOp mkdir -p "$icons_apps_dir"
cp "$dir/$icon_file" "$icons_apps_dir/$icon_file" 2>/dev/null
fi
@ -239,8 +239,7 @@ EOF
# Already root via start.sh — drop redundant sudo.
if [ $? -eq 0 ]; then
mv "$temp_file" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -53,11 +53,11 @@ webuiPatchAppConfigJson() {
[[ "$row" == "1" ]] && installed="true"
fi
local tmp="${apps_json}.tmp.$$"
local tmp="$(mktemp)"
if jq --arg slug "$app_name" --argjson cfg "$cfg_json" --argjson inst "$installed" '
.apps |= map(if ((.command // "") | endswith(" " + $slug)) then .config = $cfg | .installed = $inst else . end)
' "$apps_json" > "$tmp" 2>/dev/null; then
sudo mv "$tmp" "$apps_json"
runFileWrite "$apps_json" < "$tmp"; rm -f "$tmp"
sudo chown "$docker_install_user:$docker_install_user" "$apps_json" 2>/dev/null || true
return 0
fi

View File

@ -8,10 +8,10 @@
webuiGenerateGluetunProviders() {
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/gluetun-providers.json"
local upstream="https://raw.githubusercontent.com/qdm12/gluetun/master/internal/storage/servers.json"
local tmp="${output_file}.tmp.$$"
local tmp="$(mktemp)"
local raw="${output_file}.raw.$$"
mkdir -p "$(dirname "$output_file")"
runFileOp mkdir -p "$(dirname "$output_file")"
if ! command -v jq >/dev/null 2>&1; then
isNotice "jq not installed; skipping gluetun provider refresh."
@ -70,8 +70,7 @@ webuiGenerateGluetunProviders() {
rm -f "$raw"
if [ -s "$tmp" ]; then
mv "$tmp" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$tmp"; rm -f "$tmp"
[[ -n "$new_etag" ]] && echo "$new_etag" | tee "$etag_file" >/dev/null
isSuccessful "Refreshed gluetun provider snapshot ($(jq '.providers | length' "$output_file") providers)."
else

View File

@ -10,10 +10,10 @@ webuiGenerateAppsServicesConfig() {
isNotice "Generating apps-services.json from database..."
mkdir -p "$(dirname "$output_file")"
runFileOp mkdir -p "$(dirname "$output_file")"
# Create temp file first, then atomic move
local temp_file="${output_file}.tmp.$$"
local temp_file="$(mktemp)"
# Create header
cat > "$temp_file" << 'EOF'
@ -27,8 +27,7 @@ EOF
# Write empty array and close
echo " ]" >> "$temp_file"
echo "}" >> "$temp_file"
mv "$temp_file" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
fi
# Get all installed apps
@ -39,8 +38,7 @@ EOF
# Write empty array and close
echo " ]" >> "$temp_file"
echo "}" >> "$temp_file"
mv "$temp_file" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
fi
# Process each app
@ -292,8 +290,7 @@ EOF
# No services found, write empty entry
echo " ]" >> "$temp_file"
echo "}" >> "$temp_file"
mv "$temp_file" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
echo "No services found to generate"
fi
@ -302,9 +299,8 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
mv "$temp_file" "$output_file"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
# Set proper ownership for web UI access
createTouch "$output_file" "$docker_install_user" "silent"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -45,9 +45,9 @@ webuiGenerateAppsToolsConfig() {
fi
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps-tools.json"
local tmp="${output_file}.tmp.$$"
local tmp="$(mktemp)"
mkdir -p "$(dirname "$output_file")"
runFileOp mkdir -p "$(dirname "$output_file")"
# Heredoc carries the JSON literal verbatim — much easier to edit
# than escaping nested quotes through jq -n.
@ -422,7 +422,6 @@ JSON
fi
fi
mv "$tmp" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$tmp"; rm -f "$tmp"
isSuccessful "Generated apps-tools.json ($(jq '[.apps[].tools | length] | add' "$output_file" 2>/dev/null || echo "?") tool(s) across $(jq '.apps | length' "$output_file" 2>/dev/null || echo "?") app(s))."
}

View File

@ -13,10 +13,10 @@ webuiCreateAppsCategories() {
echo "Generating apps-categories.json..."
# Create output directory if it doesn't exist
mkdir -p "$output_dir"
runFileOp mkdir -p "$output_dir"
# Create temp file first, then atomic move
local temp_file="${output_dir}/apps-categories.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${output_dir}/apps-categories.json"
# Generate apps-categories.json for data-loader.js
@ -95,9 +95,7 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
sudo mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$docker_install_user" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -13,10 +13,10 @@ webuiCreateAppsConfigCategories() {
echo "Creating apps-config-categories.json..."
# Create output directory if it doesn't exist
mkdir -p "$output_dir"
runFileOp mkdir -p "$output_dir"
# Create temp file first, then atomic move
local temp_file="${output_dir}/apps-config-categories.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${output_dir}/apps-config-categories.json"
# Generate apps-config-categories.json
@ -77,9 +77,7 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
sudo mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$docker_install_user" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -13,10 +13,10 @@ webuiCreateAppFieldMappings() {
echo "Creating apps-field-mappings.json..."
# Create output directory if it doesn't exist
mkdir -p "$output_dir"
runFileOp mkdir -p "$output_dir"
# Create temp file first, then atomic move
local temp_file="${output_dir}/apps-field-mappings.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${output_dir}/apps-field-mappings.json"
# Generate the complete JSON in one go
@ -804,9 +804,7 @@ RESTEOF
install_name_safe=$(printf '%s' "${CFG_INSTALL_NAME:-LibrePortal}" \
| sed -e 's/[\/&]/\\&/g' -e 's/"/\\"/g')
sed -i "s|__INSTALL_NAME__|${install_name_safe}|g" "$temp_file"
mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$docker_install_user" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -79,9 +79,9 @@ webuiGenerateSystemConfigs() {
}
local output_file="${containers_dir}libreportal/frontend/data/config/generated/configs.json"
local temp_file="${output_file}.tmp.$$"
local temp_file="$(mktemp)"
mkdir -p "$(dirname "$output_file")"
runFileOp mkdir -p "$(dirname "$output_file")"
# Cache category metadata in one read pass per .category file.
# Previously each of TITLE/DESCRIPTION/ICON/SUBCATEGORY_ORDER/ORDER cost
@ -367,8 +367,7 @@ EOF
# Already running as root via start.sh — sudo was redundant overhead.
if [ $? -eq 0 ]; then
mv "$temp_file" "$output_file"
createTouch "$output_file" "$docker_install_user" "silent"
runFileWrite "$output_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -31,7 +31,7 @@ webuiSystemDisk() {
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move
local temp_file="${system_dir}/disk_usage.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${system_dir}/disk_usage.json"
# Output JSON to temp file
@ -55,9 +55,7 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$sudo_user_name" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -35,7 +35,7 @@ webuiSystemInfo() {
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move
local temp_file="${system_dir}/system_info.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${system_dir}/system_info.json"
# Output JSON to temp file
@ -52,9 +52,7 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$sudo_user_name" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -27,7 +27,7 @@ webuiSystemMemory() {
createFolders "quiet" $sudo_user_name "$system_dir"
# Create temp file first, then atomic move
local temp_file="${system_dir}/memory_usage.json.tmp.$$"
local temp_file="$(mktemp)"
local final_file="${system_dir}/memory_usage.json"
# Output JSON to temp file
@ -49,9 +49,7 @@ EOF
# Atomic move to final location
if [ $? -eq 0 ]; then
mv "$temp_file" "$final_file"
# Set proper ownership for web UI access using createTouch
createTouch "$final_file" "$sudo_user_name" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi

View File

@ -70,7 +70,7 @@ webuiSystemUpdateCheck() {
local _error_json="null"
[[ -n "$_error" ]] && _error_json="\"$_error\""
local temp_file="${final_file}.tmp.$$"
local temp_file="$(mktemp)"
cat << EOF > "$temp_file"
{
"update_available": ${_update_available},
@ -91,8 +91,7 @@ webuiSystemUpdateCheck() {
}
EOF
if [ $? -eq 0 ]; then
mv "$temp_file" "$final_file"
createTouch "$final_file" "$sudo_user_name" "silent"
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
else
rm -f "$temp_file" 2>/dev/null
fi