552 lines
17 KiB
Bash
Executable File
552 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
################################################################################
|
|
# Synchronisierungs-Script: mac-home/home-content <-> Home-Directory
|
|
#
|
|
# Synchronisiert nur die Dateien, die im Git-Repo versioniert sind.
|
|
# Unterstützt mehrere Modi mit Konflikt-Handling.
|
|
#
|
|
# Strategie bei Changes:
|
|
# - Content-basiert via md5sum (verlässlicher als Timestamp)
|
|
# - Automatische Backups bei lokalen Änderungen
|
|
# - Mehrere Modi für verschiedene Szenarien
|
|
################################################################################
|
|
|
|
# Note: Removed 'set -euo pipefail' as it breaks while-read loops with subshells
|
|
# We handle errors explicitly instead
|
|
|
|
# Farben
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Verzeichnisse
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_DIR="$SCRIPT_DIR/home-content"
|
|
HOME_DIR="$HOME"
|
|
BACKUP_DIR="$SCRIPT_DIR/.sync-backups"
|
|
LOG_FILE="$SCRIPT_DIR/.sync-home-content.log"
|
|
|
|
# Konfiguration
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
DRY_RUN=false
|
|
VERBOSE=false
|
|
INTERACTIVE=false
|
|
AUTO_BACKUP=true
|
|
|
|
################################################################################
|
|
# Hilfsfunktionen
|
|
################################################################################
|
|
|
|
log() {
|
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"
|
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
|
|
}
|
|
|
|
error() {
|
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
|
echo "[ERROR] $*" >> "$LOG_FILE"
|
|
}
|
|
|
|
success() {
|
|
echo -e "${GREEN}[OK]${NC} $*"
|
|
echo "[OK] $*" >> "$LOG_FILE"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $*"
|
|
echo "[WARN] $*" >> "$LOG_FILE"
|
|
}
|
|
|
|
verbose() {
|
|
if [[ "$VERBOSE" == true ]]; then
|
|
echo -e "${BLUE}[DEBUG]${NC} $*"
|
|
echo "[DEBUG] $*" >> "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Berechne md5sum für eine Datei (mit Fallback für verschiedene OSs)
|
|
################################################################################
|
|
|
|
get_checksum() {
|
|
local file="$1"
|
|
if [[ -f "$file" ]]; then
|
|
if command -v md5 &> /dev/null; then
|
|
# macOS
|
|
md5 -q "$file" 2>/dev/null
|
|
elif command -v md5sum &> /dev/null; then
|
|
# Linux
|
|
md5sum "$file" 2>/dev/null | awk '{print $1}'
|
|
else
|
|
error "Weder md5 noch md5sum verfügbar"
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Erstelle Backup einer Datei
|
|
################################################################################
|
|
|
|
backup_file() {
|
|
local src="$1"
|
|
local desc="$2"
|
|
|
|
[[ ! -f "$src" ]] && return 0
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
local backup_file="$BACKUP_DIR/$(basename "$src").$TIMESTAMP.backup"
|
|
cp "$src" "$backup_file"
|
|
log "Backup erstellt: $desc → $backup_file"
|
|
}
|
|
|
|
################################################################################
|
|
# Status einer Datei prüfen
|
|
################################################################################
|
|
|
|
# Vergleicht Datei im Repo mit Home-Dir
|
|
# Returns:
|
|
# 0 = identisch
|
|
# 1 = nur in Repo
|
|
# 2 = nur im Home
|
|
# 3 = unterschiedlicher Inhalt
|
|
# 4 = nur im Repo, nicht im Home
|
|
|
|
check_file_status() {
|
|
local repo_file="$1"
|
|
local home_file="$2"
|
|
local repo_name=$(basename "$repo_file")
|
|
|
|
if [[ ! -f "$repo_file" ]]; then
|
|
return 1 # nicht im Repo
|
|
fi
|
|
|
|
if [[ ! -f "$home_file" ]]; then
|
|
return 4 # nur im Repo
|
|
fi
|
|
|
|
local repo_sum=$(get_checksum "$repo_file")
|
|
local home_sum=$(get_checksum "$home_file")
|
|
|
|
if [[ "$repo_sum" == "$home_sum" ]]; then
|
|
return 0 # identisch
|
|
else
|
|
return 3 # unterschiedlich
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: STATUS
|
|
# Zeigt Unterschiede zwischen Repo und Home
|
|
################################################################################
|
|
|
|
mode_status() {
|
|
log "Status: Vergleiche Repo mit Home-Dir..."
|
|
|
|
local identical=0
|
|
local modified=0
|
|
local repo_only=0
|
|
local repo_file
|
|
local basename_file
|
|
local home_file
|
|
local status
|
|
|
|
while IFS= read -r file; do
|
|
repo_file="$REPO_DIR/$file"
|
|
basename_file=$(basename "$file")
|
|
home_file="$HOME_DIR/$basename_file"
|
|
|
|
check_file_status "$repo_file" "$home_file"
|
|
status=$?
|
|
|
|
case $status in
|
|
0)
|
|
verbose "✓ identisch: $basename_file"
|
|
((identical++))
|
|
;;
|
|
3)
|
|
warn "≠ geändert im Home: $basename_file"
|
|
if [[ "$VERBOSE" == true ]]; then
|
|
echo " Repo: $(get_checksum "$repo_file")"
|
|
echo " Home: $(get_checksum "$home_file")"
|
|
fi
|
|
((modified++))
|
|
;;
|
|
4)
|
|
warn "✗ nur im Repo: $basename_file (nicht im Home)"
|
|
((repo_only++))
|
|
;;
|
|
esac
|
|
done < <(cd "$REPO_DIR" && git ls-files)
|
|
|
|
echo ""
|
|
success "Status-Zusammenfassung:"
|
|
echo " Identisch: $identical"
|
|
echo " Geändert: $modified"
|
|
echo " Nur im Repo: $repo_only"
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: PULL
|
|
# Home-Dir → Repo (kopiere geänderte Dateien von Home ins Repo)
|
|
################################################################################
|
|
|
|
mode_pull() {
|
|
log "Pull: Synchronisiere geänderte Dateien von Home-Dir ins Repo..."
|
|
|
|
local updated=0
|
|
local skipped=0
|
|
|
|
while IFS= read -r file; do
|
|
local repo_file="$REPO_DIR/$file"
|
|
local basename=$(basename "$file")
|
|
local home_file="$HOME_DIR/$basename"
|
|
|
|
check_file_status "$repo_file" "$home_file"
|
|
local status=$?
|
|
|
|
case $status in
|
|
0)
|
|
verbose "⊙ unverändert: $basename"
|
|
;;
|
|
3)
|
|
warn "← update: $basename (Home → Repo)"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
[[ "$AUTO_BACKUP" == true ]] && backup_file "$repo_file" "$basename (vor Pull)"
|
|
cp "$home_file" "$repo_file"
|
|
((updated++))
|
|
fi
|
|
;;
|
|
4)
|
|
verbose "⊘ ausgelassen: $basename (nicht im Home)"
|
|
((skipped++))
|
|
;;
|
|
esac
|
|
done < <(cd "$REPO_DIR" && git ls-files)
|
|
|
|
success "Pull abgeschlossen: $updated aktualisiert, $skipped ausgelassen"
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: PUSH
|
|
# Repo → Home (kopiere Repo-Dateien ins Home-Dir)
|
|
################################################################################
|
|
|
|
mode_push() {
|
|
log "Push: Synchronisiere Repo-Dateien ins Home-Dir..."
|
|
|
|
local updated=0
|
|
local backed_up=0
|
|
local skipped=0
|
|
|
|
while IFS= read -r file; do
|
|
local repo_file="$REPO_DIR/$file"
|
|
local basename=$(basename "$file")
|
|
local home_file="$HOME_DIR/$basename"
|
|
|
|
check_file_status "$repo_file" "$home_file"
|
|
local status=$?
|
|
|
|
case $status in
|
|
0)
|
|
verbose "⊙ unverändert: $basename"
|
|
;;
|
|
3)
|
|
warn "→ update: $basename (Repo → Home) [lokale Änderung wird überschrieben]"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
if [[ "$AUTO_BACKUP" == true ]]; then
|
|
backup_file "$home_file" "$basename (vor Push)"
|
|
((backed_up++))
|
|
fi
|
|
cp "$repo_file" "$home_file"
|
|
((updated++))
|
|
fi
|
|
;;
|
|
4)
|
|
verbose "✚ neu: $basename (Repo → Home)"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
cp "$repo_file" "$home_file"
|
|
((updated++))
|
|
fi
|
|
;;
|
|
esac
|
|
done < <(cd "$REPO_DIR" && git ls-files)
|
|
|
|
success "Push abgeschlossen: $updated kopiert, $backed_up Backups, $skipped ausgelassen"
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: SYNC (bidirektional mit Konflikt-Handling)
|
|
################################################################################
|
|
|
|
mode_sync() {
|
|
log "Sync: Bidirektionale Synchronisierung..."
|
|
|
|
local updated=0
|
|
local backed_up=0
|
|
local conflicts=0
|
|
|
|
while IFS= read -r file; do
|
|
local repo_file="$REPO_DIR/$file"
|
|
local basename=$(basename "$file")
|
|
local home_file="$HOME_DIR/$basename"
|
|
|
|
check_file_status "$repo_file" "$home_file"
|
|
local status=$?
|
|
|
|
case $status in
|
|
0)
|
|
verbose "⊙ unverändert: $basename"
|
|
;;
|
|
3)
|
|
# Konflikt: beide unterschiedlich
|
|
if [[ "$INTERACTIVE" == true ]]; then
|
|
echo ""
|
|
warn "KONFLIKT: $basename unterscheidet sich"
|
|
echo " Repo Checksum: $(get_checksum "$repo_file")"
|
|
echo " Home Checksum: $(get_checksum "$home_file")"
|
|
echo ""
|
|
echo "Optionen:"
|
|
echo " 1) Nehme Repo-Version (→ Home überschreiben)"
|
|
echo " 2) Nehme Home-Version (← Repo überschreiben)"
|
|
echo " 3) Behalte beide (Home-Version wird gebackupt)"
|
|
echo " 4) Überspringe diese Datei"
|
|
read -p "Wahl [1-4]: " choice
|
|
|
|
case $choice in
|
|
1)
|
|
[[ "$DRY_RUN" == false ]] && {
|
|
backup_file "$home_file" "$basename (Konflikt-Lösung)"
|
|
cp "$repo_file" "$home_file"
|
|
((updated++))
|
|
}
|
|
;;
|
|
2)
|
|
[[ "$DRY_RUN" == false ]] && {
|
|
backup_file "$repo_file" "$basename (Konflikt-Lösung)"
|
|
cp "$home_file" "$repo_file"
|
|
((updated++))
|
|
}
|
|
;;
|
|
3)
|
|
[[ "$DRY_RUN" == false ]] && backup_file "$home_file" "$basename (Konflikt-Aufbewahrung)"
|
|
((backed_up++))
|
|
;;
|
|
4)
|
|
((conflicts++))
|
|
;;
|
|
esac
|
|
else
|
|
# Non-interactive: Home gewinnt (lokal neuere Version wird bevorzugt)
|
|
local repo_mtime=$(stat -f%m "$repo_file" 2>/dev/null || stat -c%Y "$repo_file")
|
|
local home_mtime=$(stat -f%m "$home_file" 2>/dev/null || stat -c%Y "$home_file")
|
|
|
|
if (( home_mtime > repo_mtime )); then
|
|
verbose "← konflikt (Home neuerer): $basename → Repo wird aktualisiert"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
backup_file "$repo_file" "$basename (Konflikt)"
|
|
cp "$home_file" "$repo_file"
|
|
((updated++))
|
|
fi
|
|
else
|
|
verbose "→ konflikt (Repo neuerer): $basename → Home wird aktualisiert"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
backup_file "$home_file" "$basename (Konflikt)"
|
|
cp "$repo_file" "$home_file"
|
|
((updated++))
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
4)
|
|
verbose "✚ neu: $basename (Repo → Home)"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
cp "$repo_file" "$home_file"
|
|
((updated++))
|
|
fi
|
|
;;
|
|
esac
|
|
done < <(cd "$REPO_DIR" && git ls-files)
|
|
|
|
success "Sync abgeschlossen: $updated synchronisiert, $backed_up Backups, $conflicts Konflikte"
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: CLEANUP
|
|
# Entferne alte Backup-Dateien (älter als 30 Tage)
|
|
################################################################################
|
|
|
|
mode_cleanup() {
|
|
log "Cleanup: Entferne alte Backups (älter als 30 Tage)..."
|
|
|
|
if [[ ! -d "$BACKUP_DIR" ]]; then
|
|
success "Kein Backup-Verzeichnis vorhanden"
|
|
return 0
|
|
fi
|
|
|
|
local removed=0
|
|
while IFS= read -r backup_file; do
|
|
rm -f "$backup_file"
|
|
verbose "Gelöscht: $backup_file"
|
|
((removed++))
|
|
done < <(find "$BACKUP_DIR" -type f -mtime +30)
|
|
|
|
success "Cleanup abgeschlossen: $removed alte Backups gelöscht"
|
|
}
|
|
|
|
################################################################################
|
|
# Modus: LIST
|
|
# Liste alle verwalteten Dateien auf
|
|
################################################################################
|
|
|
|
mode_list() {
|
|
log "Liste alle verwalteten Dateien:"
|
|
echo ""
|
|
cd "$REPO_DIR" && git ls-files | while read -r file; do
|
|
echo " • $file"
|
|
done
|
|
}
|
|
|
|
################################################################################
|
|
# Hilfefunktion
|
|
################################################################################
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Verwendung: $0 [MODE] [OPTIONS]
|
|
|
|
MODI:
|
|
status Zeige Unterschiede zwischen Repo und Home-Dir (Standard)
|
|
pull Synchronisiere von Home-Dir ins Repo (Home → Repo)
|
|
push Synchronisiere vom Repo ins Home-Dir (Repo → Home)
|
|
sync Bidirektionale Synchronisierung mit Konflikt-Handling
|
|
cleanup Entferne alte Backups (älter als 30 Tage)
|
|
list Liste alle verwalteten Dateien auf
|
|
|
|
OPTIONEN:
|
|
-d, --dry-run Zeige nur Änderungen, ohne zu modifizieren
|
|
-v, --verbose Verbose Output (DEBUG)
|
|
-i, --interactive Interaktive Konflikte lösen (nur für sync)
|
|
--no-backup Keine automatischen Backups erstellen
|
|
-h, --help Diese Hilfe anzeigen
|
|
|
|
BEISPIELE:
|
|
# Status anzeigen
|
|
$0 status
|
|
|
|
# Von Home ins Repo synchronisieren (Trockentest)
|
|
$0 pull --dry-run
|
|
|
|
# Bidirektional mit Backup synchronisieren
|
|
$0 sync
|
|
|
|
# Interaktiv mit Konflikt-Handling
|
|
$0 sync --interactive
|
|
|
|
STRATEGIE:
|
|
• Content-basiert via md5sum (zuverlässiger als Timestamps)
|
|
• Automatische Backups in .sync-backups/ vor Änderungen
|
|
• Nur Dateien synchronisieren, die in git versioniert sind
|
|
• Logs in .sync-home-content.log
|
|
|
|
EOF
|
|
}
|
|
|
|
################################################################################
|
|
# Hauptprogramm
|
|
################################################################################
|
|
|
|
main() {
|
|
local mode="status"
|
|
|
|
# Parse Argumente
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
status|pull|push|sync|cleanup|list)
|
|
mode="$1"
|
|
shift
|
|
;;
|
|
-d|--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
-i|--interactive)
|
|
INTERACTIVE=true
|
|
shift
|
|
;;
|
|
--no-backup)
|
|
AUTO_BACKUP=false
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
error "Unbekannte Option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validierungen
|
|
if [[ ! -d "$REPO_DIR" ]]; then
|
|
error "Repo-Verzeichnis nicht gefunden: $REPO_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
if ! git -C "$REPO_DIR" rev-parse --git-dir > /dev/null 2>&1; then
|
|
error "$REPO_DIR ist kein Git-Repository"
|
|
exit 1
|
|
fi
|
|
|
|
# Header
|
|
log "═══════════════════════════════════════════════════════════"
|
|
log "Modus: $mode"
|
|
[[ "$DRY_RUN" == true ]] && log "DRY-RUN: Keine Änderungen werden vorgenommen"
|
|
[[ "$INTERACTIVE" == true ]] && log "INTERAKTIV: Bei Konflikten wird nachgefragt"
|
|
log "═══════════════════════════════════════════════════════════"
|
|
|
|
# Führe Modus aus
|
|
case $mode in
|
|
status)
|
|
mode_status
|
|
;;
|
|
pull)
|
|
mode_pull
|
|
;;
|
|
push)
|
|
mode_push
|
|
;;
|
|
sync)
|
|
mode_sync
|
|
;;
|
|
cleanup)
|
|
mode_cleanup
|
|
;;
|
|
list)
|
|
mode_list
|
|
;;
|
|
*)
|
|
error "Unbekannter Modus: $mode"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
log "═══════════════════════════════════════════════════════════"
|
|
[[ "$DRY_RUN" == true ]] && success "DRY-RUN abgeschlossen (keine Änderungen)" || success "Abgeschlossen"
|
|
}
|
|
|
|
# Starte Script
|
|
main "$@"
|
|
|