`libreportal app export <app>` writes one app to a single file; `libreportal app import <file>` installs it here. This is the thing the original request described as "upload or navigate to the backup file" — a restic repository is not a file, but the want behind the phrasing is real. The format is deliberately boring: gzipped tar of the app directory with its .libreportal-manifest.json at the root. That manifest already records size, images, volumes, databases and storage location, so import reuses the phase-3 checks for free — refusing an app this version no longer ships, or one that will not fit, before unpacking anything. Export stops the app first. A tar of a running Postgres is a corrupt Postgres, and a file that looks fine until you restore it is worse than a refusal. tar runs as the owning user with --numeric-owner so container sub-UIDs survive the round trip instead of being remapped through this machine's /etc/passwd. Import re-runs the normal install pipeline after unpacking, because the compose still carries the SOURCE machine's ports, IPs and domains — that pipeline is what re-allocates them here, and migrateUrlRewrite fixes the host-bound CFG_* fields. Documented throughout as a courier format, not a backup: no history, no retention, no encryption. Importing under a different name is refused outright rather than half-working — the CFG_<APP>_* namespace and compose identities would all need rewriting, and `instance create` already answers "a second copy". Fixes a bug this surfaced: _appDirIntended did an indirect expansion on CFG_<SLUG>_STORAGE without checking <SLUG> can be a variable name, so a hyphenated or mistyped app name emitted "invalid variable name" and then reported the misleading "storage location is not mounted" for an app that simply did not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
260 lines
11 KiB
Bash
Executable File
260 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# App Commands Handler
|
|
# Handles all app subcommands by calling core functions
|
|
|
|
cliHandleAppCommands()
|
|
{
|
|
local action="$initial_command2"
|
|
local app_name="$initial_command3"
|
|
local config="$initial_command4"
|
|
local restore_arg2="$initial_command4"
|
|
local restore_arg3="$initial_command5"
|
|
local restore_arg4="$initial_command6"
|
|
local tool_name="$initial_command4"
|
|
local tool_args="$initial_command5"
|
|
|
|
local reset_network="false"
|
|
if [[ "$config" == "--reset-network" ]]; then
|
|
reset_network="true"
|
|
config=""
|
|
elif [[ "$initial_command5" == "--reset-network" ]]; then
|
|
reset_network="true"
|
|
fi
|
|
|
|
case "$action" in
|
|
"list")
|
|
if [[ -z "$app_name" ]]; then
|
|
cliShowAppHelp
|
|
elif [ "$app_name" = "available" ]; then
|
|
appScanAvailable
|
|
elif [ "$app_name" = "installed" ]; then
|
|
databaseListInstalledApps
|
|
else
|
|
isNotice "Invalid list type: $app_name"
|
|
cliShowAppHelp
|
|
fi
|
|
;;
|
|
|
|
"install")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerInstallApp "$app_name" "$config" "$reset_network"
|
|
else
|
|
local _mode=""
|
|
for _arg in "$config" "$initial_command5" "$initial_command6"; do
|
|
[[ "$_arg" == "--detach" ]] && _mode="--detach"
|
|
done
|
|
# config / --reset-network passthrough — strip CLI-only
|
|
# flags, keep what dockerInstallApp expects.
|
|
local _passthrough_config="$config"
|
|
[[ "$_passthrough_config" == "--detach" || "$_passthrough_config" == "--reset-network" ]] && _passthrough_config=""
|
|
local _cmd="libreportal app install $app_name"
|
|
[[ -n "$_passthrough_config" ]] && _cmd+=" '$_passthrough_config'"
|
|
[[ "$reset_network" == "true" ]] && _cmd+=" --reset-network"
|
|
cliTaskRun "$_cmd" "install" "$app_name" "$_mode"
|
|
fi
|
|
;;
|
|
|
|
"add")
|
|
# Add an app DEFINITION from the signed registry catalog (the
|
|
# marketplace verb -- docs/roadmap/updates-and-distribution.md §8).
|
|
# Mutating, so it routes through the task system like install.
|
|
if [[ -z "$app_name" ]]; then isError "Usage: libreportal app add <app|artifact-id>"; return 1; fi
|
|
if [[ ! "$app_name" =~ ^[A-Za-z0-9._-]+$ ]]; then isError "app add: argument has unsafe characters"; return 1; fi
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
# Lazy-loader gap: the registry read + apply pipeline live in
|
|
# their own files; mirror the artifact handler's checked source.
|
|
if ! declare -F lpFetchIndex >/dev/null 2>&1; then
|
|
local _f
|
|
for _f in source/fetch.sh source/artifacts.sh; do
|
|
if [[ ! -f "$install_scripts_dir/$_f" ]] || ! source "$install_scripts_dir/$_f"; then
|
|
isError "app add: failed to load the read pipeline ($_f) — try: libreportal regen"; return 1
|
|
fi
|
|
done
|
|
fi
|
|
if ! declare -F appAddFromRegistry >/dev/null 2>&1; then
|
|
local _af="cli/commands/artifact/cli_artifact_apply.sh"
|
|
if [[ ! -f "$install_scripts_dir/$_af" ]] || ! source "$install_scripts_dir/$_af"; then
|
|
isError "app add: failed to load the apply pipeline ($_af) — try: libreportal regen"; return 1
|
|
fi
|
|
fi
|
|
appAddFromRegistry "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app add $app_name" "app_add" "$app_name" ""
|
|
fi
|
|
;;
|
|
|
|
"uninstall")
|
|
# Optional `--delete-images` flag (in any of the trailing
|
|
# positions) tells the uninstall to also remove the app's
|
|
# docker images. Default behaviour: keep them so a reinstall
|
|
# is fast and offline-friendly.
|
|
local _del_images="false"
|
|
local _del_tasks="false"
|
|
local _u_mode=""
|
|
for _arg in "$config" "$initial_command5" "$initial_command6" "$initial_command7"; do
|
|
[[ "$_arg" == "--delete-images" ]] && _del_images="true"
|
|
[[ "$_arg" == "--delete-tasks" ]] && _del_tasks="true"
|
|
[[ "$_arg" == "--detach" ]] && _u_mode="--detach"
|
|
done
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerUninstallApp "$app_name" "$_del_images" "$_del_tasks"
|
|
else
|
|
local _cmd="libreportal app uninstall $app_name"
|
|
[[ "$_del_images" == "true" ]] && _cmd+=" --delete-images"
|
|
[[ "$_del_tasks" == "true" ]] && _cmd+=" --delete-tasks"
|
|
cliTaskRun "$_cmd" "uninstall" "$app_name" "$_u_mode"
|
|
fi
|
|
;;
|
|
|
|
"start")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerStartApp "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app start $app_name" "start" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"stop")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerStopApp "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app stop $app_name" "stop" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"restart")
|
|
# Optional 4th arg = one compose service to restart instead of the
|
|
# whole app (the WebUI Services tab routes per-service restarts
|
|
# here): libreportal app restart <app> [service]
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerRestartApp "$app_name" "$config"
|
|
else
|
|
cliTaskRun "libreportal app restart $app_name${config:+ $config}" "restart" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"up")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerComposeUp "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app up $app_name" "up" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"down")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerComposeDown "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app down $app_name" "down" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"reload")
|
|
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
dockerRestartAppViaInstall "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app reload $app_name" "reload" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"export")
|
|
# A copy you can hand around — not a backup. Runs inline: it stops
|
|
# the app, and a task row that silently stopped something would be
|
|
# worse than watching it happen.
|
|
if [[ -z "$app_name" ]]; then
|
|
isNotice "Usage: app export <app_name> [file]"
|
|
cliShowAppHelp
|
|
else
|
|
appExport "$app_name" "$initial_command4"
|
|
fi
|
|
;;
|
|
|
|
"import")
|
|
# Here $app_name is the FILE, since there is no app yet.
|
|
if [[ -z "$app_name" ]]; then
|
|
isNotice "Usage: app import <file.lpapp>"
|
|
cliShowAppHelp
|
|
elif [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
appImport "$app_name" "$initial_command4"
|
|
else
|
|
cliTaskRun "libreportal app import $app_name" "import" "libreportal"
|
|
fi
|
|
;;
|
|
|
|
"move")
|
|
# `app move <app> <location>` — relocate an app's data to another
|
|
# storage location. Long-running and it stops the app, so it goes
|
|
# through the task processor like backup/restore rather than
|
|
# blocking the CLI.
|
|
if [[ -z "$app_name" || -z "$initial_command4" ]]; then
|
|
isNotice "Usage: app move <app_name> <storage_location>"
|
|
cliShowAppHelp
|
|
elif [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
storageMoveApp "$app_name" "$initial_command4" "$initial_command5"
|
|
else
|
|
cliTaskRun "libreportal app move $app_name $initial_command4" "move" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"backup")
|
|
if [[ -z "$app_name" ]]; then
|
|
isNotice "No app provided."
|
|
cliShowAppHelp
|
|
elif [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
backupAppStart "$app_name"
|
|
else
|
|
cliTaskRun "libreportal app backup $app_name" "backup" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"restore")
|
|
if [[ -z "$app_name" ]]; then
|
|
isNotice "No app provided."
|
|
cliShowAppHelp
|
|
elif [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
|
cliAppRestore "$app_name" "$restore_arg2" "$restore_arg3" "$restore_arg4"
|
|
else
|
|
# Pass the positional args through verbatim. They may
|
|
# include local|remote1|… selector, filename, password —
|
|
# quote each to survive shell re-parse in the processor.
|
|
local _cmd="libreportal app restore $app_name"
|
|
for _a in "$restore_arg2" "$restore_arg3" "$restore_arg4"; do
|
|
[[ -n "$_a" ]] && _cmd+=" '$_a'"
|
|
done
|
|
cliTaskRun "$_cmd" "restore" "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"status")
|
|
if [[ -z "$app_name" ]]; then
|
|
isNotice "No app provided."
|
|
cliShowAppHelp
|
|
else
|
|
appStatus "$app_name"
|
|
fi
|
|
;;
|
|
|
|
"tool")
|
|
# `libreportal app tool list [<app>]` — discover available tools.
|
|
# When the second arg is the literal `list`, the third is treated
|
|
# as an optional app filter. Otherwise the standard run shape
|
|
# applies: `libreportal app tool <app> <tool_id> [args]`.
|
|
if [[ "$app_name" == "list" ]]; then
|
|
cliAppToolList "$tool_name"
|
|
elif [[ -z "$app_name" || -z "$tool_name" ]]; then
|
|
isNotice "Usage: libreportal app tool <app_name> <tool_name> [args]"
|
|
isNotice " libreportal app tool list [<app_name>]"
|
|
cliShowAppHelp
|
|
else
|
|
dockerAppRunTool "$app_name" "$tool_name" "$tool_args"
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
isNotice "Invalid app command: $action"
|
|
cliShowAppHelp
|
|
;;
|
|
esac
|
|
}
|