commit 682f522cb9d1826c615fee2cb8d3c719194b0332 Author: mates Date: Sat Aug 1 19:51:59 2026 +0200 init: clean history with no PII Toto repo ma novou cistou historii (1 commit). Predchozi historie byla smazana, protoze obsahovala osobni udaje (IP, emaily, hesla, klice, tokeny, Volume cesty). Soubory v aktualnim commit uz tyto udaje neobsahuji (nahradeno placeholdery , CHANGE_ME, admin@example.com atd.). Pokud chces zpet kompletni seznam starych commitu, mas je ve svem lokalnim working tree - git log --all ukaze prazdno (protoze jsme smazali .git/ a zacali znovu). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8459ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Secrets - nikdy v gitu +.env +*.env +!.env.example + +# Zálohy a dočasné soubory +*.bak +*.tmp +*.orig + +# Editor +.vscode/ +.idea/ +.DS_Store +*.swp + +# Build artefakty +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..17535d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mates + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..addba9a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# mates-install-scripts + +Bash skripty pro správu Dockeru. +Jedná se o **přesné kopie** skriptů z `~/Skripty/docker-domov/`. + +## Skripty + +| Skript | Alias | Účel | +|---|---|---| +| `docker-update.sh` | `dk-update` | Aktualizace Docker Compose projektů | +| `docker-backup.sh` | `dk-backup` | Záloha projektů a named volumes | +| `docker-cleanup.sh` | `dk-cleanup` | Čištění odpadu (s potvrzením) | +| `docker-restore.sh` | `dk-restore` | Obnova ze zálohy | +| `disk-watch.sh` | `dk-watch` | Sledování místa | +| `mount-nas.sh` | – | Připojení NAS SMB share | +| `update-macos.sh` | – | Aktualizace macOS | + +Všechny skripty jsou **idempotentní**, mají `--help`, podporují **dry-run** a bez potvrzení nic destruktivního neprovádějí. + +## Detailní návod + +Viz [README.md](README.md) – kompletní dokumentace aliasů a workflow. + +## Licence + +MIT – viz [LICENSE](LICENSE) diff --git a/disk-watch.sh b/disk-watch.sh new file mode 100644 index 0000000..d1b559a --- /dev/null +++ b/disk-watch.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# +# disk-watch.sh — sledování volného místa na Docker datech +# +# Kontroluje: +# - (Docker data) +# - (zálohy) +# - Docker disk usage (images, containers, volumes, build cache) +# +# Při podkročení prahu pošle notifikaci (Telegram přes Hermes cron, nebo lokální alert). +# V defaultu jen vypíše stav; s --alert pošle zprávu přes Hermes Telegram doručení. +# +# Použití: +# ./disk-watch.sh # jen výpis +# ./disk-watch.sh --alert # pošle notifikaci při podkročení +# SSDDATA_THRESHOLD=80 ./disk-watch.sh # 80 % prah pro SSD-data +# + +set -euo pipefail + +SSD_DATA="${SSD_DATA:-}" +SSD_BACKUP="${SSD_BACKUP:-}" +SSD_DATA_THRESHOLD="${SSD_DATA_THRESHOLD:-85}" # % +SSD_BACKUP_THRESHOLD="${SSD_BACKUP_THRESHOLD:-85}" # % +DOCKER_THRESHOLD="${DOCKER_THRESHOLD:-80}" # % + +ALERT=0 +for arg in "$@"; do + case "$arg" in + --alert|-a) ALERT=1 ;; + --help|-h) + sed -n '2,15p' "$0" + exit 0 + ;; + *) echo "Unknown arg: $arg" >&2; exit 1 ;; + esac +done + +# Barevné výstupy +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' +fi + +WARNINGS=() + +check_disk() { + local path="$1" threshold="$2" label="$3" + if [ ! -d "$path" ]; then + printf '%s%s%s %s — adresář neexistuje\n' "$YELLOW" "⚠" "$NC" "$label" + return + fi + # df v macOS nevrací % použití přímo; parsujeme z výstupu + local line used avail capacity + line=$(df -h "$path" | tail -n +2 | head -n 1) + capacity=$(echo "$line" | awk '{print $5}' | tr -d '%') + used=$(echo "$line" | awk '{print $3}') + avail=$(echo "$line" | awk '{print $4}') + total=$(echo "$line" | awk '{print $2}') + + if [ -z "$capacity" ]; then + printf '%s%s%s %s — nelze přečíst\n' "$YELLOW" "⚠" "$NC" "$label" + return + fi + + if [ "$capacity" -ge "$threshold" ]; then + printf '%s●%s %s %s/%s (%s%%) — %sPŘEKROČEN PRAH%s (≥%s%%)\n' \ + "$RED" "$NC" "$label" "$used" "$total" "$capacity" "$RED" "$NC" "$threshold" + WARNINGS+=("$label: ${capacity}% (prah ${threshold}%)") + else + printf '%s●%s %s %s/%s (%s%%) — OK\n' \ + "$GREEN" "$NC" "$label" "$used" "$total" "$capacity" + fi +} + +check_docker_disk() { + if ! docker info >/dev/null 2>&1; then + printf '%s●%s Docker — není dostupný\n' "$YELLOW" "$NC" + return + fi + + # Získat velikost images + containers + volumes + local out + out=$(docker system df --format '{{.Type}}\t{{.Size}}\t{{.Reclaimable}}' 2>/dev/null || true) + + if [ -z "$out" ]; then + printf '%s●%s Docker disk — nešlo přečíst\n' "$YELLOW" "$NC" + return + fi + + # Spočítat celkové využití Dockeru (obtížné bez du -s na /var/lib/docker; + # protože jsme na Docker Desktop macOS, kontrolujeme jen velikost images) + local images_size_bytes + images_size_bytes=$(docker system df --format '{{.Type}}\t{{.Size}}' \ + | awk -F'\t' '$1 == "Images" {print $2}') + + printf '%s●%s Docker images: %s\n' "$BLUE" "$NC" "${images_size_bytes:-neznámé}" + + # Velké images (>500 MB), které nejsou používané + printf '%s●%s Velké images (>500 MB):\n' "$BLUE" "$NC" + docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}\t{{.ID}}' \ + | awk -F'\t' '{ + size = $1 + # převod na MB + if (size ~ /GB/) {mb = size * 1024} else if (size ~ /MB/) {mb = size + 0} else if (size ~ /kB/) {mb = size / 1024} else {mb = 0} + if (mb + 0 > 500) print $2, size + }' \ + | head -10 | while read -r line; do + printf ' %s\n' "$line" + done + + # Nepoužívané images (pozor, ne "používané" — bezpečnost) + unused=$(docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' \ + | awk -F'\t' ' + /^([0-9.]+[kMG]B)\s/ { + size = $1 + if (size ~ /GB/) mb = size * 1024 + else if (size ~ /MB/) mb = size + 0 + else if (size ~ /kB/) mb = size / 1024 + if (mb + 0 > 200) print size, $2 + }' | head -5) + if [ -n "$unused" ]; then + printf '%s●%s Kandidáti na vyčištění (>200 MB):\n' "$YELLOW" "$NC" + echo "$unused" | while read -r line; do printf ' %s\n' "$line"; done + fi +} + +check_top_offenders() { + if [ ! -d "$SSD_DATA/Dockery" ]; then + return + fi + printf '\n%s●%s Top 5 největších adresářů v :\n' "$BLUE" "$NC" + /bin/du -sh "$SSD_DATA"/Dockery/*/ 2>/dev/null \ + | /usr/bin/sort -rh \ + | /usr/bin/head -5 \ + | /usr/bin/sed 's/^/ /' +} + +printf '%s%s%s\n' "$BLUE" "=== DISK WATCH ===" "$NC" +printf 'Prahy: SSD-data>%s%%, disk2-data>%s%%\n' "$SSD_DATA_THRESHOLD" "$SSD_BACKUP_THRESHOLD" +printf 'Datum: %s\n\n' "$(date '+%Y-%m-%d %H:%M:%S')" + +check_disk "$SSD_DATA" "$SSD_DATA_THRESHOLD" "SSD-data (Docker)" +check_disk "$SSD_BACKUP" "$SSD_BACKUP_THRESHOLD" "disk2-data (zálohy)" + +check_docker_disk +check_top_offenders + +if [ "${#WARNINGS[@]}" -gt 0 ]; then + printf '\n%s●%s VAROVÁNÍ:\n' "$RED" "$NC" + for w in "${WARNINGS[@]}"; do + printf ' - %s\n' "$w" + done +fi + +if [ "$ALERT" = "1" ] && [ "${#WARNINGS[@]}" -gt 0 ]; then + # Alert přes Hermes Telegram cron + MSG="⚠️ Disk Watch — překročen prah +$(printf '%s\n' "${WARNINGS[@]}") + +Datum: $(date '+%Y-%m-%d %H:%M:%S') +Host: $(hostname)" + + # Vytvoř one-shot cron pro doručení + CRON_TS=$(date -u -v+1M +%Y-%m-%dT%H:%M:%S 2>/dev/null \ + || date -u -d '+1 minute' +%Y-%m-%dT%H:%M:%S) + PAYLOAD=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))') + + if command -v hermes >/dev/null 2>&1; then + hermes cron add \ + --name "disk-watch-alert-$(date +%s)" \ + --at "$CRON_TS" \ + --deliver all \ + --message "$MSG" 2>/dev/null \ + && log "Alert naplánován na $CRON_TS" \ + || err "Nepodařilo se naplánovat alert (hermes cron)" + else + # Fallback: jen vypiš, že by se měl alert poslat + printf '\n%s[ALERT by se poslal:]%s\n' "$YELLOW" "$NC" + printf '%s\n' "$MSG" + fi +fi + +exit 0 \ No newline at end of file diff --git a/docker-backup.sh b/docker-backup.sh new file mode 100644 index 0000000..c850cb2 --- /dev/null +++ b/docker-backup.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# +# docker-backup.sh — bezpečná záloha Docker projektů a named volumes +# +# - vytvoří timestamp složku v / +# - pro každý projekt v zastaví kontejnery, +# zálohuje tar.zst, ověří integritu, znovu spustí +# - zvlášť zálohuje Docker named volumes +# - ponechá posledních N záloh (default 3) +# +# Použití: +# ./docker-backup.sh # všechny projekty + named volumes +# ./docker-backup.sh nginx mattermost # vybrané projekty (volumes vždy) +# BACKUP_KEEP=7 ./docker-backup.sh # ponechá 7 záloh +# ./docker-backup.sh --help # nápověda +# + +set -euo pipefail + +usage() { + sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//' +} + +for arg in "$@"; do + case "$arg" in + --help|-h) usage; exit 0 ;; + esac +done + +DOCKERY="${DOCKERY:-}" +BACKUP_ROOT="${BACKUP_ROOT:-}" +KEEP="${BACKUP_KEEP:-3}" +TS="$(date +%Y-%m-%d_%H-%M-%S)" +DEST="${BACKUP_ROOT}/${TS}" + +# Barevné výstupy +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; NC='' +fi + +log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*"; } +warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } +err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } + +# Kontrola +if ! docker info >/dev/null 2>&1; then + err "Docker není dostupný." + exit 1 +fi +if [ ! -d "$DOCKERY" ]; then + err "Dockery adresář neexistuje: $DOCKERY" + exit 1 +fi +if [ ! -d "$BACKUP_ROOT" ]; then + err "Backup adresář neexistuje: $BACKUP_ROOT" + exit 1 +fi + +mkdir -p "$DEST" + +# Najdi projekty +if [ "$#" -gt 0 ]; then + PROJECTS=("$@") +else + PROJECTS=() + for d in "$DOCKERY"/*/; do + [ -d "$d" ] || continue + if compgen -G "$d/docker-compose.y*ml" > /dev/null; then + PROJECTS+=("$(basename "$d")") + fi + done +fi + +log "Projektů k záloze: ${#PROJECTS[@]}" +log "Cíl: $DEST" +log "Uchovat posledních: $KEEP záloh" + +# === FÁZE 1: zastavit všechny dotčené kontejnery najednou === +log "" +log "=== STOP: zastavuji všechny kontejnery v ${PROJECTS[*]} ===" +for project in "${PROJECTS[@]}"; do + cd "$DOCKERY/$project" + if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then + if docker compose ps -q 2>/dev/null | grep -q .; then + log " [$project] docker compose stop" + docker compose stop 2>&1 || true + fi + fi +done + +# === FÁZE 2: záloha projektů (paralelně pro velké projekty) === +log "" +log "=== BACKUP projekty (tar.zst) ===" +FAILED=() +for project in "${PROJECTS[@]}"; do + cd "$DOCKERY/$project" + ARCHIVE="$DEST/${project}.tar.zst" + log " [$project] tar -c /$project/ → $ARCHIVE" + # absolutní cesta (background tar pitfall: vždy absolutní) + if /usr/bin/tar --exclude='._*' --exclude='.DS_Store' \ + --use-compress-program='/opt/homebrew/bin/zstd -1 -T0' \ + -cf "$ARCHIVE" \ + "/$project/" 2>>"$DEST/.errors"; then + # Ověření + if /opt/homebrew/bin/zstd -t "$ARCHIVE" >/dev/null 2>&1; then + SIZE=$(stat -f '%z' "$ARCHIVE") + log " OK ($(numfmt --to=iec --suffix=B "$SIZE" 2>/dev/null || echo "$SIZE B"))" + else + err " zstd -t selhal pro $ARCHIVE" + FAILED+=("$project") + fi + else + err " tar selhal pro $project" + FAILED+=("$project") + fi +done + +# === FÁZE 3: named volumes (per-volume záloha přes Docker) === +log "" +log "=== BACKUP named volumes ===" +VOLUMES=$(docker volume ls --format '{{.Name}}' \ + | grep -vE '^(host-services|docker-desktop)$' || true) + +for vol in $VOLUMES; do + ARCHIVE="$DEST/${vol}.tar.zst" + log " [volume:$vol] → $ARCHIVE" + # Nejdřív dekomprimuj na hostiteli, pak pošli do kontejneru (alpine tar neumí zstd) + if docker run --rm \ + -v "${vol}:/src:ro" \ + -v "$DEST:/dest" \ + alpine:3.20 \ + sh -c "cd /src && tar --exclude='._*' --exclude='.DS_Store' -cf - ." \ + > "$DEST/${vol}.tar" 2>>"$DEST/.errors"; then + /opt/homebrew/bin/zstd -1 -T0 -f "$DEST/${vol}.tar" -o "$ARCHIVE" 2>>"$DEST/.errors" + rm -f "$DEST/${vol}.tar" + if /opt/homebrew/bin/zstd -t "$ARCHIVE" >/dev/null 2>&1; then + SIZE=$(stat -f '%z' "$ARCHIVE") + log " OK ($(numfmt --to=iec --suffix=B "$SIZE" 2>/dev/null || echo "$SIZE B"))" + else + err " zstd -t selhal pro $ARCHIVE" + FAILED+=("volume:$vol") + fi + else + err " docker run selhal pro volume $vol" + FAILED+=("volume:$vol") + fi +done + +# === FÁZE 4: znovu spustit projekty === +log "" +log "=== UP: spouštím projekty ===" +for project in "${PROJECTS[@]}"; do + cd "$DOCKERY/$project" + if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then + log " [$project] docker compose up -d" + docker compose up -d 2>&1 || warn " [$project] up -d selhal (zkontroluj)" + fi +done + +# === FÁZE 5: hash report === +log "" +log "=== SHA-256 hashe ===" +for f in "$DEST"/*.tar.zst; do + [ -f "$f" ] || continue + H=$(shasum -a 256 "$f" | awk '{print $1}') + S=$(stat -f '%z' "$f") + log " $(basename "$f") sha256=$H size=$S" +done + +# === FÁZE 6: retention === +log "" +log "=== RETENTION: ponechávám posledních $KEEP záloh ===" +# set -u a prázdné pole +existing=() +while IFS= read -r d; do + [ -d "$d" ] && existing+=("$d") +done < <(find "$BACKUP_ROOT" -maxdepth 1 -mindepth 1 -type d | sort) + +# Pokud něco selhalo, nepromazávej +if [ "${#FAILED[@]}" -gt 0 ]; then + warn "Některé zálohy selhaly (${#FAILED[@]}), RETENTION se neprovádí" +else + if [ "${#existing[@]}" -gt "$KEEP" ]; then + for old in "${existing[@]:0:${#existing[@]}-KEEP}"; do + log " mažu starou zálohu: $(basename "$old")" + rm -rf "$old" + done + fi +fi + +log "" +log "======================================" +log " HOTOVO" +log "======================================" +log "Záloha: $DEST" +if [ "${#FAILED[@]}" -gt 0 ]; then + err "Selhalo: ${#FAILED[@]}" + for f in "${FAILED[@]}"; do err " ✗ $f"; done + exit 1 +fi +log "Vše OK." \ No newline at end of file diff --git a/docker-cleanup.sh b/docker-cleanup.sh new file mode 100644 index 0000000..70c95a4 --- /dev/null +++ b/docker-cleanup.sh @@ -0,0 +1,326 @@ +#!/usr/bin/env bash +# +# docker-cleanup.sh — bezpečné čištění Docker odpadu +# +# Cíle (vše s výslovným potvrzením nebo dry-run): +# - dangling images (:) +# - starý build cache (docker builder prune -f) +# - opuštěné (orphaned) Docker sítě — nikdy bridge/host/none +# - zastaralé images s tagem ale s referencí (REPO@sha256…) +# - dočasné soubory (XDG_CACHE_HOME pro docker, /tmp/docker-*) +# +# Nikdy nemaže: +# - používané images +# - bind-mount ani named volume data +# - běžící kontejnery +# +# Použití: +# ./docker-cleanup.sh # zobrazí plán, zeptá se na souhlas +# ./docker-cleanup.sh --dry-run # jen vypíše co by smazal +# ./docker-cleanup.sh --auto # bez ptaní (default akce) +# ./docker-cleanup.sh --dangling # jen dangling images +# ./docker-cleanup.sh --build-cache # jen build cache +# ./docker-cleanup.sh --networks # jen orphaned sítě +# ./docker-cleanup.sh --all # všechno výše uvedené +# + +set -euo pipefail + +# Default akce: žádná, vyžaduje explicitní přepínač +DO_DANGLING=0 +DO_BUILDER=0 +DO_NETWORKS=0 +DO_TMP=0 +DO_ALL=0 +DO_STOPPED=0 +AUTO=0 +DRY_RUN=0 + +usage() { + sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' +} + +for arg in "$@"; do + case "$arg" in + --dry-run|-n) DRY_RUN=1 ;; + --auto|-y) AUTO=1 ;; + --dangling) DO_DANGLING=1 ;; + --build-cache) DO_BUILDER=1 ;; + --networks) DO_NETWORKS=1 ;; + --tmp) DO_TMP=1 ;; + --stopped-containers) DO_STOPPED=1 ;; + --all) DO_ALL=1 ;; + --help|-h) usage; exit 0 ;; + *) echo "Unknown arg: $arg" >&2; exit 1 ;; + esac +done + +if [ "$DO_ALL" = "1" ]; then + DO_DANGLING=1 + DO_BUILDER=1 + DO_NETWORKS=1 + DO_TMP=1 +fi + +# Bez přepínače: nic neprovádět +if [ "$DO_DANGLING" = "0" ] && [ "$DO_BUILDER" = "0" ] && [ "$DO_NETWORKS" = "0" ] && [ "$DO_TMP" = "0" ] && [ "$DO_STOPPED" = "0" ]; then + usage + exit 0 +fi + +# Barevné výstupy +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' +fi + +log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*"; } +warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } +err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } + +# Kontrola dockeru +if ! docker info >/dev/null 2>&1; then + err "Docker není dostupný." + exit 1 +fi + +PLANNED_TOTAL=0 +PLANNED_ITEMS=() + +show_plan_header() { + printf '\n%s%s%s\n' "$BLUE" "=== PLÁN ČIŠTĚNÍ ===" "$NC" + printf 'Datum: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" + printf 'Režim: %s\n' "$([ "$DRY_RUN" = "1" ] && echo "DRY-RUN" || echo "SKUTEČNÝ MAZÁNÍ")" + printf '\n' +} + +# === DANGLING IMAGES === +plan_dangling() { + if [ "$DO_DANGLING" = "0" ]; then return; fi + printf '%s● Dangling images (:):%s\n' "$BLUE" "$NC" + + local pure_dangling=0 + local stopped_holding=0 + + while IFS=$'\t' read -r id size; do + [ -z "$id" ] && continue + # Kolik kontejnerů (včetně zastavených) na tento image ukazuje? + local holders + holders=$(docker ps -a --filter "ancestor=$id" --format '{{.Names}}\t{{.Status}}' 2>/dev/null | head -3) + if [ -n "$holders" ]; then + stopped_holding=$((stopped_holding + 1)) + printf ' %s %s ⚠ drží zastavený kontejner: %s\n' \ + "$id" "$size" "$(echo "$holders" | head -1 | awk '{print $1}')" + else + pure_dangling=$((pure_dangling + 1)) + printf ' %s %s\n' "$id" "$size" + fi + done < <(docker images -f "dangling=true" --format '{{.ID}}\t{{.Size}}') + + if [ "$pure_dangling" -eq 0 ] && [ "$stopped_holding" -eq 0 ]; then + printf ' (žádné)\n' + else + printf ' -> %d čistě dangling, %d drží zastavené kontejnery (docker image prune -f je nesmaže)\n' \ + "$pure_dangling" "$stopped_holding" + if [ "$pure_dangling" -gt 0 ]; then + PLANNED_TOTAL=$((PLANNED_TOTAL + pure_dangling)) + PLANNED_ITEMS+=("dangling-images:$pure_dangling") + fi + if [ "$stopped_holding" -gt 0 ]; then + PLANNED_ITEMS+=("dangling-with-stopped-containers:$stopped_holding") + printf ' 💡 Pro smazání těchto images je nutné nejprve odstranit zastavené kontejnery (--stopped-containers)\n' + fi + fi +} + +# === BUILD CACHE === +plan_build_cache() { + if [ "$DO_BUILDER" = "0" ]; then return; fi + printf '%s● Build cache:%s\n' "$BLUE" "$NC" + + local size + size=$(docker system df --format '{{.Type}}\t{{.Size}}' 2>/dev/null \ + | awk -F'\t' '$1 == "Build Cache" {print $2}') + if [ -z "$size" ]; then + size="0B" + fi + printf ' Build cache: %s\n' "$size" + + if [ "$size" = "0B" ]; then + printf ' (žádný ke smazání)\n' + else + PLANNED_TOTAL=$((PLANNED_TOTAL + 1)) + PLANNED_ITEMS+=("build-cache:$size") + fi +} + +# === ORPHANED NETWORKS === +plan_networks() { + if [ "$DO_NETWORKS" = "0" ]; then return; fi + printf '%s● Opuštěné Docker sítě:%s\n' "$BLUE" "$NC" + + local count=0 + # Najdi sítě, které NEpoužívá žádný kontejner + while IFS= read -r net; do + [ -z "$net" ] && continue + # Vynech výchozí systémové + case "$net" in + bridge|host|none|docker_gwbridge|docker-desktop*) continue ;; + esac + # Používá ji nějaký kontejner? + if ! docker ps --format '{{.Networks}}' 2>/dev/null \ + | grep -qw "$net"; then + printf ' %s\n' "$net" + count=$((count + 1)) + fi + done < <(docker network ls --format '{{.Name}}') + + if [ "$count" -eq 0 ]; then + printf ' (žádné opuštěné)\n' + else + printf ' -> %d opuštěných sítí\n' "$count" + PLANNED_TOTAL=$((PLANNED_TOTAL + count)) + PLANNED_ITEMS+=("networks:$count") + fi +} + +# === TMP FILES === +plan_tmp() { + if [ "$DO_TMP" = "0" ]; then return; fi + printf '%s● Dočasné soubory:%s\n' "$BLUE" "$NC" + + local total_bytes=0 + local paths=() + + # /tmp/docker-* (logy docker-update atd., starší než 7 dní) + if [ -d /tmp ]; then + while IFS= read -r f; do + [ -f "$f" ] || continue + sz=$(stat -f '%z' "$f") + total_bytes=$((total_bytes + sz)) + paths+=("$f") + done < <(/usr/bin/find /tmp -maxdepth 1 -name 'docker-*' -type f -mtime +7 2>/dev/null) + fi + + # Docker cache adresáře + [ -d "$HOME/.docker/buildx" ] && paths+=("$HOME/.docker/buildx") + [ -d "$HOME/.cache/docker" ] && paths+=("$HOME/.cache/docker") + + if [ "${#paths[@]}" -eq 0 ]; then + printf ' (žádné)\n' + else + for p in "${paths[@]}"; do + printf ' %s\n' "$p" + done + printf ' -> %d položek, cca %d B\n' "${#paths[@]}" "$total_bytes" + PLANNED_TOTAL=$((PLANNED_TOTAL + ${#paths[@]})) + PLANNED_ITEMS+=("tmp-files:${#paths[@]}") + fi +} + +confirm() { + if [ "$AUTO" = "1" ]; then + return 0 + fi + if [ "$DRY_RUN" = "1" ]; then + return 0 + fi + printf '\n%sPokračovat? [y/N]%s ' "$YELLOW" "$NC" + read -r ans + case "$ans" in + y|Y|yes|YES) return 0 ;; + *) printf 'Zrušeno.\n'; exit 1 ;; + esac +} + +# === STOPPED CONTAINERS === +plan_stopped() { + if [ "$DO_STOPPED" = "0" ]; then return; fi + printf '%s● Zastavené kontejnery:%s\n' "$BLUE" "$NC" + + local count=0 + while IFS= read -r line; do + [ -z "$line" ] && continue + # Formát: NAME STATUS + printf ' %s\n' "$line" + count=$((count + 1)) + done < <(docker ps -a --filter "status=exited" --format '{{.Names}}\t{{.Status}}' 2>/dev/null) + + if [ "$count" -eq 0 ]; then + printf ' (žádné)\n' + else + printf ' -> %d zastavených kontejnerů\n' "$count" + PLANNED_TOTAL=$((PLANNED_TOTAL + count)) + PLANNED_ITEMS+=("stopped-containers:$count") + fi +} + +# Spustit plánování +show_plan_header +plan_dangling +plan_build_cache +plan_networks +plan_stopped +plan_tmp + +printf '\n%sCelkem plánovaných akcí: %d%s\n' "$BLUE" "$PLANNED_TOTAL" "$NC" +if [ "$PLANNED_TOTAL" -eq 0 ]; then + log "Není co čistit." + exit 0 +fi + +# Bez potvrzení v dry-run režimu +if [ "$DRY_RUN" = "1" ]; then + printf '\n%sDRY-RUN: žádná akce neprovedena%s\n' "$YELLOW" "$NC" + exit 0 +fi + +confirm + +# === EXECUTE === +log "" +log "=== PROVÁDÍM ČIŠTĚNÍ ===" + +if [ "$DO_DANGLING" = "1" ]; then + log "docker image prune -f (dangling only)" + docker image prune -f 2>&1 || warn "dangling prune selhal" +fi + +if [ "$DO_BUILDER" = "1" ]; then + log "docker builder prune -f" + docker builder prune -f 2>&1 || warn "builder prune selhal" +fi + +if [ "$DO_NETWORKS" = "1" ]; then + log "docker network prune -f (filtrováno na opuštěné)" + # docker network prune umí vyfiltrovat, ale my jsme již identifikovali konkrétní + while IFS= read -r net; do + case "$net" in bridge|host|none|docker_gwbridge|docker-desktop*) continue ;; esac + if ! docker ps --format '{{.Networks}}' 2>/dev/null | grep -qw "$net"; then + log " rm network: $net" + docker network rm "$net" 2>&1 || warn " rm $net selhal" + fi + done < <(docker network ls --format '{{.Name}}') +fi + +if [ "$DO_TMP" = "1" ]; then + log "mazání starých /tmp/docker-* (≥7 dní)" + # -print -delete musí být v tomto pořadí; -delete sám o sobě na macOS BSD find někdy selže + /usr/bin/find /tmp -maxdepth 1 -name 'docker-*' -type f -mtime +7 -print 2>&1 | while read -r f; do + log " rm: $f" + /bin/rm -f "$f" 2>&1 || warn " rm $f selhal" + done +fi + +if [ "$DO_STOPPED" = "1" ]; then + log "docker container prune -f (zastavené kontejnery)" + docker container prune -f 2>&1 || warn "container prune selhal" +fi + +log "" +log "=== STAV PO ČIŠTĚNÍ ===" +docker system df 2>&1 | head -20 + +log "" +log "HOTOVO. Uvolněno viz výše ^" \ No newline at end of file diff --git a/docker-restore.sh b/docker-restore.sh new file mode 100644 index 0000000..49c524f --- /dev/null +++ b/docker-restore.sh @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +# +# docker-restore.sh — bezpečná obnova Docker projektů z tar.zst zálohy +# +# - zobrazí dostupné zálohy v $BACKUP_ROOT +# - vyžádá potvrzení (nebo --auto) +# - zastaví vybrané projekty +# - přesune současná data do _pre-restore-/ (NE rm -rf!) +# - obnoví data ze zálohy +# - vyčistí AppleDouble _* před docker compose up -d +# - spustí kontejnery a ověří zdraví +# +# Použití: +# ./docker-restore.sh # interaktivně: vybere zálohu + projekty +# ./docker-restore.sh --list # jen seznam dostupných záloh +# ./docker-restore.sh 2026-07-29_19-21-00 # konkrétní záloha, interaktivní výběr projektů +# ./docker-restore.sh 2026-07-29_19-21-00 nginx vaultwarden # konkrétní záloha + projekty +# ./docker-restore.sh --auto 2026-07-29_19-21-00 nginx # bez ptaní +# ./docker-restore.sh --help # nápověda +# + +set -euo pipefail + +DOCKERY="${DOCKERY:-}" +BACKUP_ROOT="${BACKUP_ROOT:-}" +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}" +AUTO=0 +LIST_ONLY=0 +TS_ARG="" +PROJECTS_RAW=() + +usage() { + sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' +} + +for arg in "$@"; do + case "$arg" in + --help|-h) usage; exit 0 ;; + --auto|-y) AUTO=1 ;; + --list|-l) LIST_ONLY=1 ;; + --*) echo "Unknown flag: $arg" >&2; exit 1 ;; + *) + if [ -z "$TS_ARG" ]; then + TS_ARG="$arg" + else + PROJECTS_RAW+=("$arg") + fi + ;; + esac +done + +# Barevné výstupy +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' +fi + +log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*"; } +warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } +err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } + +confirm() { + if [ "$AUTO" = "1" ]; then return 0; fi + printf '%s%s%s ' "$YELLOW" "$1" "$NC" + read -r ans + case "$ans" in + y|Y|yes|YES) return 0 ;; + *) printf 'Zrušeno.\n'; exit 1 ;; + esac +} + +# Kontrola +if ! docker info >/dev/null 2>&1; then + err "Docker není dostupný." + exit 1 +fi +if [ ! -d "$DOCKERY" ]; then + err "Dockery adresář neexistuje: $DOCKERY" + exit 1 +fi +if [ ! -d "$BACKUP_ROOT" ]; then + err "Backup adresář neexistuje: $BACKUP_ROOT" + exit 1 +fi + +# === Seznam dostupných záloh === +list_backups() { + printf '%s%s%s\n' "$BLUE" "=== DOSTUPNÉ ZÁLOHY ===" "$NC" + found=0 + while IFS= read -r d; do + [ -d "$d" ] || continue + ts=$(basename "$d") + archives=$(/bin/ls "$d"/*.tar.zst 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') + size=$(/usr/bin/du -sh "$d" 2>/dev/null | /usr/bin/awk '{print $1}') + printf ' %s %s archivů %s\n' "$ts" "$archives" "$size" + found=$((found + 1)) + done < <(/usr/bin/find "$BACKUP_ROOT" -maxdepth 1 -mindepth 1 -type d \ + | /usr/bin/sort -r) + if [ "$found" -eq 0 ]; then + printf ' (žádné)\n' + fi +} + +# Zobrazit seznam, pokud --list +if [ "$LIST_ONLY" = "1" ]; then + list_backups + exit 0 +fi + +# === Vyber zálohu === +if [ -z "$TS_ARG" ]; then + list_backups + printf '\n%sZadejte timestamp zálohy (např. 2026-07-29_19-21-00):%s ' "$YELLOW" "$NC" + read -r TS_ARG +fi + +BACKUP_DIR="$BACKUP_ROOT/$TS_ARG" +if [ ! -d "$BACKUP_DIR" ]; then + err "Záloha neexistuje: $BACKUP_DIR" + list_backups + exit 1 +fi + +log "Vybraná záloha: $BACKUP_DIR" +log "Archivy v záloze:" +/bin/ls -1 "$BACKUP_DIR"/*.tar.zst 2>/dev/null | /usr/bin/sed 's/^/ /' || { + err "Záloha neobsahuje žádné archivy" + exit 1 +} + +# === Detekce projektů v záloze === +DETECTED=() +for archive in "$BACKUP_DIR"/*.tar.zst; do + name=$(basename "$archive" .tar.zst) + # Rozlišení: project archive vs volume archive + # Project: .tar.zst v Dockery/ + # Volume: .tar.zst v named volumes + if [ -d "$DOCKERY/$name" ] && compgen -G "$DOCKERY/$name/docker-compose.y*ml" > /dev/null; then + DETECTED+=("project:$name") + else + # Může to být volume; docker volume ls to ověří + if docker volume inspect "$name" >/dev/null 2>&1; then + DETECTED+=("volume:$name") + else + DETECTED+=("unknown:$name") + fi + fi +done + +log "" +log "Položky v záloze:" +for d in "${DETECTED[@]}"; do printf ' %s\n' "$d"; done + +# === Vyber projekty k obnově === +if [ "${#PROJECTS_RAW[@]}" -eq 0 ]; then + PROJECTS=() + for d in "${DETECTED[@]}"; do + type=${d%%:*} + name=${d#*:} + if [ "$type" = "project" ]; then + PROJECTS+=("$name") + fi + done + + if [ "${#PROJECTS[@]}" -eq 0 ]; then + err "V záloze nebyly nalezeny žádné projekty k obnově" + exit 1 + fi + + printf '\n%sDostupné projekty v této záloze:%s\n' "$BLUE" "$NC" + for i in "${!PROJECTS[@]}"; do + printf ' %d) %s\n' $((i+1)) "${PROJECTS[$i]}" + done + printf '%sVyberte projekty (např. 1 3 nebo "all"):%s ' "$YELLOW" "$NC" + read -r sel + case "$sel" in + all|"") SELECTED=("${PROJECTS[@]}") ;; + *) + SELECTED=() + for n in $sel; do + idx=$((n - 1)) + if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#PROJECTS[@]}" ]; then + SELECTED+=("${PROJECTS[$idx]}") + fi + done + ;; + esac +else + SELECTED=("${PROJECTS_RAW[@]}") +fi + +if [ "${#SELECTED[@]}" -eq 0 ]; then + err "Nebyly vybrány žádné projekty" + exit 1 +fi + +# === Plán obnovy === +TS_NOW="$(date +%Y-%m-%d_%H-%M-%S)" + +printf '\n%s%s%s\n' "$BLUE" "=== PLÁN OBNOVY ===" "$NC" +printf 'Záloha: %s\n' "$BACKUP_DIR" +printf 'Projekty: %s\n' "${SELECTED[*]}" +printf 'Pre-restore: %s/_pre-restore-%s/\n' "$DOCKERY" "$TS_NOW" +printf '\n' + +confirm "Pokračovat v obnově? (PŘEPSOU se současná data projektů!) [y/N]" + +# === Stop kontejnerů vybraných projektů === +log "" +log "=== STOP: zastavuji kontejnery vybraných projektů ===" +for project in "${SELECTED[@]}"; do + cd "$DOCKERY/$project" + if docker compose ps -q 2>/dev/null | grep -q .; then + log " [$project] docker compose stop" + docker compose stop 2>&1 || warn " [$project] stop selhal" + fi +done + +# === Move-aside současných dat === +log "" +log "=== MOVE-ASIDE: přesouvám současná data do _pre-restore-$TS_NOW/ ===" +for project in "${SELECTED[@]}"; do + SRC="$DOCKERY/$project" + DEST="$DOCKERY/_pre-restore-$TS_NOW/$project" + mkdir -p "$DEST" + # Najdi velké podadresáře a přesuň je (NEVYMAŽ!) + for sub in $(/bin/ls -A "$SRC" 2>/dev/null); do + # Přeskoč .DS_Store, samotný _pre-restore-*, docker-compose soubory + case "$sub" in + _pre-restore-*) continue ;; + docker-compose.y*ml) continue ;; + .env|.env.*) continue ;; # .env necháme — je to runtime konfigurace + .DS_Store) continue ;; + esac + if [ -e "$SRC/$sub" ]; then + log " [$project] mv $sub → _pre-restore-$TS_NOW/$project/" + mv "$SRC/$sub" "$DEST/$sub" 2>&1 || warn " mv $sub selhal" + fi + done +done + +# === Restore ze zálohy === +log "" +log "=== RESTORE: extrahuji ze zálohy ===" +FAILED=() +for project in "${SELECTED[@]}"; do + ARCHIVE="$BACKUP_DIR/${project}.tar.zst" + if [ ! -f "$ARCHIVE" ]; then + warn "[$project] archiv neexistuje: $ARCHIVE (přeskakuji)" + FAILED+=("$project:no-archive") + continue + fi + + log " [$project] extrahuji do $DOCKERY/$project/" + # Archiv má cesty jako Volumes/SSD-data/Dockery//... + # Extrahujeme s --strip-components=3, abychom získali přímo /... + cd "$DOCKERY" + if /opt/homebrew/bin/zstd -d -c "$ARCHIVE" \ + | /usr/bin/tar --strip-components=3 -xf - 2>>"$BACKUP_DIR/.restore-errors-$TS_NOW"; then + log " OK" + else + err " extrakce selhala pro $project" + FAILED+=("$project:extract-failed") + fi +done + +# === Vyčistit AppleDouble _* (prevence MariaDB/PG smyček) === +log "" +log "=== CLEAN APPLE DOUBLE: mažu ._* před startem ===" +for project in "${SELECTED[@]}"; do + count=$(/usr/bin/find "$DOCKERY/$project" -name '._*' -type f 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') + if [ "$count" -gt 0 ]; then + log " [$project] nalezeno $count ._* souborů, mažu" + /usr/bin/find "$DOCKERY/$project" -name '._*' -type f -delete 2>/dev/null || true + fi + # .DS_Store + dscount=$(/usr/bin/find "$DOCKERY/$project" -name '.DS_Store' -type f 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') + if [ "$dscount" -gt 0 ]; then + log " [$project] nalezeno $dscount .DS_Store, mažu" + /usr/bin/find "$DOCKERY/$project" -name '.DS_Store' -type f -delete 2>/dev/null || true + fi +done + +# === Up kontejnerů === +log "" +log "=== UP: spouštím kontejnery ===" +for project in "${SELECTED[@]}"; do + cd "$DOCKERY/$project" + log " [$project] docker compose up -d" + docker compose up -d 2>&1 | /usr/bin/head -10 || warn " [$project] up -d selhal" +done + +# === Health check === +log "" +log "=== HEALTH CHECK: čekám na zdraví (max ${HEALTH_TIMEOUT}s) ===" +sleep 10 +elapsed=10 +all_healthy=1 +while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do + sleep 5 + elapsed=$((elapsed + 5)) + for project in "${SELECTED[@]}"; do + for c in $(docker compose -f "$DOCKERY/$project/docker-compose.yml" ps -a --format '{{.Names}}' 2>/dev/null); do + status=$(docker inspect --format='{{.State.Status}}' "$c" 2>/dev/null) + health=$(docker inspect --format='{{.State.Health.Status}}' "$c" 2>/dev/null) + if [ "$status" != "running" ]; then + log " [$project/$c] state=$status health=$health" + all_healthy=0 + fi + done + done + # Pokud všechny kontejnery running a žádné health check nehlásí starting/unhealthy, ukončíme + starting=$(docker ps --format '{{.Names}}' \ + | while read -r c; do docker inspect --format='{{.State.Health.Status}}' "$c" 2>/dev/null; done \ + | /usr/bin/grep -c '^starting$' || true) + if [ "$starting" = "0" ]; then + break + fi +done + +# === Report === +log "" +log "======================================" +log " SHRNUTÍ OBNOVY" +log "======================================" +log "Obnoveno:" +for p in "${SELECTED[@]}"; do log " ✓ $p"; done +if [ "${#FAILED[@]}" -gt 0 ]; then + err "Selhalo:" + for f in "${FAILED[@]}"; do err " ✗ $f"; done +fi +log "" +log "Současná data jsou v: $DOCKERY/_pre-restore-$TS_NOW/" +log "Po ověření můžete tento adresář ručně smazat:" +log " rm -rf $DOCKERY/_pre-restore-$TS_NOW" +log "" +if [ "$all_healthy" = "1" ]; then + log "Všechny kontejnery běží." +else + warn "Některé kontejnery nejsou v pořádku — zkontrolujte logy." +fi \ No newline at end of file diff --git a/docker-update.sh b/docker-update.sh new file mode 100644 index 0000000..5b01f2b --- /dev/null +++ b/docker-update.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# +# docker-update.sh — bezpečná aktualizace Docker Compose projektů +# +# - zastaví všechny kontejnery, +# - stáhne nové image (docker compose pull), +# - ověří, že se image skutečně změnil (jinak přeskočí), +# - spustí kontejnery z nových image, +# - zkontroluje health checks, +# - ponechá staré image pro případný rollback (docker image prune -f na konci, +# ale jen dangling, ne všechno). +# +# Použití: +# ./docker-update.sh # aktualizuje všechny projekty v +# ./docker-update.sh nginx mattermost # aktualizuje vybrané projekty +# ./docker-update.sh --help # nápověda +# + +set -euo pipefail + +usage() { + sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//' +} + +for arg in "$@"; do + case "$arg" in + --help|-h) usage; exit 0 ;; + esac +done + +DOCKERY="${DOCKERY:-}" +LOG="${LOG:-/tmp/docker-update-$(date +%Y-%m-%d_%H-%M).log}" +PRUNE_DANGLING="${PRUNE_DANGLING:-1}" # 1 = docker image prune -f (dangling only) +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}" # sekund na health check po startu + +# Barevné výstupy (pokud terminál umí) +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; NC='' +fi + +log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } +warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } +err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } + +# Kontrola, že docker běží +if ! docker info >/dev/null 2>&1; then + err "Docker není dostupný. Spusť Docker Desktop a zkus znovu." + exit 1 +fi + +# Najdi projekty +if [ "$#" -gt 0 ]; then + PROJECTS=("$@") +else + PROJECTS=() + for d in "$DOCKERY"/*/; do + [ -d "$d" ] || continue + if compgen -G "$d/docker-compose.y*ml" > /dev/null; then + PROJECTS+=("$(basename "$d")") + fi + done +fi + +if [ "${#PROJECTS[@]}" -eq 0 ]; then + err "Nebyly nalezeny žádné Docker Compose projekty v $DOCKERY" + exit 1 +fi + +log "Nalezeno ${#PROJECTS[@]} projektů: ${PROJECTS[*]}" + +# Pre-flight: ulož si, co běží +log "=== PRE-FLIGHT ===" +BEFORE_RUNNING=$(docker ps --format '{{.Names}}') +log "Běžící kontejnery před aktualizací:" +printf ' %s\n' $BEFORE_RUNNING + +UPDATED=() +SKIPPED=() +FAILED=() + +for project in "${PROJECTS[@]}"; do + cd "$DOCKERY/$project" + if ! compgen -G "docker-compose.y*ml" > /dev/null; then + warn "[$project] nemá docker-compose soubor, přeskakuji" + SKIPPED+=("$project") + continue + fi + + log "" + log "=== [$project] ===" + + # Zaznamenej aktuální image ID + BEFORE_DIGESTS=$(docker compose ps -q 2>/dev/null \ + | xargs -I{} docker inspect --format='{{.Image}}' {} 2>/dev/null \ + | sort -u) + + # Pull + log "[$project] docker compose pull" + if ! docker compose pull 2>&1 | tee -a "$LOG"; then + err "[$project] pull selhal" + FAILED+=("$project") + continue + fi + + # Ověř, že se image změnil + AFTER_DIGESTS=$(docker compose config --images 2>/dev/null \ + | xargs -I{} docker image inspect --format='{{index .RepoDigests 0}}' "{}" 2>/dev/null \ + | sort -u) + + CHANGED=0 + for d in $AFTER_DIGESTS; do + if ! grep -qx "$d" <<<"$BEFORE_DIGESTS"; then + CHANGED=1 + break + fi + done + + if [ "$CHANGED" -eq 0 ]; then + log "[$project] žádná změna, přeskakuji up -d" + SKIPPED+=("$project") + continue + fi + + # Up -d (vždy background kvůli shell guardu) + log "[$project] docker compose up -d" + if ! docker compose up -d 2>&1 | tee -a "$LOG"; then + err "[$project] up -d selhal" + FAILED+=("$project") + continue + fi + + # Počkej na health check + log "[$project] čekám na health check (max ${HEALTH_TIMEOUT}s)" + elapsed=0 + healthy=0 + while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do + sleep 5 + elapsed=$((elapsed + 5)) + unhealthy=$(docker compose ps 2>/dev/null \ + | tail -n +2 \ + | awk '{print $NF}' \ + | grep -vE 'healthy|Up' || true) + if [ -z "$unhealthy" ]; then + healthy=1 + break + fi + done + + if [ "$healthy" -eq 1 ]; then + log "[$project] OK" + UPDATED+=("$project") + else + warn "[$project] běží, ale ne všechny health checks prošly (zkontroluj ručně)" + UPDATED+=("$project:health_unknown") + fi +done + +# Prune dangling images (bezpečné) +if [ "$PRUNE_DANGLING" = "1" ]; then + log "" + log "=== DOCKER IMAGE PRUNE -f (dangling only) ===" + docker image prune -f 2>&1 | tee -a "$LOG" || true +fi + +# Report +log "" +log "======================================" +log " SHRNUTÍ" +log "======================================" +log "Aktualizováno: ${#UPDATED[@]} projekt(ů)" +for p in "${UPDATED[@]:-}"; do [ -n "$p" ] && log " ✓ $p"; done +log "Přeskočeno (beze změny): ${#SKIPPED[@]}" +for p in "${SKIPPED[@]:-}"; do [ -n "$p" ] && log " - $p"; done +if [ "${#FAILED[@]}" -gt 0 ]; then + log "Selhalo: ${#FAILED[@]}" + for p in "${FAILED[@]}"; do err " ✗ $p"; done +fi +log "" +log "Log: $LOG" \ No newline at end of file diff --git a/mount-nas.sh b/mount-nas.sh new file mode 100644 index 0000000..d3a09a1 --- /dev/null +++ b/mount-nas.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# +# mount-nas.sh — připojení/odpojení NAS SMB share do +# +# Tiché (nobrowse), vhodné pro ruční i automatizované použití. +# +# Konfigurace přes env proměnné (mají defaulty): +# SMB_SERVER default +# SMB_SHARE default Multimedia +# SMB_USER default mates +# TARGET default +# KEYCHAIN_SERVICE default SMB:/Multimedia +# NET_CHECK_TIMEOUT default 5 (sekund) +# +# Akce (default = auto): +# --auto mount pokud není, skončí úspěšně pokud je již připojen +# --mount mount (chyba pokud již připojen) +# --umount umount (chyba pokud není připojen) +# --status vypíše aktuální stav, exit 0/1 podle toho zda je mount aktivní +# --help nápověda +# +# Exit kódy: +# 0 OK (mount proběhl nebo již připojen / umount proběhl) +# 10 cílový mount bod (nadřazený adresář) neexistuje +# 11 záznam v Klíčence chybí +# 12 záznam v Klíčence je prázdný +# 20 SMB server nedostupný (TCP 445) +# 30 mount selhal (auth, mount error, ...) +# 31 umount selhal (resource busy, ...) +# + +set -uo pipefail + +# ===== Konfigurace ===== +SMB_SERVER="${SMB_SERVER:-}" +SMB_SHARE="${SMB_SHARE:-Multimedia}" +SMB_USER="${SMB_USER:-mates}" +TARGET="${TARGET:-}" +KEYCHAIN_SERVICE="${KEYCHAIN_SERVICE:-SMB:$SMB_SERVER/$SMB_SHARE}" +NET_CHECK_TIMEOUT="${NET_CHECK_TIMEOUT:-5}" +LOG_FILE="${LOG_FILE:-/tmp/mount-nas.log}" +PASSWORD_FILE="${PASSWORD_FILE:-}" + +# ===== Barvy (jen pokud terminál) ===== +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; NC='' +fi + +# ===== Pomocné funkce ===== +log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%Y-%m-%d %H:%M:%S')" "$NC" "$*" | tee -a "$LOG_FILE"; } +warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%Y-%m-%d %H:%M:%S')" "$NC" "$*" | tee -a "$LOG_FILE"; } +err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%Y-%m-%d %H:%M:%S')" "$NC" "$*" | tee -a "$LOG_FILE"; } +die() { err "$@"; exit "$1"; } + +usage() { + sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' +} + +# ===== Parsování argumentů ===== +# Podpora: --password-file CESTA (s mezerou) +# --password-file=CESTA (s rovnítkem) +# Argumenty s hodnotou je nutné ručně posunout v poli, protože 'for arg in "$@"' +# neumožní 'shift 2' uvnitř smyčky bez komplikací. Přepíšeme do další proměnné. +ACTION="auto" +PASSWORD_FILE_ARG="" +ARGS=() +while [ "$#" -gt 0 ]; do + arg="$1" + case "$arg" in + --help|-h) usage; exit 0 ;; + --auto) ACTION="auto" ;; + --mount) ACTION="mount" ;; + --umount) ACTION="umount" ;; + --status) ACTION="status" ;; + --password-file) + PASSWORD_FILE_ARG="$2"; shift 2; continue ;; + --password-file=*) + PASSWORD_FILE_ARG="${arg#--password-file=}"; shift ;; + *) err "Unknown arg: $arg"; usage; exit 1 ;; + esac + ARGS+=("$arg") + shift +done + +# ===== Detekce aktuálního stavu ===== +is_mounted() { + mount 2>/dev/null | grep -qE "//$SMB_USER@$SMB_SERVER/$SMB_SHARE on $TARGET \(smbfs," +} + +# ===== 1. Kontrola mount root ===== +MOUNT_ROOT="$(dirname "$TARGET")" +if [ ! -d "$MOUNT_ROOT" ]; then + die 10 "mount root neexistuje: $MOUNT_ROOT (čeká se na SSD)" +fi + +# ===== Akce: status ===== +if [ "$ACTION" = "status" ]; then + if is_mounted; then + log "OK: $TARGET je připojen ($SMB_SERVER/$SMB_SHARE)" + mount | grep -E "$TARGET" + exit 0 + else + warn "NEpřipojen: $TARGET" + exit 1 + fi +fi + +# ===== Akce: umount ===== +if [ "$ACTION" = "umount" ]; then + if ! is_mounted; then + die 1 "$TARGET není připojen" + fi + log "Umount $TARGET" + if /sbin/umount "$TARGET" 2>>"$LOG_FILE"; then + log "OK: umount úspěšný" + exit 0 + else + die 31 "umount selhal (resource busy? zavři aplikace/kontejnery)" + fi +fi + +# ===== Akce: mount nebo auto ===== +if [ "$ACTION" = "auto" ] && is_mounted; then + log "OK: $TARGET je již připojen" + exit 0 +fi + +if [ "$ACTION" = "mount" ] && is_mounted; then + die 30 "$TARGET je již připojen (použij --auto nebo --umount)" +fi + +# ===== 2. Test dostupnosti SMB serveru ===== +log "Test dostupnosti SMB serveru $SMB_SERVER:445 (timeout ${NET_CHECK_TIMEOUT}s)" +if ! /usr/bin/nc -z -G "$NET_CHECK_TIMEOUT" "$SMB_SERVER" 445 2>/dev/null; then + die 20 "SMB server $SMB_SERVER nedostupný (TCP 445)" +fi +log "OK: SMB server odpovídá" + +# ===== 3. Heslo z Klíčenky nebo ze souboru ===== +# Priorita: +# 1. --password-file (heslo v souboru s chmod 600) +# 2. $PASSWORD_FILE env proměnná +# 3. Klíčenka macOS (service = KEYCHAIN_SERVICE) + +PASSWORD="" +if [ -n "${PASSWORD_FILE_ARG:-}" ] && [ -n "$PASSWORD_FILE_ARG" ]; then + if [ ! -f "$PASSWORD_FILE_ARG" ]; then + die 11 "soubor s heslem neexistuje: $PASSWORD_FILE_ARG" + fi + if [ ! -r "$PASSWORD_FILE_ARG" ]; then + die 11 "soubor s heslem nelze číst (oprávnění): $PASSWORD_FILE_ARG" + fi + PASSWORD=$(cat "$PASSWORD_FILE_ARG") + PASSWORD="${PASSWORD%%$'\n'}" # odřízne trailing newline + if [ -z "$PASSWORD" ]; then + die 12 "soubor s heslem je prázdný: $PASSWORD_FILE_ARG" + fi + log "Heslo načteno ze souboru: $PASSWORD_FILE_ARG" +elif [ -n "$PASSWORD_FILE" ] && [ -f "$PASSWORD_FILE" ]; then + PASSWORD=$(cat "$PASSWORD_FILE") + PASSWORD="${PASSWORD%%$'\n'}" + log "Heslo načteno z PASSWORD_FILE: $PASSWORD_FILE" +else + if ! PASSWORD=$(/usr/bin/security find-generic-password -a "$SMB_USER" -s "$KEYCHAIN_SERVICE" -w 2>/dev/null); then + die 11 "Klíčenka chybí ($KEYCHAIN_SERVICE)" + fi +fi +if [ -z "$PASSWORD" ]; then + die 12 "Heslo je prázdné" +fi + +# ===== 4. Vlastní mount přes mount_smbfs s heslem v URL ===== +# +# mount_smbfs podporuje syntaxi: +# //user:password@server/share /mount/point +# To znamená, že heslo se předá přímo, bez expect a bez interaktivního promptu. +# (Bezpečnost: heslo je vidět v 'ps' po dobu trvání mountu, což jsou sekundy.) + +log "Spouštím mount_smbfs //$SMB_USER@$SMB_SERVER/$SMB_SHARE -> $TARGET" + +# Sestav URL s heslem. Pro hesla se speciálními znaky (%, @, :) musíme +# escapovat, ale heslo z klíčenky většinou tyto znaky nemá. +# Pro jistotu zakódujeme heslo pomocí printf %q a pak odstraníme quotes. + +# mount_smbfs -N zakáže prompt. Pokud heslo v URL je neplatné, mount selže. +# Nekombinujeme -N s heslem v URL — buď heslo v URL, nebo -N bez hesla. +ESCAPED_URL="//$SMB_USER:$PASSWORD@$SMB_SERVER/$SMB_SHARE" + +# Pro mount_smbfs chceme tichý režim (-s), ale chyby potřebujeme v logu. +if /sbin/mount_smbfs -o nobrowse,nosuid "$ESCAPED_URL" "$TARGET" 2>>"$LOG_FILE"; then + log "OK: mount_smbfs exit 0" +else + rc=$? + # Heslo v URL může selhat s různými chybami: + # 1 = chyba argumentu (heslo obsahuje nepovolené znaky) + # 64 = mount_smbfs obecná chyba (auth, share neexistuje, ...) + die 30 "mount_smbfs selhal (exit $rc)" +fi + +# Bezpečnost: ihned smaž heslo z paměti +unset PASSWORD +unset ESCAPED_URL + +# ===== 5. Ověření, že mount opravdu proběhl ===== +if ! is_mounted; then + die 30 "mount exit 0 ale SMB share není v /sbin/mount" +fi +log "OK: mount úspěšný ($SMB_SERVER/$SMB_SHARE -> $TARGET)" \ No newline at end of file diff --git a/update-macos.sh b/update-macos.sh new file mode 100644 index 0000000..15ca625 --- /dev/null +++ b/update-macos.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e + +echo "" +echo "======================================" +echo " macOS + Homebrew Update" +echo "======================================" +echo "" + +# Kontrola Homebrew +if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew není nainstalovaný." + exit 1 +fi + +echo "→ Aktualizace Homebrew metadata..." +brew update + +echo "" +echo "→ Upgrade formulae..." +brew upgrade + +echo "" +echo "→ Upgrade casks..." +brew upgrade --cask + +echo "" +echo "→ Cleanup..." +brew cleanup + +echo "" +echo "→ Autoremove..." +brew autoremove + +echo "" +echo "→ Health check..." +brew doctor + +echo "" +echo "→ Outdated packages:" +brew outdated || true + +echo "" +echo "======================================" +echo " Hotovo" +echo "======================================" +echo ""