feat(app): import-check — inspect an export without importing it
`libreportal app import-check <file-or-directory>` reports what an import would do, as one JSON object per file, without touching anything. It runs the same checks appImport makes — app still shipped, not already installed, fits on the target, storage location still exists — so a UI can show them and ask for acceptance before acting. This is what makes a path-based import safe to drive from the WebUI when the repository restore is not. A .lpapp is a plain tarball, not encrypted, so there is no password to collect and nothing secret crosses from the browser to the host — the blocker recorded in first-run-restore.md §4.1 simply does not apply. Accepts a single file or a directory of them, so "point at this folder" works as well as "point at this file". Verified against real .lpapp files built for the purpose: app name read from the tar's top-level directory rather than the filename, size read from the manifest, a location the machine does not have downgraded to a warning naming the fallback, and refusals for an already-installed app and a file that is not an export at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ef60cce98a
commit
da0136149b
@ -110,6 +110,106 @@ appImportName()
|
||||
tar -tzf "$file" 2>/dev/null | head -1 | cut -d/ -f1
|
||||
}
|
||||
|
||||
# Inspect one or more exports WITHOUT importing anything, and report what would
|
||||
# happen. This is what lets the WebUI ask for acceptance before it acts: the
|
||||
# same checks appImport makes, run early and rendered as a list.
|
||||
#
|
||||
# A .lpapp is a plain tarball — not encrypted — so unlike a backup repository
|
||||
# there is no password to collect, and nothing secret has to cross from the
|
||||
# browser to the host. That is the whole reason this can live in the WebUI when
|
||||
# the repository restore cannot.
|
||||
#
|
||||
# Emits one JSON object per line: {"file","app","size_bytes","verdict","detail"}
|
||||
# verdict is ok | warn | refuse.
|
||||
appImportCheck()
|
||||
{
|
||||
local target="$1"
|
||||
if [[ -z "$target" ]]; then
|
||||
isError "Usage: app import-check <file-or-directory>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a files=()
|
||||
if [[ -d "$target" ]]; then
|
||||
while IFS= read -r f; do [[ -n "$f" ]] && files+=("$f"); done \
|
||||
< <(find "$target" -maxdepth 1 -type f -name "*.${lpAppExtension}" 2>/dev/null | sort)
|
||||
elif [[ -f "$target" ]]; then
|
||||
files=("$target")
|
||||
else
|
||||
printf '{"file":"%s","app":"","size_bytes":0,"verdict":"refuse","detail":"No such file or directory"}\n' "$(_lpJsonStr "$target")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
printf '{"file":"%s","app":"","size_bytes":0,"verdict":"refuse","detail":"No .%s files found there"}\n' \
|
||||
"$(_lpJsonStr "$target")" "$lpAppExtension"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local f app manifest size_bytes loc dir verdict detail
|
||||
for f in "${files[@]}"; do
|
||||
verdict="ok"; detail="ready to import"
|
||||
app=$(appImportName "$f")
|
||||
|
||||
if [[ -z "$app" || ! "$app" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
_lpImportRow "$f" "" 0 refuse "Not a LibrePortal export"
|
||||
continue
|
||||
fi
|
||||
|
||||
manifest=$(appImportManifest "$f")
|
||||
size_bytes=$(printf '%s' "$manifest" | grep -o '"size_bytes":[0-9]*' | head -1 | cut -d: -f2)
|
||||
loc=$(printf '%s' "$manifest" | grep -o '"location":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
[[ "$size_bytes" =~ ^[0-9]+$ ]] || size_bytes=0
|
||||
|
||||
if [[ ! -f "${install_containers_dir%/}/$app/$app.config" ]]; then
|
||||
_lpImportRow "$f" "$app" "$size_bytes" refuse "This version no longer ships $app"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! dir=$(appDir "$app" 2>/dev/null); then
|
||||
_lpImportRow "$f" "$app" "$size_bytes" refuse "Its storage location is not mounted"
|
||||
continue
|
||||
fi
|
||||
if [[ -d "$dir" ]]; then
|
||||
_lpImportRow "$f" "$app" "$size_bytes" refuse "$app is already installed — uninstall it first"
|
||||
continue
|
||||
fi
|
||||
|
||||
if (( size_bytes > 0 )); then
|
||||
local need_kb=$(( size_bytes / 1024 )) avail_kb
|
||||
avail_kb=$(df -Pk "${dir%/*}" 2>/dev/null | awk 'NR==2 {print $4}')
|
||||
if [[ -n "$avail_kb" ]] && (( avail_kb < need_kb )); then
|
||||
_lpImportRow "$f" "$app" "$size_bytes" refuse \
|
||||
"Needs $(( need_kb / 1048576 ))G, $(( avail_kb / 1048576 ))G free"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$loc" && "$loc" != "default" && "$loc" != "primary" ]] \
|
||||
&& ! storageLocationPath "$loc" >/dev/null 2>&1; then
|
||||
verdict="warn"
|
||||
detail="came from location \"$loc\", which this machine does not have — will use $(storageLocationName "${dir%/*}")"
|
||||
fi
|
||||
|
||||
_lpImportRow "$f" "$app" "$size_bytes" "$verdict" "$detail"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
_lpJsonStr()
|
||||
{
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/ }"; s="${s//$'\n'/ }"; s="${s//$'\r'/}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
_lpImportRow()
|
||||
{
|
||||
printf '{"file":"%s","app":"%s","size_bytes":%s,"verdict":"%s","detail":"%s"}\n' \
|
||||
"$(_lpJsonStr "$1")" "$(_lpJsonStr "$2")" "${3:-0}" "$4" "$(_lpJsonStr "$5")"
|
||||
}
|
||||
|
||||
appImport()
|
||||
{
|
||||
local file="$1"
|
||||
|
||||
@ -170,6 +170,17 @@ cliHandleAppCommands()
|
||||
fi
|
||||
;;
|
||||
|
||||
"import-check")
|
||||
# Report what an import WOULD do, without doing it. Read-only, so it
|
||||
# runs inline rather than through a task — the WebUI needs the answer
|
||||
# before it can ask for acceptance.
|
||||
if [[ -z "$app_name" ]]; then
|
||||
isNotice "Usage: app import-check <file-or-directory>"
|
||||
else
|
||||
appImportCheck "$app_name"
|
||||
fi
|
||||
;;
|
||||
|
||||
"import")
|
||||
# Here $app_name is the FILE, since there is no app yet.
|
||||
if [[ -z "$app_name" ]]; then
|
||||
|
||||
@ -48,6 +48,7 @@ declare -gA LP_FN_MAP=(
|
||||
[appGluetunRecreateRouted]="gluetun/scripts/gluetun_recreate_routed.sh"
|
||||
[appGluetunRefreshProviders]="gluetun/tools/gluetun_refresh_providers.sh"
|
||||
[appImport]="app/app_portable.sh"
|
||||
[appImportCheck]="app/app_portable.sh"
|
||||
[appImportManifest]="app/app_portable.sh"
|
||||
[appImportName]="app/app_portable.sh"
|
||||
[appInstallCheckRequirements]="checks/requirements/check_app_install.sh"
|
||||
@ -676,6 +677,7 @@ declare -gA LP_FN_MAP=(
|
||||
[lpFetchSource]="source/fetch.sh"
|
||||
[_lpFetchTool]="source/fetch.sh"
|
||||
[lpFootprintStale]="source/fetch.sh"
|
||||
[_lpImportRow]="app/app_portable.sh"
|
||||
[lpIndexArtifactIds]="source/artifacts.sh"
|
||||
[lpIndexTop]="source/artifacts.sh"
|
||||
[lpInstalledFootprintVersion]="source/fetch.sh"
|
||||
@ -1280,6 +1282,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[appGluetunRecreateRouted]="containers"
|
||||
[appGluetunRefreshProviders]="containers"
|
||||
[appImport]="scripts"
|
||||
[appImportCheck]="scripts"
|
||||
[appImportManifest]="scripts"
|
||||
[appImportName]="scripts"
|
||||
[appInstallCheckRequirements]="scripts"
|
||||
@ -1908,6 +1911,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[lpFetchSource]="scripts"
|
||||
[_lpFetchTool]="scripts"
|
||||
[lpFootprintStale]="scripts"
|
||||
[_lpImportRow]="scripts"
|
||||
[lpIndexArtifactIds]="scripts"
|
||||
[lpIndexTop]="scripts"
|
||||
[lpInstalledFootprintVersion]="scripts"
|
||||
@ -2550,6 +2554,7 @@ appGiteaSetAdmin() { unset -f appGiteaSetAdmin; __lpAutoload "${install_containe
|
||||
appGluetunRecreateRouted() { unset -f appGluetunRecreateRouted; __lpAutoload "${install_containers_dir}gluetun/scripts/gluetun_recreate_routed.sh"; appGluetunRecreateRouted "$@"; }
|
||||
appGluetunRefreshProviders() { unset -f appGluetunRefreshProviders; __lpAutoload "${install_containers_dir}gluetun/tools/gluetun_refresh_providers.sh"; appGluetunRefreshProviders "$@"; }
|
||||
appImport() { unset -f appImport; __lpAutoload "${install_scripts_dir}app/app_portable.sh"; appImport "$@"; }
|
||||
appImportCheck() { unset -f appImportCheck; __lpAutoload "${install_scripts_dir}app/app_portable.sh"; appImportCheck "$@"; }
|
||||
appImportManifest() { unset -f appImportManifest; __lpAutoload "${install_scripts_dir}app/app_portable.sh"; appImportManifest "$@"; }
|
||||
appImportName() { unset -f appImportName; __lpAutoload "${install_scripts_dir}app/app_portable.sh"; appImportName "$@"; }
|
||||
appInstallCheckRequirements() { unset -f appInstallCheckRequirements; __lpAutoload "${install_scripts_dir}checks/requirements/check_app_install.sh"; appInstallCheckRequirements "$@"; }
|
||||
@ -3178,6 +3183,7 @@ lpFetchRelease() { unset -f lpFetchRelease; __lpAutoload "${install_scripts_dir}
|
||||
lpFetchSource() { unset -f lpFetchSource; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpFetchSource "$@"; }
|
||||
_lpFetchTool() { unset -f _lpFetchTool; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpFetchTool "$@"; }
|
||||
lpFootprintStale() { unset -f lpFootprintStale; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpFootprintStale "$@"; }
|
||||
_lpImportRow() { unset -f _lpImportRow; __lpAutoload "${install_scripts_dir}app/app_portable.sh"; _lpImportRow "$@"; }
|
||||
lpIndexArtifactIds() { unset -f lpIndexArtifactIds; __lpAutoload "${install_scripts_dir}source/artifacts.sh"; lpIndexArtifactIds "$@"; }
|
||||
lpIndexTop() { unset -f lpIndexTop; __lpAutoload "${install_scripts_dir}source/artifacts.sh"; lpIndexTop "$@"; }
|
||||
lpInstalledFootprintVersion() { unset -f lpInstalledFootprintVersion; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpInstalledFootprintVersion "$@"; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user