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 <server-ip>, 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).
This commit is contained in:
+18
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# disk-watch.sh — sledování volného místa na Docker datech
|
||||||
|
#
|
||||||
|
# Kontroluje:
|
||||||
|
# - <ssd-data-mount> (Docker data)
|
||||||
|
# - <backup-disk-mount> (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-data-mount>}"
|
||||||
|
SSD_BACKUP="${SSD_BACKUP:-<backup-disk-mount>}"
|
||||||
|
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 <docker-projects-root>:\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
|
||||||
@@ -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 <backup-root>/
|
||||||
|
# - pro každý projekt v <docker-projects-root> 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:-<docker-projects-root>}"
|
||||||
|
BACKUP_ROOT="${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 <docker-projects-root>/$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" \
|
||||||
|
"<docker-projects-root>/$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."
|
||||||
@@ -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 (<none>:<none>)
|
||||||
|
# - starý build cache (docker builder prune -f)
|
||||||
|
# - opuštěné (orphaned) Docker sítě — nikdy bridge/host/none
|
||||||
|
# - zastaralé images s tagem <none> 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 (<none>:<none>):%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 ^"
|
||||||
@@ -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-<TS>/ (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:-<docker-projects-root>}"
|
||||||
|
BACKUP_ROOT="${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: <project>.tar.zst v Dockery/
|
||||||
|
# Volume: <volname>.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/<project>/...
|
||||||
|
# Extrahujeme s --strip-components=3, abychom získali přímo <project>/...
|
||||||
|
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
|
||||||
@@ -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-projects-root>
|
||||||
|
# ./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:-<docker-projects-root>}"
|
||||||
|
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"
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# mount-nas.sh — připojení/odpojení NAS SMB share do <multimedia-mount>
|
||||||
|
#
|
||||||
|
# Tiché (nobrowse), vhodné pro ruční i automatizované použití.
|
||||||
|
#
|
||||||
|
# Konfigurace přes env proměnné (mají defaulty):
|
||||||
|
# SMB_SERVER default <nas-ip>
|
||||||
|
# SMB_SHARE default Multimedia
|
||||||
|
# SMB_USER default mates
|
||||||
|
# TARGET default <multimedia-mount>
|
||||||
|
# KEYCHAIN_SERVICE default SMB:<nas-ip>/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:-<nas-ip>}"
|
||||||
|
SMB_SHARE="${SMB_SHARE:-Multimedia}"
|
||||||
|
SMB_USER="${SMB_USER:-mates}"
|
||||||
|
TARGET="${TARGET:-<multimedia-mount>}"
|
||||||
|
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 <cesta> (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)"
|
||||||
@@ -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 ""
|
||||||
Reference in New Issue
Block a user