Shure π
Here are all the configurations and the script:
vdirsyncer.conf:
[general]
status_path = "~/Vdirsyncer-status/"
### KONTAKTE-BLOCK ###
# Konfiguration des lokalen Nextcloud-Kontakte-Verzeichnisses
[storage KontakteNextcloudLokal]
type = "filesystem"
path = "~/Kontakte/Nextcloud/"
fileext = ".vcf"
[storage KontakteGrommunioLokal]
type = "filesystem"
path = "~/Kontakte/Grommunio/"
fileext = ".vcf"
# Konfiguration des Grommunio-Kontakte-Verzeichnisses
[storage KontakteGrommunio]
type = "carddav"
url = "https://mail........."
username = ".........."
password = "..........."
# Konfiguration des Nextcloud-Kontakte-Verzeichnisses
[storage KontakteNextcloud]
type = "carddav"
url = "https://nextcloud......."
username = ".........."
password = "........."
# Kontaktesynchronisation von Grommunio mit lokal
[pair GrommunioKontakte]
a = "KontakteGrommunioLokal"
b = "KontakteGrommunio"
collections = [["KontakteSyncGrommunio", "Kontakte", "Kontakte"]]
# Kontaktesynchronisation von Nextcloud mit lokal
[pair NextcloudKontakte]
a = "KontakteNextcloudLokal"
b = "KontakteNextcloud"
collections = [["KontakteSyncNextcloud", "Kontakte", "kontakte"]]
### KALENDER-BLOCK ###
# Konfiguration des lokalen Kalender-Verzeichnisses
[storage KalenderLokalGrommunio]
type = "filesystem"
path = "~/Kalender/Grommunio/"
fileext = ".ics"
[storage KalenderLokalNextcloud]
type = "filesystem"
path = "~/Kalender/Nextcloud/"
fileext = ".ics"
# Konfiguration des Grommunio-Kalenders
[storage KalenderGrommunio]
type = "caldav"
url = "https://mail......."
username = "........"
password = "........."
# Konfiguration des Nextcloud-Kalenders
[storage KalenderNextcloud]
type = "caldav"
url = "https://nextcloud......."
username = "......."
password = ".........."
# Synchronisation GrommunioKalender und lokal
[pair GrommunioKalender]
a = "KalenderLokalGrommunio"
b = "KalenderGrommunio"
collections = [["GrommunioSync", "Kalender", "Kalender"]]
# Synchronisation NextcloudKalender und lokal
[pair NextcloudKalender]
a = "KalenderLokalNextcloud"
b = "KalenderNextcloud"
collections = [["NextcloudSync", "Kalender", "kalender"]]
### AUFGABEN-BLOCK ###
# Aufgaben lokal
[storage AufgabenLokal]
type = "filesystem"
path = "~/Aufgaben/"
fileext = ".ics"
# Aufgaben Nextcloud
[storage AufgabenNextcloud]
type = "caldav"
url = "https://nextcloud........"
username = "..........."
password = ".........."
# Aufgaben Grommunio
[storage AufgabenGrommunio]
type = "caldav"
url = "https://mail........."
username = ".........."
password = "..........."
# Paarung GrommunioMitLokal
[pair AufgabenGrommunioLokal]
a = "AufgabenLokal"
b = "AufgabenGrommunio"
collections = [["AufgabenSyncLokalGrommunio", "Aufgaben", "Aufgaben"]]
# Paarung NextcloudMitLokal
[pair AufgabenNextcloudLokal]
a = "AufgabenLokal"
b = "AufgabenNextcloud"
collections = [["AufgabenSyncLokalNextcloud", "Aufgaben", "aufgaben"]]
The script for swapping the ENCODING=BASE64-encoding to ENCODING=B:
#!/usr/bin/env bash
# =============================================================================
# GrommunioNextcloudCarddavPhotoSync.sh
#
# Synchronisiert zwei CardDAV-Server ΓΌber lokale AdressbΓΌcher,
# wobei ENCODING=BASE64 β ENCODING=B automatisch konvertiert wird.
#
# Architektur:
#
# [Server A] βvdirsyncerβ [DIR_BASE64] βdieses Skriptβ [DIR_B] βvdirsyncerβ [Server B]
# (ENCODING=BASE64 bleibt) (Konvertierung) (ENCODING=B bleibt)
#
# Voraussetzungen: vdirsyncer, md5sum (coreutils)
# =============================================================================
set -euo pipefail
# βββ Konfiguration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
VDIRSYNCER_CONFIG="${HOME}/.config/vdirsyncer/config"
DIR_BASE64="${HOME}/Kontakte/Grommunio/Kontakte" # Lokales Adressbuch fΓΌr Server A (ENCODING=BASE64)
DIR_B="${HOME}/Kontakte/Nextcloud/Kontakte" # Lokales Adressbuch fΓΌr Server B (ENCODING=B)
PAIR_BASE64="GrommunioKontakte/KontakteSyncGrommunio" # Name des vdirsyncer-Paars fΓΌr Server A
PAIR_B="NextcloudKontakte/KontakteSyncNextcloud" # Name des vdirsyncer-Paars fΓΌr Server B
STATUS_DIR="${HOME}/Skripte/GrommunioNextcloudCarddavPhotoHashes" # Gespeicherte Hashes fΓΌr Γnderungserkennung
CONFLICT_DIR="${HOME}/Kontakte/Konflikte" # Konflikt-Backups
LOG_FILE="${HOME}/Skripte/GrommunioNextcloudCarddavPhoto.log"
# KonfliktlΓΆsung bei echten Inhaltskonflikten: "a_wins" | "b_wins" | "newest"
CONFLICT_RESOLUTION="newest"
# βββ Initialisierung ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
mkdir -p "$DIR_BASE64" "$DIR_B" "$STATUS_DIR" "$CONFLICT_DIR" "$(dirname "$LOG_FILE")"
log() {
local level="$1"; shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE"
}
# βββ vCard-Hilfsfunktionen ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Normalisierter Hash: ENCODING=BASE64 und ENCODING=B werden gleichbehandelt.
# Dadurch erkennt das Skript Encoding-Unterschiede NICHT als inhaltliche Γnderung.
normalized_hash() {
sed 's/\bENCODING=BASE64\b/ENCODING=B/gI' "$1" \
| sed 's/\r//' \
| md5sum | cut -d' ' -f1
}
# ENCODING=BASE64 β ENCODING=B
convert_to_b() {
local src="$1" dst="$2"
sed 's/\bENCODING=BASE64\b/ENCODING=B/gI' "$src" > "$dst"
}
# ENCODING=B β ENCODING=BASE64 (nur isoliertes =B, nicht z.B. =JPEG)
convert_to_base64() {
local src="$1" dst="$2"
sed 's/\(ENCODING=\)B\b/\1BASE64/gI' "$src" > "$dst"
}
# Gespeicherten Hash lesen (leerer String = Datei war noch nie bekannt / gelΓΆscht)
read_hash() {
local hashfile="${STATUS_DIR}/${1}"
[[ -f "$hashfile" ]] && cat "$hashfile" || echo "DELETED"
}
write_hash() {
echo "$2" > "${STATUS_DIR}/${1}"
}
# βββ Schritt 1: Vdirsyncer mit beiden Servern synchronisieren βββββββββββββββββ
log "INFO" "ββββββββββββββββββββββββββββββββββββββββ"
log "INFO" "Starte CardDAV-Bridge-Sync"
log "INFO" "Sync: Server A (ENCODING=BASE64) β ${DIR_BASE64}"
if ! vdirsyncer --config "$VDIRSYNCER_CONFIG" sync "$PAIR_BASE64" 2>&1 | tee -a "$LOG_FILE"; then
log "WARN" "vdirsyncer $PAIR_BASE64 mit Fehlern beendet β fahre fort"
fi
log "INFO" "Sync: Server B (ENCODING=B) β ${DIR_B}"
if ! vdirsyncer --config "$VDIRSYNCER_CONFIG" sync "$PAIR_B" 2>&1 | tee -a "$LOG_FILE"; then
log "WARN" "vdirsyncer $PAIR_B mit Fehlern beendet β fahre fort"
fi
# βββ Schritt 2: Alle .vcf-Dateinamen aus beiden Verzeichnissen sammeln ββββββββ
declare -A all_files
for f in "$DIR_BASE64"/*.vcf "$DIR_B"/*.vcf; do
#[[ -f "$f" ]] && all_files["$(basename "$f")"]=1
if [[ -f "$f" ]]; then
all_files["$(basename "$f")"]=1
fi
done
if [[ ${#all_files[@]} -eq 0 ]]; then
log "INFO" "Keine .vcf-Dateien gefunden β nichts zu tun."
exit 0
fi
log "INFO" "Verarbeite ${#all_files[@]} Kontakt-Datei(en)..."
count_a_to_b=0
count_b_to_a=0
count_deleted=0
count_conflicts=0
count_unchanged=0
# βββ Schritt 3: Bidirektionaler lokaler Abgleich ββββββββββββββββββββββββββββββ
for filename in "${!all_files[@]}"; do
file_a="${DIR_BASE64}/${filename}"
file_b="${DIR_B}/${filename}"
key="${filename%.vcf}" # SchlΓΌssel fΓΌr Status-Dateien (ohne .vcf)
# Aktuelle normalisierte Hashes
hash_curr_a=$([[ -f "$file_a" ]] && normalized_hash "$file_a" || echo "DELETED")
hash_curr_b=$([[ -f "$file_b" ]] && normalized_hash "$file_b" || echo "DELETED")
# Zuletzt bekannte Hashes
hash_prev_a=$(read_hash "a_${key}")
hash_prev_b=$(read_hash "b_${key}")
# Γnderungsflags
changed_a=$([[ "$hash_curr_a" != "$hash_prev_a" ]] && echo true || echo false)
changed_b=$([[ "$hash_curr_b" != "$hash_prev_b" ]] && echo true || echo false)
# ββ Entscheidungsmatrix ββββββββββββββββββββββββββββββββββββββββββββββββββββ
if ! $changed_a && ! $changed_b; then
# Keine Seite geΓ€ndert
# ((count_unchanged++))
count_unchanged=$(( count_unchanged + 1 ))
continue
elif [[ "$hash_curr_a" == "$hash_curr_b" ]] && $changed_a && $changed_b; then
# Beide geΓ€ndert, aber Inhalt identisch (z.B. erster Lauf mit bereits
# befΓΌllten Verzeichnissen) β kein Konflikt, nur Hashes speichern
log "INFO" " Inhalt identisch (Encoding normalisiert): ${filename}"
write_hash "a_${key}" "$hash_curr_a"
write_hash "b_${key}" "$hash_curr_b"
# ((count_unchanged++))
count_unchanged=$(( count_unchanged + 1 ))
elif $changed_a && ! $changed_b; then
# Nur A geΓ€ndert β nach B ΓΌbertragen
if [[ "$hash_curr_a" == "DELETED" ]]; then
log "INFO" " LΓΆschung A β B: ${filename}"
rm -f "$file_b"
# ((count_deleted++))
count_deleted=$((count_deleted + 1))
else
log "INFO" " Neu/GeΓ€ndert A β B: ${filename}"
convert_to_b "$file_a" "$file_b"
# ((count_a_to_b++))
count_a_to_b=$((count_a_to_b + 1))
fi
new_hash_b=$([[ -f "$file_b" ]] && normalized_hash "$file_b" || echo "DELETED")
write_hash "a_${key}" "$hash_curr_a"
write_hash "b_${key}" "$new_hash_b"
elif $changed_b && ! $changed_a; then
# Nur B geΓ€ndert β nach A ΓΌbertragen
if [[ "$hash_curr_b" == "DELETED" ]]; then
log "INFO" " LΓΆschung B β A: ${filename}"
rm -f "$file_a"
# ((count_deleted++))
count_deleted=$((count_deleted + 1))
else
log "INFO" " Neu/GeΓ€ndert B β A: ${filename}"
convert_to_base64 "$file_b" "$file_a"
# ((count_b_to_a++))
count_b_to_a=$((count_b_to_a + 1))
fi
new_hash_a=$([[ -f "$file_a" ]] && normalized_hash "$file_a" || echo "DELETED")
write_hash "a_${key}" "$new_hash_a"
write_hash "b_${key}" "$hash_curr_b"
elif $changed_a && $changed_b; then
# Echter Inhaltskonflikt β Backups anlegen
log "WARN" " KONFLIKT erkannt: ${filename}"
[[ -f "$file_a" ]] && cp "$file_a" "${CONFLICT_DIR}/${key}.$(date '+%Y%m%d_%H%M%S').a.vcf"
[[ -f "$file_b" ]] && cp "$file_b" "${CONFLICT_DIR}/${key}.$(date '+%Y%m%d_%H%M%S').b.vcf"
# ((count_conflicts++))
count_conflicts=$((count_conflicts + 1))
# KonfliktlΓΆsung anwenden
case "$CONFLICT_RESOLUTION" in
a_wins)
log "WARN" " Konflikt β A gewinnt: ${filename}"
if [[ -f "$file_a" ]]; then
convert_to_b "$file_a" "$file_b"
else
rm -f "$file_b"
fi
;;
b_wins)
log "WARN" " Konflikt β B gewinnt: ${filename}"
if [[ -f "$file_b" ]]; then
convert_to_base64 "$file_b" "$file_a"
else
rm -f "$file_a"
fi
;;
newest)
# Neuere Datei (mtime) gewinnt
if [[ -f "$file_a" && -f "$file_b" ]]; then
if [[ "$file_a" -nt "$file_b" ]]; then
log "WARN" " Konflikt β A (neuer) gewinnt: ${filename}"
convert_to_b "$file_a" "$file_b"
else
log "WARN" " Konflikt β B (neuer) gewinnt: ${filename}"
convert_to_base64 "$file_b" "$file_a"
fi
fi
;;
esac
new_hash_a=$([[ -f "$file_a" ]] && normalized_hash "$file_a" || echo "DELETED")
new_hash_b=$([[ -f "$file_b" ]] && normalized_hash "$file_b" || echo "DELETED")
write_hash "a_${key}" "$new_hash_a"
write_hash "b_${key}" "$new_hash_b"
fi
done
log "INFO" "Lokaler Abgleich:"
log "INFO" " A β B: ${count_a_to_b}"
log "INFO" " B β A: ${count_b_to_a}"
log "INFO" " GelΓΆscht: ${count_deleted}"
log "INFO" " UnverΓ€ndert: ${count_unchanged}"
log "INFO" " Konflikte: ${count_conflicts}"
# βββ Schritt 4: Lokale Γnderungen zurΓΌck zu den Servern pushen ββββββββββββββββ
if (( count_b_to_a > 0 || count_deleted > 0 )); then
log "INFO" "Push Γnderungen β Server A"
vdirsyncer --config "$VDIRSYNCER_CONFIG" sync "$PAIR_BASE64" 2>&1 | tee -a "$LOG_FILE" || \
log "WARN" "Push zu Server A mit Fehlern beendet"
fi
if (( count_a_to_b > 0 || count_deleted > 0 )); then
log "INFO" "Push Γnderungen β Server B"
vdirsyncer --config "$VDIRSYNCER_CONFIG" sync "$PAIR_B" 2>&1 | tee -a "$LOG_FILE" || \
log "WARN" "Push zu Server B mit Fehlern beendet"
fi
# βββ Abschlussbericht βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (( count_conflicts > 0 )); then
log "WARN" "ACHTUNG: ${count_conflicts} Konflikt(e) β bitte prΓΌfen: ${CONFLICT_DIR}"
fi
log "INFO" "Sync abgeschlossen."
Any ideas why uploading contact pictures from Nextcloud to Grommunio won't work?