From e8512b56f5da82073b004fa44232f568dd9c5375 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Thu, 10 Sep 2026 10:01:46 +0200 Subject: [PATCH] Preserve model caches and Docker networks during cleanup --- .gitignore | 4 + CHANGELOG.md | 9 + README.md | 6 + config/llama-local.env.example | 13 + docs/CLEANUP_RECOVERY.md | 88 ++++++ docs/WORKSTATION.md | 14 +- maintenance/arch-maintenance.sh | 443 +++++++++++++++++++++++++++++ maintenance/arch-sanity-cleanup.sh | 280 ++++++++++++++++++ tests/smoke.sh | 2 +- 9 files changed, 854 insertions(+), 5 deletions(-) create mode 100644 config/llama-local.env.example create mode 100644 docs/CLEANUP_RECOVERY.md create mode 100755 maintenance/arch-maintenance.sh create mode 100755 maintenance/arch-sanity-cleanup.sh diff --git a/.gitignore b/.gitignore index 834bb2f..22aed8a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ dist/ /hermes-test-*/ __pycache__/ *.pyc + +# Lokale KI-Modelle und unvollständige Downloads +*.gguf +*.downloadInProgress diff --git a/CHANGELOG.md b/CHANGELOG.md index c922889..0026861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2026-09-10 — Cleanup-Reparatur und dauerhafte Modelle + +- Korrigierte Cleanup-Skripte unter maintenance/ aufgenommen: kein globales + Cache-Leeren im Sanity-Skript und keine Docker-Netzbereinigung. +- Vorlage für lokale Modell-/Projektor-Dateien ergänzt; baseline beibehalten. +- Wiederherstellung von Modellserver und hermes-net samt Prüfgrenzen dokumentiert. +- Modelldateien/Downloads von Git ausgeschlossen; Cleanup-Syntaxprüfung ergänzt. +- Workstation-Notiz zur inzwischen erfolgten Updater-Installation korrigiert. + # 2026-09-09 — Workstation-Abgleich - Healthcheck und llama-Helfer übernommen; portable Helferpfade ergänzt. diff --git a/README.md b/README.md index f8dc033..fc3f8ad 100644 --- a/README.md +++ b/README.md @@ -206,3 +206,9 @@ eingefrorenes Arch-Abbild. Für denselben Modell-/Backendstand Quell-SHA sowie Modell-/Projektor-Prüfsummen notieren. Der offizielle Hermes-Installer und Arch liefern beim Neuaufsetzen sonst den dann verfügbaren Stand. Technische Quellen stehen in `docs/LLAMA_INSTALL.md` und `docs/RTX3080.md`. + +## Cleanup und dauerhafte Modellablage + +Siehe [Cleanup und Wiederherstellung](docs/CLEANUP_RECOVERY.md) und +[Vorlage für lokale Modelle](config/llama-local.env.example). +Die optionalen Skripte in maintenance/ werden nicht automatisch installiert. diff --git a/config/llama-local.env.example b/config/llama-local.env.example new file mode 100644 index 0000000..98ba1e4 --- /dev/null +++ b/config/llama-local.env.example @@ -0,0 +1,13 @@ +# Alternative zur HF-Bootstrap-Vorlage; erst nach Bereitstellung beider Dateien. +# Bestehende llama.env nur gezielt bearbeiten, nicht ungeprüft überschreiben. +LLAMA_SERVER_BIN="" +LLAMA_MODEL_REVISION="0669b98607d47046c7c2b3f801011d54a08cfccf" +LLAMA_MODEL_DIR="$HOME/.local/share/hermes-stack/models/qwen38-27b/$LLAMA_MODEL_REVISION" +LLAMA_MODEL="$LLAMA_MODEL_DIR/Qwen3.8-27B-Q4_K_M.gguf" +LLAMA_MMPROJ="$LLAMA_MODEL_DIR/mmproj-Qwen3.8-27B-Q8_0.gguf" +LLAMA_HF_REPO="" +LLAMA_ALIAS="qwen38-27b-local" +LLAMA_HOST="127.0.0.1" +LLAMA_PORT="8080" +LLAMA_PROFILE="baseline" +LLAMA_ARGS=() diff --git a/docs/CLEANUP_RECOVERY.md b/docs/CLEANUP_RECOVERY.md new file mode 100644 index 0000000..d292c0f --- /dev/null +++ b/docs/CLEANUP_RECOVERY.md @@ -0,0 +1,88 @@ +# Cleanup-Verträglichkeit und Wiederherstellung + +## Verifizierter Stand vom 2026-09-10 + +Ein globales Leeren von ~/.cache entfernte Modell und Bildprojektor. +Beim Aufwachen aus dem Idle-Sleep brach llama.cpp deshalb ab. Docker system +prune entfernte außerdem das gerade unbenutzte Netz hermes-net. +Der automatische Dienstneustart lud die Dateien erneut herunter. +Nach vollständigem Download wurden Modell und Projektor als reguläre Dateien +außerhalb des Caches abgelegt und mit cmp gegen die Quellen geprüft. + +Nach Umstellung und kontrolliertem Neustart bestätigte das Serverlog beide +neuen Pfade. Eine Textanfrage lieferte OK, der Healthcheck Result=success. +Profil baseline bleibt unverändert (65536 Kontext, 1536 MiB Fit-Zielmarge). +Damit sind Modellstart, kurze Textinferenz und leichte Gesundheitsprüfung +belegt; ein kompletter Bildjob und der Docker-Netzwerk-Canary wurden bei +dieser Wiederherstellung noch nicht erneut getestet. + +## Modellablage auf weiteren Rechnern + +Die bestehende config/llama.env.example dient weiter zum HF-Bootstrap. +Downloads vollständig beenden lassen. Keine .downloadInProgress-Dateien +übernehmen. Die verifizierte Revision lautet: +0669b98607d47046c7c2b3f801011d54a08cfccf + +Die Quellen liegen unter: +~/.cache/huggingface/hub/models--ggml-org--Qwen3.8-27B-GGUF/snapshots/REVISION/ +Die dauerhaften Dateien gehören unter: +~/.local/share/hermes-stack/models/qwen38-27b/REVISION/ + +Benötigt werden Qwen3.8-27B-Q4_K_M.gguf und mmproj-Qwen3.8-27B-Q8_0.gguf. +Cache-Symlinks beim Kopieren dereferenzieren (cp -L), reguläre Kopien mit +cmp prüfen und die Cache-Quellen bis zur erfolgreichen Laufzeitprüfung behalten. +Falls der Bootstrap eine andere Revision lädt, diese zuerst prüfen und +bewusst auswählen; nicht verschiedene Modell-/Projektor-Revisionen mischen. + +config/llama-local.env.example zeigt die lokale Konfiguration. In bestehender +llama.env nur LLAMA_MODEL und LLAMA_MMPROJ setzen und LLAMA_HF_REPO leeren; +Binary-Auswahl und Profil beibehalten. Vorher sichern. Auf neuen Rechnern die +Vorlage erst nach Bereitstellung der passenden Dateien verwenden. +Die Dateien werden nicht ins Git-Repository aufgenommen. Hermes-Updates +aktualisieren diese fest ausgewählte lokale Modellrevision nicht automatisch. + +Prüfen: hermes-stack-llama-run --check und --print. Erst nach Jobende mit +hermes-stack-llama-restart --jobs-finished neu starten. Anschließend /health, +eine kurze Inferenz und hermes-stack-health.service prüfen. Die --check-Prüfung +allein belegt nur Konfiguration und Optionsnamen, keine funktionierende Inferenz. + +## Docker-Netz + +Am 10. September wurde hermes-net als normale Bridge wieder angelegt: + +```bash +docker network create --driver bridge hermes-net +docker network inspect hermes-net +``` + +Nur bei fehlendem Netz ausführen. Vorhandene Netze nicht entfernen. +Docker wählte auf der Workstation 172.18.0.0/16 mit Gateway 172.18.0.1. +Diese Adressen sind kein portabler Sollwert; auf neuen Rechnern Docker wählen +lassen. Sonderoptionen des früher gelöschten Netzes waren nicht dokumentiert. +In der privaten config.env DOCKER_NETWORK=hermes-net setzen, wenn Hermes +dieses Netz verwendet. Die leere Deployment-Vorlage deaktiviert die Prüfung. +install.sh erstellt das Netz nicht; Anlage und Container-Verbindung separat prüfen. + +## Cleanup-Skripte + +maintenance/ enthält die korrigierten Skripte. install.sh installiert und +startet sie nicht. Auf der Workstation liegen sie unter ~/Scripts; vorhandene +Dateien vor einem manuellen Austausch sichern. Keine Zugangsdaten übernehmen. + +- arch-maintenance.sh: standardmäßig Dry-Run, Änderungen erst mit --apply. + Container, Images und Builder-Cache werden getrennt bereinigt; Netze bleiben. + --docker-volumes bleibt eine ausdrückliche, datenlöschende Option. +- arch-sanity-cleanup.sh: interaktiv, kein allgemeiner Dry-Run. Im allgemeinen + User-Cache nur Thumbnail-/Fontconfig-Dateien älter als 30 Tage löschen. + Modellcache und Downloads bleiben erhalten. Docker-Netze werden nicht bereinigt. +- Beide Skripte behalten weitere Wartungsfunktionen, etwa Paket- und AUR- + Bereinigung. --docker-aggressive bzw. die Image-Rückfrage kann weiterhin + unbenutzte Runtime-Images entfernen; diese müssen später erneut geladen werden. +- Eigene tmpfiles-Regeln, andere Cleanup-Werkzeuge und manuelle Löschbefehle + sind hiervon nicht erfasst. Ein Netzname allein schützt nicht vor prune. + +Die Korrekturen wurden mit bash -n sowie gezielten Tests mit temporären Dateien +und einem Docker-Ersatzprogramm geprüft: Modell-/Download-Dateien erhalten, +alte Thumbnail-/Fontdateien entfernt, keine Docker-Netzbereinigung aufgerufen. +Die normale Repo-Prüfung prüft die Cleanup-Skripte auf Syntax, führt sie aber +nicht aus. Root-Wartung und echte Docker-Prunes sind kein Bestandteil der Tests. diff --git a/docs/WORKSTATION.md b/docs/WORKSTATION.md index 1bf4e55..d02a66b 100644 --- a/docs/WORKSTATION.md +++ b/docs/WORKSTATION.md @@ -116,7 +116,13 @@ Die Docker-Netzprüfung ist optional: DOCKER_NETWORK in config.env auf den Namen des verwendeten Netzes setzen. Leer bedeutet keine Netzprüfung. Die Prüfung erstellt kein Netz. Private Konfigurationen bleiben außerhalb Git. -Der Repository-Updater enthält die aufgerufenen Retry-Pfade und den gemeinsamen -Benachrichtigungshelfer. Die zuletzt verglichene installierte Fassung enthält -noch ungenutzte Retry-Funktionen. Der Git-Abgleich aktualisiert die Workstation -nicht; die Übernahme des Repository-Updaters steht separat aus. +Der Repository-Updater wurde am 2026-09-09 mit Sicherung auf die Workstation +übernommen und bytegleich geprüft; die Konfigurationsdefaults wurden ergänzt. +Ein echter Update-/Rollback-Lauf der neuen Fassung war damit noch nicht geprüft. + + +## Wiederherstellung vom 2026-09-10 + +Modell und Projektor liegen jetzt außerhalb von ~/.cache. Die korrigierten +Cleanup-Skripte sind unter ~/Scripts installiert. Details und Prüfgrenzen: +[CLEANUP_RECOVERY.md](CLEANUP_RECOVERY.md). diff --git a/maintenance/arch-maintenance.sh b/maintenance/arch-maintenance.sh new file mode 100755 index 0000000..b22de17 --- /dev/null +++ b/maintenance/arch-maintenance.sh @@ -0,0 +1,443 @@ +#!/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 "$@" diff --git a/maintenance/arch-sanity-cleanup.sh b/maintenance/arch-sanity-cleanup.sh new file mode 100755 index 0000000..324bb3f --- /dev/null +++ b/maintenance/arch-sanity-cleanup.sh @@ -0,0 +1,280 @@ +#!/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 /" diff --git a/tests/smoke.sh b/tests/smoke.sh index 3de02f7..3d188ba 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -5,7 +5,7 @@ ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" failures=0 for script in "$ROOT_DIR/install.sh" "$ROOT_DIR/uninstall.sh" \ - "$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh "$ROOT_DIR"/config/*.example; do + "$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh "$ROOT_DIR"/config/*.example "$ROOT_DIR"/maintenance/*.sh; do if bash -n "$script"; then printf 'OK bash -n %s\n' "${script#"$ROOT_DIR/"}" else