#!/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 "$@"