diff --git a/init.sh b/init.sh index 61a3e95..8fe65c9 100755 --- a/init.sh +++ b/init.sh @@ -2262,6 +2262,22 @@ runFullUninstall() fi fi + # The storage registry lives inside the helpers directory about to be + # removed, so read it NOW — the summary at the end of this function reports + # which locations still hold app data, and reading it afterwards found + # nothing and said nothing, which is the exact silence it exists to prevent. + local _lp_left_report="" + if [[ -r /usr/local/lib/libreportal/storage.roots ]]; then + local _rid _rpath _rrest _rn + while IFS=$'\t' read -r _rid _rpath _rrest || [[ -n "$_rid" ]]; do + [[ -z "$_rpath" || "$_rid" == \#* ]] && continue + [[ -d "$_rpath" ]] || continue + _rn=$(find "$_rpath" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) + (( _rn > 0 )) || continue + _lp_left_report+=" $(printf '%-40s %s app director%s' "$_rpath" "$_rn" "$( (( _rn == 1 )) && echo y || echo ies )")"$'\n' + done < /usr/local/lib/libreportal/storage.roots + fi + # 3. Remove the out-of-/docker footprint (see docs/architecture/system-footprint.md). rm -f /usr/local/bin/libreportal /usr/local/bin/libreportal-uninstall /usr/local/bin/libreportal-relocate rm -rf /usr/local/lib/libreportal @@ -2323,29 +2339,15 @@ runFullUninstall() # — but doing one silently and the other silently is the one option nobody # can predict. So say exactly what is still there. # - # It also stops being obvious: the directories are owned by a uid that no - # longer maps to a user once the container account is gone, and each location - # keeps its .libreportal-storage marker, which a later install will recognise - # and adopt. - local _reg="/usr/local/lib/libreportal/storage.roots" - [[ -r "$_reg" ]] || _reg="${lp_lib_dir:-/usr/local/lib/libreportal}/storage.roots" - if [[ -r "$_reg" ]]; then - local _id _path _rest _left=0 - while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do - [[ -z "$_path" || "$_id" == \#* ]] && continue - [[ -d "$_path" ]] || continue - local _n; _n=$(find "$_path" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) - (( _n > 0 )) || continue - (( _left == 0 )) && { echo ""; isNotice "App data left on these storage locations — nothing here was deleted:"; } - printf ' %-40s %s app director%s\n' "$_path" "$_n" "$( (( _n == 1 )) && echo y || echo ies )" - _left=1 - done < "$_reg" - if (( _left )); then - echo "" - printf ' %s\n' "Remove them by hand if you meant to, or leave them — a reinstall can adopt" - printf ' %s\n' "the location again and the apps restore back onto it." - echo "" - fi + # Read from the snapshot taken before the registry was deleted, above. + if [[ -n "$_lp_left_report" ]]; then + echo "" + isNotice "App data left on these storage locations — nothing here was deleted:" + printf '%s' "$_lp_left_report" + echo "" + printf ' %s\n' "Remove them by hand if you meant to, or leave them — a reinstall can adopt" + printf ' %s\n' "the location again and the apps restore back onto it." + echo "" fi } diff --git a/scripts/dev/lp-uninstall-report-test b/scripts/dev/lp-uninstall-report-test new file mode 100755 index 0000000..228ffad --- /dev/null +++ b/scripts/dev/lp-uninstall-report-test @@ -0,0 +1,73 @@ +#!/bin/bash +# Does the teardown still say what it left behind, after deleting the thing it +# reads that from? +# +# scripts/dev/lp-uninstall-report-test +# +# A full uninstall removes the primary containers root and everything in it, but +# app data on an ADDITIONAL storage location survives. Leaving it is right — +# deleting someone's data off a separate disk unasked is worse — but saying +# nothing about it is not, especially as those directories end up owned by a uid +# that stops mapping to a user once the container account is gone. +# +# The report reads the storage registry, which lives INSIDE the helpers +# directory the same function deletes. Written after that deletion it found an +# empty registry and printed nothing: the exact silence it exists to prevent, +# and indistinguishable from "there was nothing left". So what this guards is +# not the wording but the ORDER — the capture has to happen before the delete. +# +# Runs the real fragments from init.sh with the deletion between them. + +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-unreport-XXXXXX")" +trap 'rm -rf "$BASE"' EXIT + +fail=0 +chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } + +CAPTURE=$(awk '/^\t# The storage registry lives inside the helpers directory/,/^\tfi$/' "$REPO/init.sh") +REPORT=$(awk '/^\t# App data on an ADDITIONAL storage location is not removed/,/^\tfi$/' "$REPO/init.sh") +[[ -n "$CAPTURE" ]] || { echo " FAIL could not extract the capture block from init.sh"; exit 1; } +[[ -n "$REPORT" ]] || { echo " FAIL could not extract the report block from init.sh"; exit 1; } + +# A registry where the helpers live, and data on one of the two locations. +mkdir -p "$BASE/lib" "$BASE/loc1/appA" "$BASE/loc1/appB" "$BASE/loc2" "$BASE/gone" +printf '1\t%s\t1\tuuid\n2\t%s\t2\tuuid\n3\t%s\t3\tuuid\n' \ + "$BASE/loc1" "$BASE/loc2" "$BASE/nonexistent" > "$BASE/lib/storage.roots" + +run() { + local tmp; tmp=$(mktemp) + { + echo 'isNotice(){ echo "NOTICE: $*"; }' + # The real paths are absolute; point them at the fixture. + printf '%s\n' "$CAPTURE" | sed "s#/usr/local/lib/libreportal#$BASE/lib#g" + # The deletion that sits between them in the real function. + echo "rm -rf \"$BASE/lib\"" + printf '%s\n' "$REPORT" + } > "$tmp" + bash "$tmp" 2>&1 + rm -f "$tmp" +} + +out=$(run) + +echo "--- it reports what survived, after the registry is gone ---" +chk "registry really was deleted" "$([[ -e "$BASE/lib" ]] && echo yes || echo no)" "no" +chk "says something" "$(grep -c 'App data left' <<< "$out")" "1" +chk "names the location" "$(grep -c "$BASE/loc1" <<< "$out")" "1" +chk "counts its apps" "$(grep -c '2 app directories' <<< "$out")" "1" + +echo "--- and stays quiet about locations with nothing on them ---" +chk "empty location omitted" "$(grep -c "$BASE/loc2" <<< "$out")" "0" +chk "missing path omitted" "$(grep -c 'nonexistent' <<< "$out")" "0" + +echo "--- nothing left anywhere: no report at all ---" +rm -rf "$BASE/loc1/appA" "$BASE/loc1/appB" +mkdir -p "$BASE/lib" +printf '1\t%s\t1\tuuid\n' "$BASE/loc1" > "$BASE/lib/storage.roots" +out=$(run) +chk "silent when there is nothing to say" "$(grep -c 'App data left' <<< "$out")" "0" + +echo "" +if (( fail )); then echo "FAILED"; exit 1; fi +echo "All uninstall-report checks passed."