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:
2026-08-01 19:51:59 +02:00
commit 682f522cb9
10 changed files with 1560 additions and 0 deletions
+18
View File
@@ -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
+21
View File
@@ -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.
+26
View File
@@ -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
View File
@@ -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
+203
View File
@@ -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."
+326
View File
@@ -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 ^"
+342
View File
@@ -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
+181
View File
@@ -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
View File
@@ -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)"
+48
View File
@@ -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 ""