#!/usr/bin/env bash set -euo pipefail # Arch Sanity Cleanup Script (with rclone-mount exclusions) # - conservative defaults # - prompts before destructive actions # - avoids scanning rclone mounts (OneDrive/Nextcloud) # - preserves model caches and Docker networks (2026-09-10) # ----------------- CONFIG: set your rclone mountpoints here ----------------- ONEDRIVE_MOUNT="${ONEDRIVE_MOUNT:-$HOME/OneDrive}" NEXTCLOUD_MOUNT="${NEXTCLOUD_MOUNT:-$HOME/Nextcloud}" EXCLUDE_PATHS=("$ONEDRIVE_MOUNT" "$NEXTCLOUD_MOUNT") # --------------------------------------------------------------------------- have() { command -v "$1" >/dev/null 2>&1; } hr() { printf "\n%s\n" "------------------------------------------------------------"; } ask() { local prompt="$1" read -r -p "$prompt [y/N] " reply [[ "${reply:-}" =~ ^[Yy]$ ]] } bytes_free_root() { df -B1 --output=avail / | tail -n1 | tr -d ' ' } to_gib() { python - <<'PY' import sys b=int(sys.stdin.read().strip() or "0") print(f"{b/1024/1024/1024:.2f} GiB") PY } report_space() { local b b="$(bytes_free_root)" echo "Free space on / : $(printf "%s" "$b" | to_gib)" } is_mountpoint() { local p="$1" [[ -n "$p" ]] && mountpoint -q "$p" 2>/dev/null } du_safe() { # du with exclusions for rclone mounts local path="$1" shift || true local args=(du -sh) for ex in "${EXCLUDE_PATHS[@]}"; do args+=(--exclude="$ex") done "${args[@]}" "$path" "$@" 2>/dev/null || true } find_bigfiles_safe() { # find with pruning rclone mounts local root="$1" local minsize="$2" # e.g. +2G shift 2 || true # If the root itself is (or is under) a mount we want to exclude, bail. for ex in "${EXCLUDE_PATHS[@]}"; do [[ "$root" == "$ex" || "$root" == "$ex/"* ]] && return 0 done # Build prune expression for all excluded paths that exist local prune=() for ex in "${EXCLUDE_PATHS[@]}"; do [[ -e "$ex" ]] && prune+=( -path "$ex" -o -path "$ex/*" -o ) done if (( ${#prune[@]} > 0 )); then # Remove trailing -o unset 'prune[${#prune[@]}-1]' find "$root" \( "${prune[@]}" \) -prune -o -type f -size "$minsize" -printf "%10s %p\n" 2>/dev/null \ | sort -nr | head -n 15 || true else find "$root" -type f -size "$minsize" -printf "%10s %p\n" 2>/dev/null \ | sort -nr | head -n 15 || true fi } cleanup_safe_user_caches() { local cache_root="${1:-${XDG_CACHE_HOME:-$HOME/.cache}}" local dir echo "Gezielte User-Cache-Bereinigung: nur Thumbnail-/Fontconfig-Dateien aelter als 30 Tage." echo "Andere Caches, insbesondere Hugging-Face-Modelle, bleiben erhalten." if [[ -L "$cache_root" ]]; then echo "Cache-Wurzel ist ein Symlink; Bereinigung uebersprungen." return 0 fi for dir in "$cache_root/thumbnails" "$cache_root/fontconfig"; do [[ -d "$dir" && ! -L "$dir" ]] || continue if is_mountpoint "$dir"; then echo "Mountpoint bleibt erhalten: $dir" continue fi du -sh "$dir" 2>/dev/null || true if ask "Alte Dateien in $dir entfernen?"; then find "$dir" -xdev -type f -mtime +30 -delete fi done } cleanup_docker_preserving_networks() { if ! docker info >/dev/null 2>&1; then echo "Docker nicht erreichbar; Bereinigung uebersprungen." return 0 fi docker system df || true echo "Docker-Netze und Volumes bleiben bei dieser Bereinigung erhalten." if ask "Gestoppte Container, dangling Images und Build-Cache entfernen?"; then docker container prune -f docker image prune -f docker builder prune -f fi if ask "Auch alle ungenutzten Docker-Images entfernen (-a)?"; then docker image prune -a -f fi } # ----------------- intro ----------------- hr echo "Arch Sanity Cleanup" echo "Date: $(date)" report_space hr echo "Excluding rclone mounts from scans:" for ex in "${EXCLUDE_PATHS[@]}"; do if [[ -e "$ex" ]]; then if is_mountpoint "$ex"; then echo " - $ex (mountpoint ✅ excluded)" else echo " - $ex (path exists, but not a mountpoint ⚠️ still excluded)" fi else echo " - $ex (not found ⚠️ will be ignored)" fi done # ----------------- quick size hints ----------------- hr echo "Quick size hints (excluding rclone mounts; may take a bit):" for p in /var /var/cache /var/log "$HOME" "$HOME/.cache"; do [[ -e "$p" ]] && du_safe "$p" done # ----------------- pacman cache ----------------- hr if [[ -d /var/cache/pacman/pkg ]]; then echo "Pacman cache size:" sudo du -sh /var/cache/pacman/pkg 2>/dev/null || true if have paccache; then echo echo "paccache available." echo " - paccache -r keeps last 3 versions (safe default)" echo " - paccache -rk1 keeps last 1 version (more aggressive)" if ask "Clean pacman cache (keep last 3 versions)?" ; then sudo paccache -r fi if ask "Also clean pacman cache more aggressively (keep last 1 version)?" ; then sudo paccache -rk1 fi else echo "paccache not found. Install pacman-contrib for safer cache cleanup:" echo " sudo pacman -S pacman-contrib" if ask "Run 'sudo pacman -Sc' (removes uninstalled pkg cache)?" ; then sudo pacman -Sc fi fi fi # ----------------- orphan dependencies ----------------- hr echo "Checking orphan packages (no longer required dependencies)..." orphans="$(pacman -Qtdq 2>/dev/null || true)" if [[ -n "${orphans}" ]]; then echo "Orphans found:" echo "${orphans}" | sed 's/^/ - /' if ask "Remove ALL orphan packages (sudo pacman -Rns ...) ?" ; then # shellcheck disable=SC2086 sudo pacman -Rns ${orphans} fi else echo "No orphan packages found." fi # ----------------- foreign (AUR) info + yay cache ----------------- hr echo "Foreign packages (usually AUR) installed:" foreign="$(pacman -Qm 2>/dev/null || true)" if [[ -n "${foreign}" ]]; then echo "${foreign}" | sed 's/^/ - /' | head -n 50 [[ $(echo "${foreign}" | wc -l) -gt 50 ]] && echo " ... (showing first 50)" else echo "None." fi if have yay; then hr echo "yay cache size (if exists):" [[ -d "$HOME/.cache/yay" ]] && du -sh "$HOME/.cache/yay" || echo " (no ~/.cache/yay)" if ask "Clean yay build/cache (yay -Sc)?" ; then yay -Sc fi fi # ----------------- systemd journal cleanup ----------------- hr if have journalctl; then echo "Journald disk usage:" sudo journalctl --disk-usage || true if ask "Vacuum journal to 500M?" ; then sudo journalctl --vacuum-size=500M fi if ask "Also vacuum journal older than 2 weeks?" ; then sudo journalctl --vacuum-time=2weeks fi fi # ----------------- user caches ----------------- hr cleanup_safe_user_caches # ----------------- dotnet / nuget caches ----------------- hr if have dotnet; then echo "NuGet package cache size:" [[ -d "$HOME/.nuget/packages" ]] && du -sh "$HOME/.nuget/packages" || echo " (no ~/.nuget/packages)" echo if ask "Clear dotnet NuGet locals (dotnet nuget locals all --clear)?" ; then dotnet nuget locals all --clear fi fi # ----------------- docker / podman ----------------- hr if have docker; then cleanup_docker_preserving_networks fi if have podman; then hr echo "Podman disk usage:" podman system df || true echo echo "WARNING: podman prune can delete unused images/containers." if ask "Run 'podman system prune'?" ; then podman system prune fi fi # ----------------- flatpak ----------------- hr if have flatpak; then echo "Flatpak unused cleanup:" if ask "Remove unused flatpak runtimes/apps (flatpak uninstall --unused)?" ; then flatpak uninstall --unused fi fi # ----------------- big files finder (informational) ----------------- hr echo "Top large files (>2GiB) under /var and ~ (excluding rclone mounts):" echo " /var:" sudo bash -c "$(declare -f find_bigfiles_safe); $(declare -p EXCLUDE_PATHS); find_bigfiles_safe /var +2G" || true echo " $HOME:" find_bigfiles_safe "$HOME" +2G || true # ----------------- summary ----------------- hr echo "Cleanup done." report_space hr echo "Tip: for interactive analysis, avoid scanning rclone mounts:" echo " sudo ncdu --exclude $ONEDRIVE_MOUNT --exclude $NEXTCLOUD_MOUNT /"