From 37233f0a4779afac6cbd602efe83352483bb5f12 Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Tue, 30 Jun 2026 19:09:07 +0200 Subject: [PATCH] fix(safeclean): dry-run/apply docker consistency, eCryptfs warning, rotated log cleanup --- safeclean.sh | 62 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) mode change 100644 => 100755 safeclean.sh diff --git a/safeclean.sh b/safeclean.sh old mode 100644 new mode 100755 index ddcd476..551db4b --- a/safeclean.sh +++ b/safeclean.sh @@ -279,14 +279,44 @@ clean_docker() { if ! docker info >/dev/null 2>&1; then printf ' %-26s %sdaemon not reachable%s\n' "Docker" "$c_dim" "$c_rst"; return fi + + local age_h=$(( AGE_DAYS * 24 )) + echo " Docker reclaimable space:" docker system df 2>/dev/null | sed 's/^/ /' - [ "$DRY_RUN" = 1 ] && return + + if [ "$DRY_RUN" = 1 ]; then + if [ "$AGGRESSIVE_DOCKER" = 0 ] && [ "$AGE_DAYS" -gt 0 ]; then + # apply uses --filter until=Xh, so docker system df overstates actual reclaim. + # Estimate: sum sizes of dangling images older than age_h hours (what prune targets). + local img_bytes=0 img_count=0 id s + while IFS= read -r id; do + [ -z "$id" ] && continue + img_count=$(( img_count + 1 )) + s=$(docker image inspect --format '{{.Size}}' "$id" 2>/dev/null) || s=0 + img_bytes=$(( img_bytes + ${s:-0} )) + done < <(docker image ls --filter "dangling=true" --filter "until=${age_h}h" \ + --format "{{.ID}}" 2>/dev/null) + local img_human + img_human=$(numfmt --to=si --suffix=B -- "$img_bytes" 2>/dev/null \ + || echo "${img_bytes}B") + echo + printf ' %sNOTE: apply uses --filter until=%dh — only items older than %d day(s) are removed.%s\n' \ + "$c_yel" "$age_h" "$AGE_DAYS" "$c_rst" + printf ' Dangling images >%dd apply would actually remove: %s%s%s (%d image(s))\n' \ + "$AGE_DAYS" "$c_bold" "$img_human" "$c_rst" "$img_count" + printf ' %s(stopped containers + build cache also contribute; totals shown above)%s\n' \ + "$c_dim" "$c_rst" + printf ' %sTip: --docker-all removes ALL unused images regardless of age.%s\n' \ + "$c_dim" "$c_rst" + fi + return + fi local flags="-f" [ "$AGGRESSIVE_DOCKER" = 1 ] && flags="-af" # -a also removes unused (not just dangling) images local until_args=() - [ "$AGE_DAYS" -gt 0 ] && until_args=(--filter "until=$(( AGE_DAYS * 24 ))h") + [ "$AGE_DAYS" -gt 0 ] && until_args=(--filter "until=${age_h}h") if confirm "Run 'docker system prune ${flags} ${until_args[*]}' (volumes are NOT touched)?"; then docker system prune $flags "${until_args[@]}" else @@ -298,9 +328,16 @@ clean_system() { echo echo "${c_bold}System (sudo) ─────────────────────────────────────────────${c_rst}" if [ "$DRY_RUN" = 1 ]; then - echo " ${c_dim}dry-run: would offer apt clean, journald vacuum, old snap removal${c_rst}" + echo " ${c_dim}dry-run: would offer apt clean, journald vacuum, rotated log cleanup, old snap removal${c_rst}" echo " apt archive cache:"; sudo -n du -sh /var/cache/apt/archives 2>/dev/null | sed 's/^/ /' || true echo " journal logs:"; journalctl --disk-usage 2>/dev/null | sed 's/^/ /' || true + echo " rotated logs (/var/log/*.gz, *.1, *.2 …):" + local rot_total + rot_total=$(find /var/log -maxdepth 1 -type f \ + \( -name '*.gz' -o -name '*.[0-9]' -o -name '*.[0-9][0-9]' \) \ + -print0 2>/dev/null | xargs -0 -r du -sch 2>/dev/null \ + | tail -n1 | awk '{print $1}') || rot_total="" + printf ' %s%s%s\n' "$c_bold" "${rot_total:-0}" "$c_rst" return fi @@ -314,6 +351,22 @@ clean_system() { sudo journalctl --vacuum-time="${jdays}d" fi + local rotated=() + while IFS= read -r -d '' f; do rotated+=("$f"); done < <( + find /var/log -maxdepth 1 -type f \ + \( -name '*.gz' -o -name '*.[0-9]' -o -name '*.[0-9][0-9]' \) \ + -print0 2>/dev/null + ) + if [ ${#rotated[@]} -gt 0 ]; then + local rot_size + rot_size=$(du -sch "${rotated[@]}" 2>/dev/null | tail -n1 | awk '{print $1}') + if confirm "Delete rotated system logs (${rot_size}, ${#rotated[@]} file(s) — not active logs)?"; then + sudo rm -f -- "${rotated[@]}" && printf ' %sremoved.%s\n' "$c_grn" "$c_rst" + else + echo " skipped." + fi + fi + if confirm "snap: remove disabled (old) revisions?"; then snap list --all 2>/dev/null | awk '/disabled/{print $1, $3}' | \ while read -r name rev; do @@ -342,6 +395,9 @@ echo echo "${c_bold}Largest items under $SCAN_ROOT:${c_rst}" du -h --max-depth=1 "$SCAN_ROOT" 2>/dev/null | sort -rh | head -n 12 | sed 's/^/ /' +if [ -d "${SCAN_ROOT}/.Private" ] && mount 2>/dev/null | grep -q " on ${SCAN_ROOT} type ecryptfs"; then + printf ' %sNote: eCryptfs detected — .Private and home directory are the same data; sizes above may be double-counted.%s\n' "$c_yel" "$c_rst" +fi echo echo "${c_bold}Reclaimable categories ──────────────────────────────────────${c_rst}"