281 lines
7.9 KiB
Bash
Executable File
281 lines
7.9 KiB
Bash
Executable File
#!/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 /"
|