#!/usr/bin/env bash # arch-maintenance.sh # Konservatives Wartungsskript fuer Arch Linux # Standard: Dry-Run. Loeschen nur mit --apply. # 2026-09-10: Docker-Netze immer erhalten; kein globales system prune. set -uo pipefail IFS=$'\n\t' APPLY=0 YES=0 PACMAN_KEEP=3 PACMAN_KEEP_UNINSTALLED=0 JOURNAL_MAX_AGE="14d" JOURNAL_MAX_SIZE="500M" DO_DOCKER=1 DOCKER_UNTIL="168h" DOCKER_AGGRESSIVE=0 DOCKER_VOLUMES=0 DO_ORPHANS=1 DO_AUR_CACHE=1 AUR_CACHE_DAYS=30 THUMB_CACHE_DAYS=30 LOG_DIR="/var/log/arch-maintenance" LOG_FILE="" usage() { cat <<'EOF' Arch Linux Wartungsskript Usage: sudo arch-maintenance.sh [Optionen] Sicherer Standard: Ohne --apply wird nur analysiert / simuliert. Docker-Netze bleiben auch mit --apply und --docker-aggressive erhalten. User-Caches: nur alte yay/paru-Eintraege und Thumbnails, kein globales Leeren. Optionen: --apply Wirklich bereinigen --yes Keine Rueckfragen stellen --no-docker Docker-Bereinigung ueberspringen --docker-aggressive Auch ungenutzte Docker-Images entfernen (-a) --docker-volumes Ungenutzte Docker-Volumes entfernen; Vorsicht: kann Daten loeschen --no-orphans Pacman-Orphans nicht entfernen --no-aur-cache yay/paru Build-Caches nicht bereinigen --pacman-keep N N alte Versionen installierter Pakete behalten, Default: 3 --journal-age AGE Journal-Alter, Default: 14d --journal-size SIZE Journal-Groesse, Default: 500M --docker-until AGE Docker prune Filter, Default: 168h --help Hilfe anzeigen Beispiele: sudo arch-maintenance.sh sudo arch-maintenance.sh --apply sudo arch-maintenance.sh --apply --yes sudo arch-maintenance.sh --apply --docker-aggressive sudo arch-maintenance.sh --apply --docker-volumes EOF } log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" } warn() { log "WARN: $*" } die() { log "ERROR: $*" exit 1 } cmdline() { printf '%q ' "$@" } have_cmd() { command -v "$1" >/dev/null 2>&1 } confirm() { local prompt="$1" if (( YES )); then return 0 fi if [[ ! -t 0 ]]; then warn "Nicht-interaktive Shell ohne --yes: '$prompt' wird uebersprungen." return 1 fi local answer read -r -p "$prompt [y/N] " answer [[ "$answer" =~ ^([yYjJ]|yes|YES|ja|JA)$ ]] } execute() { local description="$1" shift log "==> $description" log "CMD: $(cmdline "$@")" if (( APPLY )); then if "$@"; then log "OK: $description" else warn "Fehlgeschlagen, fahre fort: $description" fi else log "DRY-RUN: nicht ausgefuehrt" fi } run_and_log() { local description="$1" shift log "==> $description" log "CMD: $(cmdline "$@")" if "$@"; then log "OK: $description" else warn "Fehlgeschlagen, fahre fort: $description" fi } init_logging() { mkdir -p "$LOG_DIR" || die "Kann Log-Verzeichnis nicht erstellen: $LOG_DIR" chmod 0755 "$LOG_DIR" || true LOG_FILE="$LOG_DIR/maintenance-$(date '+%Y%m%d-%H%M%S').log" touch "$LOG_FILE" || die "Kann Log-Datei nicht erstellen: $LOG_FILE" chmod 0644 "$LOG_FILE" || true exec > >(tee -a "$LOG_FILE") 2>&1 log "Log-Datei: $LOG_FILE" if (( APPLY )); then log "Modus: APPLY" else log "Modus: DRY-RUN" fi } require_root() { [[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Bitte mit sudo/root ausfuehren." } parse_args() { while (($#)); do case "$1" in --apply) APPLY=1 ;; --yes) YES=1 ;; --no-docker) DO_DOCKER=0 ;; --docker-aggressive) DOCKER_AGGRESSIVE=1 ;; --docker-volumes) DOCKER_VOLUMES=1 ;; --no-orphans) DO_ORPHANS=0 ;; --no-aur-cache) DO_AUR_CACHE=0 ;; --pacman-keep) shift [[ "${1:-}" =~ ^[0-9]+$ ]] || die "--pacman-keep erwartet eine Zahl" PACMAN_KEEP="$1" ;; --journal-age) shift JOURNAL_MAX_AGE="${1:-}" [[ -n "$JOURNAL_MAX_AGE" ]] || die "--journal-age erwartet einen Wert" ;; --journal-size) shift JOURNAL_MAX_SIZE="${1:-}" [[ -n "$JOURNAL_MAX_SIZE" ]] || die "--journal-size erwartet einen Wert" ;; --docker-until) shift DOCKER_UNTIL="${1:-}" [[ -n "$DOCKER_UNTIL" ]] || die "--docker-until erwartet einen Wert" ;; --help|-h) usage exit 0 ;; *) die "Unbekannte Option: $1" ;; esac shift done } real_user() { if [[ -n "${SUDO_USER:-}" && "${SUDO_USER:-}" != "root" ]]; then printf '%s\n' "$SUDO_USER" else logname 2>/dev/null || printf 'root\n' fi } home_of_user() { local user="$1" getent passwd "$user" | cut -d: -f6 } show_disk_usage() { log "==> Speicherbelegung" df -hT / /home /var 2>/dev/null || df -hT } cleanup_pacman_cache() { log "==> Pacman Cache" if ! have_cmd paccache; then warn "paccache nicht gefunden. Installiere es mit: sudo pacman -S --needed pacman-contrib" return fi if (( APPLY )); then execute "Pacman Cache bereinigen; letzte ${PACMAN_KEEP} Versionen installierter Pakete behalten" \ paccache -r -k "$PACMAN_KEEP" execute "Pacman Cache fuer deinstallierte Pakete bereinigen; ${PACMAN_KEEP_UNINSTALLED} Versionen behalten" \ paccache -r -u -k "$PACMAN_KEEP_UNINSTALLED" else run_and_log "Pacman Cache Dry-Run fuer installierte Pakete" \ paccache -d -r -k "$PACMAN_KEEP" run_and_log "Pacman Cache Dry-Run fuer deinstallierte Pakete" \ paccache -d -r -u -k "$PACMAN_KEEP_UNINSTALLED" fi } cleanup_orphans() { (( DO_ORPHANS )) || { log "Pacman-Orphans: uebersprungen" return } log "==> Pacman Orphans" if ! have_cmd pacman; then warn "pacman nicht gefunden?" return fi local -a orphans=() mapfile -t orphans < <(pacman -Qqdt 2>/dev/null || true) if ((${#orphans[@]} == 0)); then log "Keine Orphan-Pakete gefunden." return fi log "Gefundene Orphan-Pakete:" printf ' %s\n' "${orphans[@]}" if (( APPLY )); then if confirm "Diese Orphan-Pakete mit pacman -Rns entfernen?"; then pacman -Rns --noconfirm "${orphans[@]}" || warn "Orphan-Entfernung fehlgeschlagen." else log "Orphan-Entfernung uebersprungen." fi else log "DRY-RUN: Orphans wuerden mit pacman -Rns entfernt." fi } cleanup_journal() { log "==> systemd Journal" if ! have_cmd journalctl; then warn "journalctl nicht gefunden." return fi run_and_log "Journal-Groesse vor Bereinigung" journalctl --disk-usage if (( APPLY )); then execute "Journal rotieren" journalctl --rotate execute "Journal vacuum: max. ${JOURNAL_MAX_AGE}, max. ${JOURNAL_MAX_SIZE}" \ journalctl --vacuum-time="$JOURNAL_MAX_AGE" --vacuum-size="$JOURNAL_MAX_SIZE" run_and_log "Journal-Groesse nach Bereinigung" journalctl --disk-usage else log "DRY-RUN: Journal wuerde rotiert und mit --vacuum-time=${JOURNAL_MAX_AGE} --vacuum-size=${JOURNAL_MAX_SIZE} begrenzt." fi } cleanup_tmpfiles() { log "==> systemd-tmpfiles" if ! have_cmd systemd-tmpfiles; then warn "systemd-tmpfiles nicht gefunden." return fi execute "Temporäre Dateien nach systemd-tmpfiles-Regeln bereinigen" \ systemd-tmpfiles --clean } cleanup_docker() { (( DO_DOCKER )) || { log "Docker: uebersprungen" return } log "==> Docker" if ! have_cmd docker; then log "Docker nicht installiert, uebersprungen." return fi if ! docker info >/dev/null 2>&1; then warn "Docker daemon nicht erreichbar oder keine Berechtigung. Docker-Bereinigung uebersprungen." return fi run_and_log "Docker Speicherbelegung vor Bereinigung" docker system df -v log "Docker-Netze bleiben erhalten, auch wenn gerade kein Container sie verwendet." execute "Gestoppte Docker-Container bereinigen, erstellt vor ${DOCKER_UNTIL}" \ docker container prune -f --filter "until=${DOCKER_UNTIL}" if (( DOCKER_AGGRESSIVE )); then execute "Alle ungenutzten Docker-Images bereinigen, erstellt vor ${DOCKER_UNTIL}" \ docker image prune -a -f --filter "until=${DOCKER_UNTIL}" else execute "Dangling Docker-Images bereinigen, erstellt vor ${DOCKER_UNTIL}" \ docker image prune -f --filter "until=${DOCKER_UNTIL}" fi execute "Docker Builder Cache bereinigen, aelter als ${DOCKER_UNTIL}" \ docker builder prune -f --filter "until=${DOCKER_UNTIL}" if (( DOCKER_VOLUMES )); then if (( APPLY )); then if confirm "Ungenutzte Docker-Volumes entfernen? Das kann Daten loeschen."; then execute "Ungenutzte Docker-Volumes entfernen; Volumes mit label keep bleiben erhalten" \ docker volume prune -f --filter "label!=keep" else log "Docker-Volumes uebersprungen." fi else log "DRY-RUN: Ungenutzte Docker-Volumes wuerden mit docker volume prune entfernt; nur bei --docker-volumes." fi else log "Docker-Volumes werden nicht bereinigt. Nutze --docker-volumes, wenn du das wirklich willst." fi run_and_log "Docker Speicherbelegung nach Bereinigung / Analyse" docker system df -v } cleanup_user_caches() { (( DO_AUR_CACHE )) || { log "User/AUR Cache: uebersprungen" return } local user home user="$(real_user)" home="$(home_of_user "$user")" if [[ -z "$home" || ! -d "$home" ]]; then warn "Home-Verzeichnis fuer User '$user' nicht gefunden." return fi log "==> User Cache fuer $user ($home)" local dir for dir in "$home/.cache/yay" "$home/.cache/paru"; do [[ -d "$dir" ]] || continue log "AUR Cache Kandidaten in $dir, aelter als ${AUR_CACHE_DAYS} Tage:" find "$dir" -mindepth 1 -maxdepth 1 -mtime +"$AUR_CACHE_DAYS" -print 2>/dev/null || true if (( APPLY )); then find "$dir" -mindepth 1 -maxdepth 1 -mtime +"$AUR_CACHE_DAYS" -exec rm -rf -- {} + 2>/dev/null \ || warn "Konnte AUR Cache nicht vollstaendig bereinigen: $dir" else log "DRY-RUN: alte Eintraege in $dir wuerden geloescht." fi done dir="$home/.cache/thumbnails" if [[ -d "$dir" ]]; then log "Thumbnail Cache Kandidaten in $dir, aelter als ${THUMB_CACHE_DAYS} Tage:" find "$dir" -type f -mtime +"$THUMB_CACHE_DAYS" -print 2>/dev/null || true if (( APPLY )); then find "$dir" -type f -mtime +"$THUMB_CACHE_DAYS" -delete 2>/dev/null \ || warn "Konnte Thumbnail Cache nicht vollstaendig bereinigen." find "$dir" -type d -empty -delete 2>/dev/null || true else log "DRY-RUN: alte Thumbnail-Dateien wuerden geloescht." fi fi } main() { parse_args "$@" require_root init_logging log "Starte Arch-Wartung" show_disk_usage cleanup_pacman_cache cleanup_orphans cleanup_journal cleanup_tmpfiles cleanup_docker cleanup_user_caches show_disk_usage log "Fertig. Log: $LOG_FILE" } main "$@"