LibrePortal/scripts/database/app/db_app_scan.sh
librelad 8b5e02c760 refactor(storage): resolve every app directory through appDir
The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.

Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.

Three places needed judgement rather than substitution:

db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.

instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.

peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.

Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 04:09:51 +01:00

167 lines
7.6 KiB
Bash
Executable File

#!/bin/bash
databaseAppScan()
{
# Check if sqlite3 is available
if ! command -v sqlite3 &> /dev/null; then
isNotice "sqlite3 command not found. Make sure it's installed."
fi
# Check if database file is available
if [ ! -f "$docker_dir/$db_file" ] ; then
isNotice "Database file not found. Make sure it's installed."
fi
isHeader "Scanning Docker folder for apps"
# Check if the folder exists
if [ ! -d "$containers_dir" ]; then
checkSuccess "Install path not found or not a directory: $containers_dir"
fi
# Scan every storage root. storageApps SKIPS a root whose drive is absent,
# so an app on an unplugged disk is simply missing from this list — which is
# why every "the folder is gone" branch below must first ask
# appStorageAvailable before it deletes anything. Without that guard,
# unplugging a drive would delete the database rows and port allocations of
# every app living on it.
local folder_names=$(storageApps)
# Check if no folders are found
if [ -z "$folder_names" ]; then
checkSuccess "No apps found."
fi
# Initialize the updated_count variable to keep track of updates made to the database
local updated_count=0
# Get the list of all folder names and statuses from the database
local existing_folders=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT name, status, uninstall_date FROM apps;")
# Create an array to store existing folder names in the database
local existing_folder_names=()
while IFS='|' read -r folder_name status uninstall_date; do
if [[ -n "$folder_name" ]]; then
existing_folder_names+=("$folder_name")
# Check if the folder exists on whichever storage root holds it
if [ -d "$(appDir "$folder_name")" ]; then
if (( status == 0 )); then
isNotice "The folder for $folder_name has been found."
# Update the database to set the status to 1 (installed) and unset the uninstall_date
local result; result=$(runInstallOp sqlite3 "$docker_dir/$db_file" "UPDATE apps SET status = 1, uninstall_date = NULL WHERE name = '$folder_name';")
checkSuccess "Updating apps database for $folder_name to installed status."
((updated_count++)) # Increment updated_count
fi
fi
fi
done <<< "$existing_folders"
# Loop through every app directory on every available storage root
while IFS= read -r app_dir; do
# Get the app name from the folder name
local app_name=$(basename "$app_dir")
# Check if the app name is not already in the database
if ! [[ " ${existing_folder_names[@]} " =~ " $app_name " ]]; then
# Check if the folder contains a valid .config file
if [ -f "$app_dir/$app_name.config" ]; then
# Extract the date and time from the folder name (if present)
local folder_datetime=$(echo "$app_name" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')
if [ -z "$folder_datetime" ]; then
# If no date and time are found in the folder name, use the current date and time
local folder_datetime=$(date "+%Y-%m-%d %H:%M:%S")
fi
# Split folder_datetime into date and time variables
local folder_date=$(echo "$folder_datetime" | awk '{print $1}')
local folder_time=$(echo "$folder_datetime" | awk '{print $2}')
# Add the new entry to the database with a default status of 1 (installed) and the extracted or current date
local result; result=$(runInstallOp sqlite3 "$docker_dir/$db_file" "INSERT INTO apps (name, status, install_date, install_time) VALUES ('$app_name', 1, '$folder_date', '$folder_time');")
checkSuccess "Adding $app_name to the apps database."
((updated_count++)) # Increment updated_count
fi
fi
done < <(storageAppDirs)
# Create an array to store folder names that should be removed from the database
local folders_to_remove=()
# Get a list of folder names that exist in the database but not in the current folder structure
for folder_name in "${existing_folder_names[@]}"; do
# Not gone — just on a drive that is not attached. Leave it alone.
appStorageAvailable "$folder_name" || continue
if [ ! -d "$(appDir "$folder_name")" ]; then
local folders_to_remove+=("$folder_name")
fi
done
# Get a list of folder names that exist in the database but not in the current folder structure
for folder_name in "${existing_folder_names[@]}"; do
if ! appStorageAvailable "$folder_name"; then
isNotice "Skipping $folder_name — its storage location is not mounted."
continue
fi
if [ ! -d "$(appDir "$folder_name")" ]; then
# Check if this folder is actually associated with an entry in the database
if [[ " ${folder_names[@]} " =~ " $folder_name " ]]; then
isNotice "Folder $folder_name no longer exists. Removing from the Database."
# Delete the entry from the apps table
local result; result=$(runInstallOp sqlite3 "$docker_dir/$db_file" "DELETE FROM apps WHERE name = '$app_name';")
checkSuccess "Removing $app_name from the apps database."
portsRemoveFromDatabase $app_name;
((updated_count++)) # Increment updated_count
fi
fi
done
# Check if all apps are up to date
if [ "$updated_count" -eq 0 ]; then
checkSuccess "All apps are up to date."
fi
# Reap leftover app folders that hold no real data: completely empty, or
# holding only the regenerable .config and/or the migrate.txt marker. There
# is nothing to lose in any of these cases, so we clean them automatically
# rather than prompting — the old "WIPE ALL DATA" question fired precisely
# when there was no data to wipe.
for folder_name in $folder_names; do
[[ "$folder_name" == "libreportal" ]] && continue
local folder_path="$(appDir "$folder_name")"
if [ ! -d "$folder_path" ]; then
isNotice "Folder $folder_name no longer exists — removing it from the database."
runInstallOp sqlite3 "$docker_dir/$db_file" "DELETE FROM apps WHERE name = '$folder_name';"
checkSuccess "Removing $folder_name from the apps database."
((updated_count++))
continue
fi
local num_files=$(runFileOp find "$folder_path" -maxdepth 1 -type f | wc -l)
local has_config=false has_migrate=false
[ -f "$folder_path/$folder_name.config" ] && has_config=true
[ -f "$folder_path/migrate.txt" ] && has_migrate=true
# Describe why the folder counts as a no-data leftover (empty string =
# it has real content, so leave it alone).
local reason=""
if [ "$num_files" -eq 0 ]; then
reason="empty"
elif [ "$num_files" -eq 1 ] && $has_config; then
reason="only a config file"
elif [ "$num_files" -eq 1 ] && $has_migrate; then
reason="only a migrate.txt marker"
elif [ "$num_files" -eq 2 ] && $has_config && $has_migrate; then
reason="only a config file and a migrate.txt marker"
fi
if [[ -n "$reason" ]]; then
isNotice "Cleaning up $folder_name ($reason — no data to lose)."
dockerUninstallApp "$folder_name"
fi
done
}