diff --git a/safeclean.sh b/safeclean.sh index 551db4b..f310fea 100755 --- a/safeclean.sh +++ b/safeclean.sh @@ -8,16 +8,27 @@ # What it targets (all regenerable / safe to lose): # core dumps, Python caches, virtualenvs, node_modules, build targets, # ~/.m2, gradle/pip/npm/yarn/go caches, trash, thumbnails, Docker cruft, -# and (with --system) apt cache, journald logs, old snap revisions. +# browser & IDE caches (only under ~/.cache — never profiles), tool +# download caches (playwright/puppeteer/electron), ML model caches +# (huggingface/torch — models re-download on demand!), cargo registry +# cache, unused rustup toolchains, Flatpak app caches, PlatformIO's +# .cache — and, only with the explicit --platformio flag, PlatformIO +# packages/platforms (regenerable via pio, but slow to re-fetch). +# With --system: apt cache, journald logs, rotated logs, old snap +# revisions, systemd coredumps. # -# What it NEVER touches: your documents, media, and especially cloud-sync -# folders (Dropbox / sync / CosmoseGDrive) — those are pruned from every scan. +# What it NEVER touches: your documents, media, ~/projects, +# ~/homelab-preload, and especially cloud-sync folders (Dropbox / sync / +# CosmoseGDrive) — those are pruned from every scan. Big mixed dirs +# (~/.local/share, ~/.config, ~/.cache as a whole) are only REPORTED +# for manual review, never deleted. # # Usage: # ./safeclean.sh # report only (safe, default) # ./safeclean.sh --apply # delete, asking before each category # ./safeclean.sh --apply -y # delete without per-category prompts # ./safeclean.sh --apply --system # also clean system stuff (uses sudo) +# ./safeclean.sh --platformio # also offer PlatformIO packages/platforms # ./safeclean.sh --root /path # scan a different tree (default: $HOME) # ./safeclean.sh --days 30 # only items untouched for 30+ days (default 7) # ./safeclean.sh --no-age # ignore age, consider everything eligible @@ -33,6 +44,8 @@ ASSUME_YES=0 DO_SYSTEM=0 VERBOSE=0 AGGRESSIVE_DOCKER=0 # set via --docker-all to drop ALL unused images +CLEAN_PLATFORMIO=0 # set via --platformio to also offer ~/.platformio + # packages/platforms (regenerable via pio, but slow) AGE_DAYS=7 # only touch items NOT modified within this many days; # set to 0 (or --no-age) to ignore age entirely @@ -48,6 +61,8 @@ PRUNE_DIRS=( "$HOME/snap" # per-app snap data "$HOME/.steam" "$HOME/Steam" + "$HOME/projects" # real project data — hands off + "$HOME/homelab-preload" # preloaded data, expensive to re-fetch ) # ------------------------------- arg parsing -------------------------------- @@ -58,12 +73,15 @@ while [ $# -gt 0 ]; do -y|--yes) ASSUME_YES=1 ;; --system) DO_SYSTEM=1 ;; --docker-all) AGGRESSIVE_DOCKER=1 ;; + --platformio) CLEAN_PLATFORMIO=1 ;; --days) shift; AGE_DAYS="${1:?--days needs a number}" ;; --no-age) AGE_DAYS=0 ;; -v|--verbose) VERBOSE=1 ;; --root) shift; SCAN_ROOT="${1:?--root needs a path}" ;; -h|--help) - sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//' + # print the header comment block (everything up to the first + # non-comment line), so help never drifts out of sync + awk 'NR > 1 && !/^#/ { exit } NR > 1 { sub(/^# ?/, ""); print }' "$0" exit 0 ;; *) echo "unknown option: $1 (try --help)" >&2; exit 2 ;; esac @@ -249,6 +267,123 @@ scan_user_caches() { report_and_clean "Dev & app caches" } +# Browser & IDE caches: only the ~/.cache subtrees — profiles (~/.mozilla, +# ~/.config/google-chrome, ~/.config/JetBrains) are never touched. +scan_browser_ide_caches() { + reset_targets + local d + for d in \ + "$HOME/.cache/JetBrains" \ + "$HOME/.cache/google-chrome" \ + "$HOME/.cache/chromium" \ + "$HOME/.cache/mozilla" + do + [ -e "$d" ] && TARGETS+=("$d") + done + report_and_clean "Browser & IDE caches" +} + +# Downloaded browser/runtime bundles; re-fetched on demand by the tool. +scan_tool_download_caches() { + reset_targets + local d + for d in \ + "$HOME/.cache/ms-playwright" \ + "$HOME/.cache/puppeteer" \ + "$HOME/.cache/electron" \ + "$HOME/.cache/electron-builder" + do + [ -e "$d" ] && TARGETS+=("$d") + done + report_and_clean "Tool download caches" +} + +# ML model caches: fully regenerable, but re-downloading can mean many GB. +scan_ml_caches() { + reset_targets + local d + for d in "$HOME/.cache/huggingface" "$HOME/.cache/torch"; do + [ -e "$d" ] && TARGETS+=("$d") + done + if [ ${#TARGETS[@]} -gt 0 ]; then + printf ' %sNote: ML model caches — models re-download on demand (can be many GB).%s\n' \ + "$c_yel" "$c_rst" + fi + report_and_clean "ML model caches" +} + +# Downloaded .crate archives; cargo re-fetches them on demand. +# (pip wheels live under ~/.cache/pip, already covered by "Dev & app caches".) +scan_cargo_registry_cache() { + reset_targets + [ -e "$HOME/.cargo/registry/cache" ] && TARGETS+=("$HOME/.cargo/registry/cache") + report_and_clean "Cargo registry cache" +} + +# Old rustup toolchains: regenerable via `rustup toolchain install`, but the +# default toolchain must survive. If we can't tell which one that is, we skip +# the whole category rather than guess. +scan_rustup_toolchains() { + reset_targets + local tdir="$HOME/.rustup/toolchains" def="" d + if [ -d "$tdir" ]; then + if command -v rustup >/dev/null 2>&1; then + def=$(rustup show active-toolchain 2>/dev/null | awk 'NR==1{print $1}') + fi + if [ -z "$def" ] && [ -f "$HOME/.rustup/settings.toml" ]; then + def=$(awk -F'"' '/^default_toolchain/{print $2}' "$HOME/.rustup/settings.toml") + fi + if [ -z "$def" ]; then + printf ' %-26s %scannot determine default toolchain — skipped%s\n' \ + "Rust toolchains (unused)" "$c_dim" "$c_rst" + return + fi + for d in "$tdir"/*/; do + d="${d%/}" + [ -d "$d" ] || continue + [ "$(basename "$d")" = "$def" ] && continue + TARGETS+=("$d") + done + fi + report_and_clean "Rust toolchains (unused)" +} + +scan_flatpak_caches() { + reset_targets + local d + for d in "$HOME"/.var/app/*/cache; do + [ -d "$d" ] && TARGETS+=("$d") + done + report_and_clean "Flatpak app caches" +} + +# PlatformIO packages/platforms: regenerable (pio re-downloads on the next +# build) but slow to re-fetch, so only offered behind the explicit +# --platformio flag. ~/.platformio/.cache is always covered ("Dev & app +# caches" above). +scan_platformio_packages() { + local pkgs="$HOME/.platformio/packages" plats="$HOME/.platformio/platforms" + if [ "$CLEAN_PLATFORMIO" = 0 ]; then + if [ -d "$pkgs" ] || [ -d "$plats" ]; then + local sz + sz=$(du -sch "$pkgs" "$plats" 2>/dev/null | tail -n1 | awk '{print $1}') + printf ' %-26s %s%s — skipped (opt in with --platformio)%s\n' \ + "PlatformIO pkgs/platforms" "$c_dim" "${sz:-?}" "$c_rst" + fi + return + fi + reset_targets + local d + for d in "$pkgs" "$plats"; do + [ -e "$d" ] && TARGETS+=("$d") + done + if [ ${#TARGETS[@]} -gt 0 ]; then + printf ' %sWARNING: pio will re-download packages/platforms on the next build (slow).%s\n' \ + "$c_yel" "$c_rst" + fi + report_and_clean "PlatformIO pkgs/platforms" +} + # Go's caches need their own tool: module cache files are read-only, so # `go clean` is the correct way to drop them (with a chmod+rm fallback). clean_go_cache() { @@ -328,9 +463,10 @@ 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, rotated log cleanup, old snap removal${c_rst}" + echo " ${c_dim}dry-run: would offer apt clean, journald vacuum, rotated log cleanup, old snap removal, systemd coredump cleanup${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 " systemd coredumps:"; sudo -n du -sh /var/lib/systemd/coredump 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 \ @@ -367,6 +503,21 @@ clean_system() { fi fi + if [ -d /var/lib/systemd/coredump ]; then + local cd_size + cd_size=$(sudo du -sh /var/lib/systemd/coredump 2>/dev/null | awk '{print $1}') + if confirm "systemd coredumps: delete dumps older than ${AGE_DAYS} day(s) (dir total: ${cd_size:-?})?"; then + if [ "$AGE_DAYS" -gt 0 ]; then + sudo find /var/lib/systemd/coredump -type f -mtime "+$AGE_DAYS" -delete + else + sudo find /var/lib/systemd/coredump -type f -delete + fi + 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 @@ -375,6 +526,23 @@ clean_system() { fi } +# Report-only: the biggest subdirectories of trees that MIX regenerable junk +# with real data (app state, settings, histories). This function never +# deletes anything and never feeds TARGETS — the owner reviews by hand. +report_large_dirs() { + echo + echo "${c_bold}Large dirs for MANUAL review (NEVER deleted by this script) ─${c_rst}" + echo " ${c_dim}Top 15 subdirectories of ~/.local/share, ~/.config and ~/.cache by size:${c_rst}" + local r + { + for r in "$HOME/.local/share" "$HOME/.config" "$HOME/.cache"; do + # per-subdir sizes; drop du's trailing total line for the root itself + [ -d "$r" ] && du -h --max-depth=1 "$r" 2>/dev/null | sed '$d' + done + } | sort -rh | head -n 15 | sed 's/^/ /' + echo " ${c_dim}Review by hand: du -sh — these may hold real data, so decide yourself.${c_rst}" +} + # --------------------------------- run -------------------------------------- echo "${c_bold}safeclean${c_rst} — root: $SCAN_ROOT" @@ -407,11 +575,20 @@ scan_virtualenvs scan_node_modules scan_build_targets scan_user_caches +scan_browser_ide_caches +scan_tool_download_caches +scan_ml_caches +scan_cargo_registry_cache +scan_rustup_toolchains +scan_flatpak_caches +scan_platformio_packages clean_go_cache clean_docker [ "$DO_SYSTEM" = 1 ] && clean_system +report_large_dirs + echo if [ "$DRY_RUN" = 1 ]; then echo "Done. (this was a dry-run — add --apply to delete)"