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>
167 lines
7.6 KiB
Bash
Executable File
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
|
|
}
|