#!/usr/bin/env bash # Arch LXC Secure Cleanup & Audit Script # Usage: sudo ./arch-cleanup.sh [--dry-run] set -euo pipefail # --------------------------------------------------------------------------- # Safety check # --------------------------------------------------------------------------- if [[ $EUID -ne 0 ]]; then echo "Error: Run this script with sudo: sudo $0 [--dry-run]" exit 1 fi # --------------------------------------------------------------------------- # Config & User detection # --------------------------------------------------------------------------- DRY_RUN=false if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=true echo ">>> DRY-RUN MODE: No changes will be made <<<" echo "" fi TARGET_USER="${SUDO_USER:-$(logname 2>/dev/null || echo '')}" TARGET_UID="" if [[ "$TARGET_USER" == "root" ]]; then TARGET_USER="" fi if [[ -n "$TARGET_USER" ]]; then TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || echo '')" fi # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- RED=$'\e[31m' GREEN=$'\e[32m' YELLOW=$'\e[33m' BLUE=$'\e[34m' CYAN=$'\e[36m' NC=$'\e[0m' log() { printf "${GREEN}[INFO]${NC} %s\n" "$*"; } warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; } err() { printf "${RED}[ERROR]${NC} %s\n" "$*"; } section() { printf "\n${BLUE}=== %s ===${NC}\n" "$*"; } confirm() { [[ "$DRY_RUN" == true ]] && return 0 local r read -r -p "$1 [y/N] " r [[ "$r" =~ ^[Yy]$ ]] } run() { if [[ "$DRY_RUN" == true ]]; then printf "${CYAN}[DRY-RUN]${NC} %s\n" "$*" else "$@" fi } # --------------------------------------------------------------------------- # Phase 1 — Audit: Where is the 170 GB? # --------------------------------------------------------------------------- section "Disk Usage Summary" df -hT / | grep -E 'Filesystem|/dev/' section "Suspect Directory Sizes" suspects=( /var/cache/pacman/pkg /var/lib/containers /var/log /var/log/journal /var/lib/systemd/coredump /var/tmp /tmp /root ) for dir in "${suspects[@]}"; do [[ -d "$dir" ]] || continue sz=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') printf " %-40s %s\n" "$dir" "$sz" done if [[ -n "$TARGET_USER" && -d "/home/$TARGET_USER" ]]; then section "User '$TARGET_USER' Suspect Directories" user_suspects=( "/home/$TARGET_USER/.cache" "/home/$TARGET_USER/.cache/paru" "/home/$TARGET_USER/.cache/paru/clone" "/home/$TARGET_USER/.cache/paru/pkg" "/home/$TARGET_USER/.cache/yay" "/home/$TARGET_USER/.local/share/containers" ) for dir in "${user_suspects[@]}"; do [[ -e "$dir" ]] || continue sz=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') printf " %-50s %s\n" "$dir" "$sz" done fi section "Large Individual Files (>1 GB)" for search_root in /var /home /root /opt; do [[ -d "$search_root" ]] || continue find "$search_root" -xdev -maxdepth 5 -type f -size +1G -exec ls -lh {} + 2>/dev/null | head -n 20 done echo "" if ! confirm "Continue with cleanup?"; then log "Aborted by user." exit 0 fi # --------------------------------------------------------------------------- # Phase 2 — Cleanup # --------------------------------------------------------------------------- # --- Pacman cache --- section "Pacman Cache Cleanup" if command -v paccache &>/dev/null; then log "Removing cached packages for uninstalled software..." run paccache -ruv log "Keeping last 2 versions of installed packages, removing older..." run paccache -rvk2 else warn "'paccache' (pacman-contrib) not found." if confirm "Install pacman-contrib now? (Recommended; needs a little free space)"; then if [[ "$DRY_RUN" == true ]]; then log "[DRY-RUN] Would install pacman-contrib and run paccache" else pacman -S --noconfirm --needed pacman-contrib paccache -ruv paccache -rvk2 fi else if confirm "Run fallback 'pacman -Sc'? (Removes ALL uninstalled cached packages)"; then run pacman -Sc --noconfirm fi fi fi # --- Orphaned packages --- section "Orphaned Packages" orphans="$(pacman -Qdtq 2>/dev/null || true)" if [[ -z "$orphans" ]]; then log "No orphaned packages found." else warn "Orphaned packages found:" echo "$orphans" if confirm "Remove these orphaned packages?"; then if [[ "$DRY_RUN" == true ]]; then log "[DRY-RUN] Would remove: $orphans" else # shellcheck disable=SC2086 pacman -Rns --noconfirm $orphans fi fi fi # --- AUR Helpers (paru / yay) --- section "AUR Helper Cache Cleanup" clean_aur() { local helper=$1 local binpath clone_dir pkg_dir sz binpath="$(command -v "$helper" 2>/dev/null || true)" if [[ -z "$binpath" ]]; then log "$helper not installed." return fi if [[ -z "$TARGET_USER" ]]; then warn "Cannot determine non-root user for $helper cleanup." return fi log "Running safe cache clean for $helper..." run sudo -u "$TARGET_USER" "$binpath" -Sc --noconfirm || warn "$helper -Sc finished with warnings" clone_dir="/home/$TARGET_USER/.cache/$helper/clone" if [[ -d "$clone_dir" ]]; then sz=$(du -sh "$clone_dir" 2>/dev/null | awk '{print $1}') warn "$helper clone directory size: $sz" if confirm "Delete $helper PKGBUILD clones? (They must be re-downloaded later)"; then run rm -rf "${clone_dir:?}"/* log "Cleaned $clone_dir" fi fi pkg_dir="/home/$TARGET_USER/.cache/$helper/pkg" if [[ -d "$pkg_dir" ]] && command -v paccache &>/dev/null; then log "Running paccache on $helper package dir..." run paccache -ruv --cachedir "$pkg_dir" || true fi } clean_aur "paru" clean_aur "yay" # --- Podman (rootful + rootless) --- section "Podman Cleanup" if command -v podman &>/dev/null; then log "Rootful Podman disk usage:" run podman system df || true if confirm "Prune unused rootful Podman resources (containers, networks, images, build cache)?"; then run podman system prune -a -f fi else log "Podman (rootful) not installed." fi if [[ -n "$TARGET_USER" && -n "$TARGET_UID" ]]; then if sudo -u "$TARGET_USER" podman --version &>/dev/null; then log "Rootless Podman disk usage for $TARGET_USER:" run sudo -u "$TARGET_USER" podman system df || true if confirm "Prune unused rootless Podman resources for $TARGET_USER?"; then run sudo -u "$TARGET_USER" \ XDG_RUNTIME_DIR="/run/user/$TARGET_UID" \ podman system prune -a -f || run sudo -u "$TARGET_USER" podman system prune -a -f fi else log "Rootless Podman not configured for $TARGET_USER." fi else log "Skipping rootless Podman (no target user detected)." fi # --- Logs --- section "System Logs" if [[ -d /var/log/journal ]] && command -v journalctl &>/dev/null; then jsz=$(du -sh /var/log/journal 2>/dev/null | awk '{print $1}') log "Persistent journal size: $jsz" if confirm "Vacuum journal to max 500 MB?"; then run journalctl --vacuum-size=500M fi else log "No persistent systemd journal detected." fi if find /var/log -maxdepth 2 -type f -size +500M 2>/dev/null | grep -q .; then warn "Large uncompressed log files remain in /var/log. You may want to truncate them manually." fi # --------------------------------------------------------------------------- # Final Summary # --------------------------------------------------------------------------- section "Final Disk Usage" df -hT / log "Cleanup complete. Review the output above to see how much was recovered." if [[ "$DRY_RUN" == true ]]; then log "This was a DRY RUN. Re-run without --dry-run to actually free space." fi