Preserve model caches and Docker networks during cleanup

This commit is contained in:
admin committed 2026-09-10 10:01:46 +02:00
1 parent 3c1080e152
commit e8512b56f5
9 files changed
+854 -5

No files matched your search

+443
View File
@@ -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 "$@"
+280
View File
@@ -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 /"