Compare commits

..

8 Commits

Author SHA1 Message Date
duffyduck 31aef01ccd Release v0.0.0.27 2026-06-08 12:41:47 +02:00
duffyduck 6c9721acc6 Support UNC paths for shared lock directory; create it if missing
Das gemeinsame Verzeichnis darf ein UNC-Pfad sein (\\server\freigabe\...),
kein Netzlaufwerksbuchstabe noetig. Statt nur Directory.Exists zu pruefen
(und sonst still ohne Sperre zu syncen) wird das Verzeichnis bei Bedarf
angelegt; nur bei echtem Zugriffsfehler wird ohne Sperre fortgefahren.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:40:31 +02:00
duffyduck 212ced4c81 Add cross-client sync lock via shared directory
Verhindert, dass mehrere Arbeitsplaetze gleichzeitig dasselbe Starface-
Adressbuch synchronisieren (Dubletten/Lost-Updates bei echter Ueberlappung).

- Neues optionales Setting "Gemeinsames Verzeichnis" (UserSettings.SharedDirectory)
  in der Einstellungen-Maske inkl. Ordner-Browser.
- SyncLock: atomare Lock-Datei (FileMode.CreateNew) im gemeinsamen Verzeichnis,
  waehrend des Syncs offen gehalten -> bei Absturz gibt das OS das Handle frei
  und ein anderer Client uebernimmt die verwaiste Datei (Stale-Erkennung 15 Min,
  Loeschen scheitert am offenen Handle eines lebenden Halters).
- MainForm wartet vor dem Sync bis zu 2 Min auf eine freie Sperre, sonst wird
  der Lauf uebersprungen. Ohne/bei nicht erreichbarem Verzeichnis laeuft der
  Sync ohne diese Sperre weiter (lokaler Interlocked-Schutz bleibt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:38:45 +02:00
duffyduck d3fa452504 Add field-level 3-way merge for bidirectional conflicts
Bisher wurde bei einem Konflikt (beide Seiten geaendert) der ganze Datensatz
ueberschrieben - eine gleichzeitige Aenderung an einem anderen Feld ging
verloren (z.B. A aendert Telefon in Outlook, B aendert Mail in Starface ->
eine Aenderung weg).

Jetzt:
- Mapping speichert je Seite einen Snapshot des letzten Sync-Stands
  (LastOutlook/LastStarface), zusaetzlich zu den Hashes.
- Bei beidseitiger Aenderung im Both-Modus wird feldweise gemergt
  (ContactMerger): unterschiedliche Felder bleiben beide erhalten, nur bei
  echtem Konflikt am selben Feld gewinnt Outlook.
- Echte Feld-Konflikte landen in SyncResult.Conflicts und werden im MainForm
  per Tray-Meldung angezeigt.
- Snapshots werden in allen Baseline-Punkten gesetzt (Phase 1-3) und fuer
  aeltere Mappings beim naechsten unveraenderten Sync nachgetragen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:35:10 +02:00
duffyduck bee17a7fc6 Make local sync re-entrancy guard atomic
Der Schutz gegen gleichzeitige Syncs (manuell vs. Auto-Sync-Timer) war ein
nicht-atomares pruefen-und-setzen auf einem volatile bool. Zwischen Pruefung
und Setzen konnten ein UI-Klick und der Timer-Thread beide durchrutschen und
zwei Syncs gleichzeitig starten.

Jetzt per Interlocked.CompareExchange atomar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:14:23 +02:00
duffyduck 1987e25c37 Release v0.0.0.26 2026-06-08 12:04:57 +02:00
duffyduck b5ad59ff9d One-way sync modes now do a full replace of the target
Outlook->Starface macht das Starface-Adressbuch zur exakten Kopie von
Outlook: Kontakte, die nur in Starface existieren, werden geloescht.
Starface->Outlook entsprechend umgekehrt (Phase 4).

Sicherheit:
- Loeschphase laeuft nur bei vollstaendig geladener Liste (unvollstaendige
  Ladevorgaenge brechen schon vorher ab).
- Ist die Quelle komplett leer (z.B. falscher Ordner), wird die Loeschphase
  uebersprungen statt die Zielseite zu leeren.

UI: Profil-Editor zeigt jetzt unter der Sync-Richtung einen Warnhinweis, der
das jeweilige Verhalten erklaert. README/CHANGELOG aktualisiert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:04:17 +02:00
duffyduck 1e9ff63833 Propagate deletions in bidirectional sync via baseline tombstone
Im Both-Modus wurde ein auf einer Seite geloeschter Kontakt bisher auf der
anderen Seite einfach wieder angelegt, statt die Loeschung zu spiegeln.

Jetzt wird anhand der gespeicherten Baseline (LastOutlookHash /
LastStarfaceHash) entschieden:
- Gegenseite seit letztem Sync unveraendert -> es war eine Loeschung ->
  auf der anderen Seite ebenfalls loeschen.
- Gegenseite wurde geaendert -> Bearbeitung gewinnt -> neu anlegen
  (kein Datenverlust).

In den Ein-Richtungs-Modi bleibt die Quelle fuehrend: eine Loeschung im
Ziel wird aus der Quelle wiederhergestellt (StarfaceToOutlook legt einen
in Outlook geloeschten Kontakt jetzt ebenfalls wieder an statt ein totes
Mapping zu behalten).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:59:12 +02:00
13 changed files with 681 additions and 48 deletions
+36
View File
@@ -36,8 +36,44 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
wurde. Jede Seite hat jetzt eine eigene Baseline (`LastOutlookHash` / wurde. Jede Seite hat jetzt eine eigene Baseline (`LastOutlookHash` /
`LastStarfaceHash`); nur tatsaechlich geaenderte Kontakte werden geschrieben. `LastStarfaceHash`); nur tatsaechlich geaenderte Kontakte werden geschrieben.
### Hinzugefuegt
- **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst
sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein
Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere
Clients warten, bis sie frei ist (bis 2 Min, sonst wird der Lauf uebersprungen).
Stuerzt ein Client ab, uebernimmt ein anderer die verwaiste Sperre. Ist kein
Verzeichnis konfiguriert oder nicht erreichbar, wird ohne diese Sperre
weitergearbeitet (der lokale Schutz auf dem PC bleibt).
- **Feldweises 3-Wege-Merge bei Konflikten (bidirektional).** Wenn derselbe
Kontakt zwischen zwei Syncs auf beiden Seiten geaendert wurde, bleiben jetzt
Aenderungen an *unterschiedlichen* Feldern beide erhalten (z.B. einer aendert
die Telefonnummer in Outlook, ein anderer die E-Mail in Starface). Nur wenn
DASSELBE Feld auf beiden Seiten unterschiedlich geaendert wurde, greift die
Vorrang-Regel (Outlook gewinnt). Dafuer wird im Mapping zusaetzlich ein
Snapshot des letzten Sync-Stands je Seite gespeichert. Solche echten
Feld-Konflikte werden dem Benutzer per Tray-Meldung angezeigt.
### Geaendert ### Geaendert
- **Doppelte Syncs verhindert (lokal).** Der Schutz gegen gleichzeitig laufende
Syncs (manuell + Auto-Sync-Timer) ist jetzt atomar (`Interlocked`) statt eines
nicht-atomaren `volatile bool`, bei dem beide in einem Zeitfenster
durchrutschen konnten.
- **Ein-Richtungs-Modi sind jetzt echtes "Ersetzen".** Outlook->Starface macht
das Starface-Adressbuch zu einer exakten Kopie von Outlook: Kontakte, die nur
in Starface existieren (kein Pendant in Outlook), werden geloescht.
Starface->Outlook entsprechend umgekehrt. Schutz: Ist die Quelle komplett
leer (z.B. falscher Ordner gewaehlt), wird die Loeschphase uebersprungen
statt die Zielseite zu leeren. Mass-Loeschungen finden nur bei vollstaendig
geladener Liste statt (unvollstaendige Ladevorgaenge brechen vorher ab).
- **Bidirektionale Loeschungen** werden jetzt erkannt. Wird ein Kontakt auf
einer Seite geloescht und ist die andere Seite seit dem letzten Sync
unveraendert (Abgleich ueber die gespeicherte Baseline), wird die Loeschung
auf die andere Seite gespiegelt statt den Kontakt wieder anzulegen. Wurde
die andere Seite zwischenzeitlich bearbeitet, gewinnt die Bearbeitung und der
Kontakt wird neu angelegt (kein Datenverlust). In den Ein-Richtungs-Modi
bleibt die jeweilige Quelle fuehrend (Loeschung im Ziel wird wiederhergestellt).
- Starface-Kontaktdetails werden beim Laden parallel abgerufen (8 gleichzeitig) - Starface-Kontaktdetails werden beim Laden parallel abgerufen (8 gleichzeitig)
statt einzeln nacheinander deutlich schneller bei grossen Adressbuechern. statt einzeln nacheinander deutlich schneller bei grossen Adressbuechern.
- `UpdateContact` (Outlook) und `UpdateContactAsync` (Starface) geben jetzt den - `UpdateContact` (Outlook) und `UpdateContactAsync` (Starface) geben jetzt den
+4 -1
View File
@@ -8,7 +8,10 @@ Windows-Anwendung zur bidirektionalen Synchronisation von Kontakten zwischen Mic
- **Profil-System** zum Verwalten mehrerer Sync-Konfigurationen (verschiedene Adressbuecher oder Anlagen) - **Profil-System** zum Verwalten mehrerer Sync-Konfigurationen (verschiedene Adressbuecher oder Anlagen)
- **Starface-Adressbuecher**: Zentrales Adressbuch, persoenliches Adressbuch und Tag-basierte Adressbuecher - **Starface-Adressbuecher**: Zentrales Adressbuch, persoenliches Adressbuch und Tag-basierte Adressbuecher
- **Outlook-Kontaktordner**: Frei waehlbarer Kontaktordner als Sync-Ziel - **Outlook-Kontaktordner**: Frei waehlbarer Kontaktordner als Sync-Ziel
- **Sync-Richtung** konfigurierbar: Outlook -> Starface, Starface -> Outlook oder bidirektional - **Sync-Richtung** konfigurierbar:
- *Bidirektional*: Aenderungen werden in beide Richtungen abgeglichen (inkl. Loeschungen)
- *Outlook -> Starface*: Das Starface-Adressbuch wird zur exakten Kopie von Outlook (nur in Starface vorhandene Kontakte werden geloescht)
- *Starface -> Outlook*: Der Outlook-Ordner wird zur exakten Kopie von Starface (nur in Outlook vorhandene Kontakte werden geloescht)
- **Intelligentes Matching**: Kontakte werden anhand von E-Mail-Adresse oder Name abgeglichen - **Intelligentes Matching**: Kontakte werden anhand von E-Mail-Adresse oder Name abgeglichen
- **Aenderungserkennung**: Nur geaenderte Kontakte werden uebertragen (Hash-basiert) - **Aenderungserkennung**: Nur geaenderte Kontakte werden uebertragen (Hash-basiert)
- **Auto-Sync**: Optionaler automatischer Sync in konfigurierbarem Intervall - **Auto-Sync**: Optionaler automatischer Sync in konfigurierbarem Intervall
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php) ; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync" #define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.25" #define MyAppVersion "0.0.0.27"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation" #define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de" #define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe" #define MyAppExeName "StarfaceOutlookSync.exe"
@@ -53,6 +53,13 @@ namespace StarfaceOutlookSync.Models
public string LastOutlookHash { get; set; } = ""; public string LastOutlookHash { get; set; } = "";
public string LastStarfaceHash { get; set; } = ""; public string LastStarfaceHash { get; set; } = "";
// Snapshot des zuletzt synchronisierten Stands je Seite. Ermoeglicht ein
// feldweises 3-Wege-Merge bei Konflikten (welche Seite hat welches Feld
// geaendert), statt den ganzen Datensatz zu ueberschreiben. Null bei
// Alt-Mappings -> dann Fallback auf ganz-ueberschreiben.
public UnifiedContact LastOutlook { get; set; }
public UnifiedContact LastStarface { get; set; }
// Alt-Feld (vor v0.0.0.24). Nur noch fuer Migration bestehender Mappings. // Alt-Feld (vor v0.0.0.24). Nur noch fuer Migration bestehender Mappings.
public string LastSyncHash { get; set; } = ""; public string LastSyncHash { get; set; } = "";
} }
@@ -65,5 +72,23 @@ namespace StarfaceOutlookSync.Models
public int Updated { get; set; } public int Updated { get; set; }
public int Errors { get; set; } public int Errors { get; set; }
public System.Collections.Generic.List<string> ErrorMessages { get; set; } = new System.Collections.Generic.List<string>(); public System.Collections.Generic.List<string> ErrorMessages { get; set; } = new System.Collections.Generic.List<string>();
// Echte Feld-Konflikte (dasselbe Feld auf beiden Seiten geaendert), die
// ueber die Vorrang-Regel aufgeloest wurden. Fuer Benutzer-Hinweise.
public System.Collections.Generic.List<FieldConflict> Conflicts { get; set; } = new System.Collections.Generic.List<FieldConflict>();
}
public class FieldConflict
{
public string StarfaceId { get; set; } = "";
public string ContactName { get; set; } = "";
public string Field { get; set; } = "";
public string OutlookValue { get; set; } = "";
public string StarfaceValue { get; set; } = "";
public string Winner { get; set; } = ""; // "Outlook" oder "Starface"
public override string ToString() =>
$"{ContactName}: Feld '{Field}' auf beiden Seiten geaendert " +
$"(Outlook: '{OutlookValue}' / Starface: '{StarfaceValue}') -> {Winner} uebernommen";
} }
} }
@@ -11,6 +11,11 @@ namespace StarfaceOutlookSync.Models
public bool SyncOnStart { get; set; } = false; public bool SyncOnStart { get; set; } = false;
public bool AutoAcceptOutlookPrompt { get; set; } = false; public bool AutoAcceptOutlookPrompt { get; set; } = false;
// Gemeinsames Verzeichnis (Netzlaufwerk/UNC) fuer die clientuebergreifende
// Sync-Sperre. Leer = keine Sperre (nur lokaler Schutz). Verhindert, dass
// mehrere Arbeitsplaetze gleichzeitig dasselbe Adressbuch synchronisieren.
public string SharedDirectory { get; set; } = "";
private static readonly string SettingsFile = Path.Combine( private static readonly string SettingsFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StarfaceOutlookSync", "settings.json"); "StarfaceOutlookSync", "settings.json");
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StarfaceOutlookSync.Models;
namespace StarfaceOutlookSync.Services
{
/// <summary>
/// Feldweises 3-Wege-Merge zweier Kontaktstaende gegen ihre Baseline.
/// Erlaubt es, dass zwei Stellen unterschiedliche Felder desselben Kontakts
/// aendern, ohne dass eine Aenderung verloren geht. Nur wenn DASSELBE Feld
/// auf beiden Seiten geaendert wurde, greift die Vorrang-Regel.
/// </summary>
public static class ContactMerger
{
private class FieldDef
{
public string Label;
public Func<UnifiedContact, string> Get;
public Action<UnifiedContact, string> Set;
public bool IsPhone;
}
private static readonly FieldDef[] Fields = new[]
{
new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v },
new FieldDef { Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v },
new FieldDef { Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v },
new FieldDef { Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v },
new FieldDef { Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v },
new FieldDef { Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v },
new FieldDef { Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true },
new FieldDef { Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true },
new FieldDef { Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true },
new FieldDef { Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true },
new FieldDef { Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v },
new FieldDef { Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v },
new FieldDef { Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v },
new FieldDef { Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v },
new FieldDef { Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v },
new FieldDef { Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v },
new FieldDef { Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v },
new FieldDef { Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v },
new FieldDef { Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v },
new FieldDef { Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
};
/// <summary>
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
/// </summary>
public static (UnifiedContact merged, List<FieldConflict> conflicts) Merge(
UnifiedContact baseOutlook, UnifiedContact baseStarface,
UnifiedContact outlook, UnifiedContact starface,
bool outlookWins)
{
var merged = new UnifiedContact
{
OutlookEntryId = outlook.OutlookEntryId,
StarfaceId = starface.StarfaceId
};
var conflicts = new List<FieldConflict>();
foreach (var f in Fields)
{
string olv = f.Get(outlook) ?? "";
string sfv = f.Get(starface) ?? "";
string bol = f.Get(baseOutlook) ?? "";
string bsf = f.Get(baseStarface) ?? "";
bool olChanged = !Equal(olv, bol, f.IsPhone);
bool sfChanged = !Equal(sfv, bsf, f.IsPhone);
string chosen;
if (olChanged && !sfChanged)
{
chosen = olv;
}
else if (sfChanged && !olChanged)
{
chosen = sfv;
}
else if (olChanged && sfChanged)
{
if (Equal(olv, sfv, f.IsPhone))
{
// Beide auf denselben Wert geaendert -> kein echter Konflikt.
chosen = olv;
}
else
{
chosen = outlookWins ? olv : sfv;
conflicts.Add(new FieldConflict
{
StarfaceId = starface.StarfaceId,
ContactName = outlook.DisplayName,
Field = f.Label,
OutlookValue = olv,
StarfaceValue = sfv,
Winner = outlookWins ? "Outlook" : "Starface"
});
}
}
else
{
// Keine Seite hat dieses Feld geaendert -> Outlook-Wert behalten.
chosen = olv;
}
f.Set(merged, chosen);
}
return (merged, conflicts);
}
private static bool Equal(string a, string b, bool isPhone)
{
if (isPhone)
return NormalizePhone(a) == NormalizePhone(b);
return string.Equals(a ?? "", b ?? "", StringComparison.OrdinalIgnoreCase);
}
private static string NormalizePhone(string phone)
{
if (string.IsNullOrEmpty(phone)) return "";
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
}
}
}
+208 -32
View File
@@ -15,6 +15,20 @@ namespace StarfaceOutlookSync.Services
private void Log(string message) => OnProgress?.Invoke(message); private void Log(string message) => OnProgress?.Invoke(message);
/// <summary>
/// Setzt die Baseline eines Mappings auf den uebergebenen Stand beider
/// Seiten (Snapshot + Hash). Der Snapshot wird fuer das Feld-Merge bei
/// kuenftigen Konflikten gebraucht.
/// </summary>
private static void SetBaseline(SyncMapping m, UnifiedContact outlook, UnifiedContact starface)
{
m.LastOutlook = outlook;
m.LastStarface = starface;
m.LastOutlookHash = outlook?.GetHash() ?? "";
m.LastStarfaceHash = starface?.GetHash() ?? "";
m.LastSyncHash = "";
}
/// <summary> /// <summary>
/// Findet einen passenden Kontakt in der Kandidatenliste. /// Findet einen passenden Kontakt in der Kandidatenliste.
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen /// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
@@ -163,19 +177,46 @@ namespace StarfaceOutlookSync.Services
continue; continue;
} }
// Wirklich geloescht -> in Starface auch loeschen // Wirklich in Outlook geloescht.
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface) if (profile.SyncDirection == SyncDirection.OutlookToStarface)
{ {
// Outlook ist fuehrend -> Loeschung nach Starface spiegeln.
if (await starface.DeleteContactAsync(mapping.StarfaceId)) if (await starface.DeleteContactAsync(mapping.StarfaceId))
{ {
result.Updated++; result.Updated++;
Log($" Geloescht (OL->SF): {sc.DisplayName}"); Log($" Geloescht (OL->SF): {sc.DisplayName}");
} }
continue;
} }
else
if (profile.SyncDirection == SyncDirection.Both)
{ {
newMappings.Add(mapping); // Bidirektional: anhand der Baseline pruefen, ob die
// Starface-Seite seit dem letzten Sync unveraendert ist.
bool sfUnchanged = !string.IsNullOrEmpty(mapping.LastStarfaceHash)
&& sc.GetHash() == mapping.LastStarfaceHash;
if (sfUnchanged)
{
// Unveraendert + in Outlook geloescht -> Loeschung gilt
// -> auch aus Starface entfernen.
if (await starface.DeleteContactAsync(mapping.StarfaceId))
{
result.Updated++;
Log($" Geloescht (OL->SF): {sc.DisplayName}");
}
continue;
}
// In Outlook geloescht, aber in Starface geaendert ->
// Bearbeitung gewinnt, in Outlook neu anlegen (Phase 3).
Log($" In Outlook geloescht, in Starface geaendert -> neu anlegen: {sc.DisplayName}");
processedStarfaceIds.Remove(sc.StarfaceId);
continue;
} }
// StarfaceToOutlook: Starface ist alleinige Quelle -> in Outlook
// neu anlegen (Loeschung im Ziel zaehlt nicht).
Log($" Outlook-Kontakt geloescht, wird neu angelegt: {sc.DisplayName}");
processedStarfaceIds.Remove(sc.StarfaceId);
continue; continue;
} }
@@ -197,21 +238,10 @@ namespace StarfaceOutlookSync.Services
continue; continue;
} }
// Wirklich geloescht. // Wirklich in Starface geloescht.
if (profile.SyncDirection == SyncDirection.Both if (profile.SyncDirection == SyncDirection.StarfaceToOutlook)
|| profile.SyncDirection == SyncDirection.OutlookToStarface)
{ {
// Outlook ist (mit-)fuehrend -> Kontakt in Starface neu // Starface ist fuehrend -> Loeschung nach Outlook spiegeln.
// anlegen. Mapping verwerfen und oc wieder freigeben,
// damit Phase 2 ihn anlegt (inkl. Duplikat-Pruefung).
Log($" Starface-Kontakt geloescht, wird neu angelegt: {oc.DisplayName}");
processedOutlookIds.Remove(oc.OutlookEntryId);
continue;
}
else
{
// StarfaceToOutlook: Starface ist fuehrend, Loeschung
// nach Outlook spiegeln.
if (_outlookService.DeleteContact(oc.OutlookEntryId)) if (_outlookService.DeleteContact(oc.OutlookEntryId))
{ {
result.Updated++; result.Updated++;
@@ -219,6 +249,36 @@ namespace StarfaceOutlookSync.Services
} }
continue; continue;
} }
if (profile.SyncDirection == SyncDirection.Both)
{
// Bidirektional: anhand der Baseline entscheiden, ob der
// Outlook-Kontakt seit dem letzten Sync unveraendert ist.
bool olUnchanged = !string.IsNullOrEmpty(mapping.LastOutlookHash)
&& oc.GetHash() == mapping.LastOutlookHash;
if (olUnchanged)
{
// Unveraendert + in Starface geloescht -> Loeschung gilt
// -> aus Outlook entfernen.
if (_outlookService.DeleteContact(oc.OutlookEntryId))
{
result.Updated++;
Log($" Geloescht (SF->OL): {oc.DisplayName}");
}
continue;
}
// In Starface geloescht, aber in Outlook geaendert ->
// Bearbeitung gewinnt, in Starface neu anlegen.
Log($" In Starface geloescht, in Outlook geaendert -> neu anlegen: {oc.DisplayName}");
processedOutlookIds.Remove(oc.OutlookEntryId);
continue;
}
// OutlookToStarface: Outlook ist alleinige Quelle -> Kontakt
// in Starface neu anlegen (Loeschung im Ziel zaehlt nicht).
Log($" Starface-Kontakt geloescht, wird neu angelegt: {oc.DisplayName}");
processedOutlookIds.Remove(oc.OutlookEntryId);
continue;
} }
if (oc != null && sc != null) if (oc != null && sc != null)
@@ -236,9 +296,7 @@ namespace StarfaceOutlookSync.Services
if (string.IsNullOrEmpty(mapping.LastOutlookHash) && if (string.IsNullOrEmpty(mapping.LastOutlookHash) &&
string.IsNullOrEmpty(mapping.LastStarfaceHash)) string.IsNullOrEmpty(mapping.LastStarfaceHash))
{ {
mapping.LastOutlookHash = olHash; SetBaseline(mapping, oc, sc);
mapping.LastStarfaceHash = sfHash;
mapping.LastSyncHash = "";
newMappings.Add(mapping); newMappings.Add(mapping);
continue; continue;
} }
@@ -246,14 +304,23 @@ namespace StarfaceOutlookSync.Services
bool olChanged = olHash != mapping.LastOutlookHash; bool olChanged = olHash != mapping.LastOutlookHash;
bool sfChanged = sfHash != mapping.LastStarfaceHash; bool sfChanged = sfHash != mapping.LastStarfaceHash;
if (!olChanged && !sfChanged)
{
// Unveraendert. Snapshots aelterer Mappings (nur Hash)
// nachtragen, damit kuenftige Konflikte gemergt werden koennen.
if (mapping.LastOutlook == null || mapping.LastStarface == null)
SetBaseline(mapping, oc, sc);
newMappings.Add(mapping);
continue;
}
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)) if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
{ {
// Outlook hat sich geaendert -> Starface updaten // Outlook hat sich geaendert -> Starface updaten
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook); var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
if (updated != null) if (updated != null)
{ {
mapping.LastOutlookHash = olHash; SetBaseline(mapping, oc, updated);
mapping.LastStarfaceHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Aktualisiert (OL->SF): {oc.DisplayName}"); Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
} }
@@ -264,22 +331,54 @@ namespace StarfaceOutlookSync.Services
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc); var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
if (updated != null) if (updated != null)
{ {
mapping.LastStarfaceHash = sfHash; SetBaseline(mapping, updated, sc);
mapping.LastOutlookHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Aktualisiert (SF->OL): {sc.DisplayName}"); Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
} }
} }
else if (olChanged && sfChanged) else if (olChanged && sfChanged)
{ {
// Beide geaendert -> Konflikt, Outlook bevorzugt // Beide Seiten geaendert.
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook) if (profile.SyncDirection == SyncDirection.Both
&& mapping.LastOutlook != null && mapping.LastStarface != null)
{ {
// Feldweises 3-Wege-Merge: unterschiedliche Felder
// bleiben beide erhalten; nur bei gleichem Feld auf
// beiden Seiten gewinnt Outlook.
var (merged, conflicts) = ContactMerger.Merge(
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.StarfaceAddressBook);
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
if (updatedSf != null || updatedOl != null)
{
if (updatedOl != null)
{
mapping.LastOutlook = updatedOl;
mapping.LastOutlookHash = updatedOl.GetHash();
}
if (updatedSf != null)
{
mapping.LastStarface = updatedSf;
mapping.LastStarfaceHash = updatedSf.GetHash();
}
mapping.LastSyncHash = "";
result.Updated++;
foreach (var cf in conflicts) result.Conflicts.Add(cf);
Log(conflicts.Count > 0
? $" Beidseitig geaendert, zusammengefuehrt ({conflicts.Count} Feld-Konflikt(e), Outlook gewinnt): {oc.DisplayName}"
: $" Beidseitig geaendert, zusammengefuehrt: {oc.DisplayName}");
}
}
else if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
{
// Fallback ohne Snapshot bzw. OutlookToStarface:
// Outlook gewinnt komplett.
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook); var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
if (updated != null) if (updated != null)
{ {
mapping.LastOutlookHash = olHash; SetBaseline(mapping, oc, updated);
mapping.LastStarfaceHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Konflikt (OL gewinnt): {oc.DisplayName}"); Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
} }
@@ -289,14 +388,12 @@ namespace StarfaceOutlookSync.Services
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc); var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
if (updated != null) if (updated != null)
{ {
mapping.LastStarfaceHash = sfHash; SetBaseline(mapping, updated, sc);
mapping.LastOutlookHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Konflikt (SF gewinnt): {sc.DisplayName}"); Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
} }
} }
} }
// Beide unveraendert -> nichts tun
} }
newMappings.Add(mapping); newMappings.Add(mapping);
@@ -336,6 +433,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId, OutlookEntryId = oc.OutlookEntryId,
StarfaceId = match.StarfaceId, StarfaceId = match.StarfaceId,
LastOutlook = oc,
LastStarface = updated,
LastOutlookHash = oc.GetHash(), LastOutlookHash = oc.GetHash(),
LastStarfaceHash = updated.GetHash() LastStarfaceHash = updated.GetHash()
}); });
@@ -357,6 +456,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId, OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId, StarfaceId = created.StarfaceId,
LastOutlook = oc,
LastStarface = created,
LastOutlookHash = oc.GetHash(), LastOutlookHash = oc.GetHash(),
LastStarfaceHash = created.GetHash() LastStarfaceHash = created.GetHash()
}); });
@@ -412,6 +513,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = match.OutlookEntryId, OutlookEntryId = match.OutlookEntryId,
StarfaceId = sc.StarfaceId, StarfaceId = sc.StarfaceId,
LastStarface = sc,
LastOutlook = updated,
LastStarfaceHash = sc.GetHash(), LastStarfaceHash = sc.GetHash(),
LastOutlookHash = updated.GetHash() LastOutlookHash = updated.GetHash()
}); });
@@ -432,6 +535,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = created.OutlookEntryId, OutlookEntryId = created.OutlookEntryId,
StarfaceId = sc.StarfaceId, StarfaceId = sc.StarfaceId,
LastStarface = sc,
LastOutlook = created,
LastStarfaceHash = sc.GetHash(), LastStarfaceHash = sc.GetHash(),
LastOutlookHash = created.GetHash() LastOutlookHash = created.GetHash()
}); });
@@ -448,6 +553,77 @@ namespace StarfaceOutlookSync.Services
} }
} }
// ============================================
// Phase 4: Ersetzen-Modus - Zielseite an Quelle angleichen
// ============================================
// In den Ein-Richtungs-Modi soll die Zielseite eine exakte Kopie
// der Quelle werden. Kontakte, die nur auf der Zielseite existieren
// (kein Mapping, kein Treffer in der Quelle), werden geloescht.
// Sicher, weil unvollstaendige Ladevorgaenge vorher abbrechen.
if (profile.SyncDirection == SyncDirection.OutlookToStarface)
{
// Schutz gegen versehentliches Leerraeumen (z.B. falscher Ordner).
if (outlookContacts.Count == 0)
{
Log("Ersetzen-Modus uebersprungen: Outlook-Ordner ist leer (Schutz vor versehentlichem Leeren).");
}
else
{
var leftover = starfaceContacts
.Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId))
.ToList();
if (leftover.Count > 0)
Log($"Ersetzen-Modus: entferne {leftover.Count} Kontakt(e) aus Starface, die nicht in Outlook existieren");
foreach (var sc in leftover)
{
try
{
if (await starface.DeleteContactAsync(sc.StarfaceId))
{
result.Updated++;
Log($" Geloescht (nur in Starface): {sc.DisplayName}");
}
}
catch (Exception ex)
{
result.Errors++;
result.ErrorMessages.Add($"Loeschen SF {sc.DisplayName}: {ex.Message}");
}
}
}
}
else if (profile.SyncDirection == SyncDirection.StarfaceToOutlook)
{
if (starfaceContacts.Count == 0)
{
Log("Ersetzen-Modus uebersprungen: Starface-Adressbuch ist leer (Schutz vor versehentlichem Leeren).");
}
else
{
var leftover = outlookContacts
.Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId))
.ToList();
if (leftover.Count > 0)
Log($"Ersetzen-Modus: entferne {leftover.Count} Kontakt(e) aus Outlook, die nicht in Starface existieren");
foreach (var oc in leftover)
{
try
{
if (_outlookService.DeleteContact(oc.OutlookEntryId))
{
result.Updated++;
Log($" Geloescht (nur in Outlook): {oc.DisplayName}");
}
}
catch (Exception ex)
{
result.Errors++;
result.ErrorMessages.Add($"Loeschen OL {oc.DisplayName}: {ex.Message}");
}
}
}
}
// Mappings speichern // Mappings speichern
_profileManager.SaveMappings(profile.Id, newMappings); _profileManager.SaveMappings(profile.Id, newMappings);
_profileManager.UpdateLastSync(profile.Id); _profileManager.UpdateLastSync(profile.Id);
@@ -0,0 +1,120 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace StarfaceOutlookSync.Services
{
/// <summary>
/// Clientuebergreifende Sperre ueber eine Lock-Datei in einem gemeinsamen
/// Verzeichnis (Netzlaufwerk/Share). Verhindert, dass mehrere Arbeitsplaetze
/// gleichzeitig dasselbe Starface-Adressbuch synchronisieren.
///
/// Atomar ueber FileMode.CreateNew: das Anlegen schlaegt fehl, wenn die Datei
/// schon existiert. Die Datei wird waehrend des Syncs offen gehalten
/// (FileShare.None) - stuerzt ein Client ab, gibt das Betriebssystem das
/// Handle frei und ein anderer Client kann die (dann verwaiste) Datei
/// uebernehmen. Eine reine Zeitstempel-Pruefung allein waere nicht sicher.
/// </summary>
public sealed class SyncLock : IDisposable
{
public const string LockFileName = "starface-sync.lock";
// Aelter als das -> als verwaist behandeln und Uebernahme VERSUCHEN.
// Lebt der Eigentuemer noch, schlaegt das Loeschen am offenen Handle fehl,
// die Sperre bleibt also korrekt bestehen.
private static readonly TimeSpan StaleAfter = TimeSpan.FromMinutes(15);
private FileStream _stream;
private readonly string _path;
private SyncLock(string path, FileStream stream)
{
_path = path;
_stream = stream;
}
/// <summary>Sperre, die nichts haelt (wenn kein gemeinsames Verzeichnis konfiguriert/erreichbar ist).</summary>
public static SyncLock NoOp() => new SyncLock(null, null);
/// <summary>
/// Versucht die Sperre zu holen. Gibt null zurueck, wenn gerade ein
/// anderer (lebender) Client synct. heldBy enthaelt - sofern lesbar -
/// Infos zum aktuellen Halter.
/// </summary>
public static SyncLock TryAcquire(string dir, out string heldBy)
{
heldBy = null;
var path = Path.Combine(dir, LockFileName);
var lockObj = TryCreate(path);
if (lockObj != null) return lockObj;
// Existiert bereits. Halter ermitteln und ggf. verwaiste Datei uebernehmen.
heldBy = ReadOwner(path);
if (IsStale(path))
{
try { File.Delete(path); } // scheitert, wenn der Halter noch lebt (Handle offen)
catch { return null; }
return TryCreate(path);
}
return null;
}
private static SyncLock TryCreate(string path)
{
try
{
var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
var info = $"{Environment.MachineName}|{Environment.UserName}|{DateTime.UtcNow:o}|pid{GetPid()}";
var bytes = Encoding.UTF8.GetBytes(info);
fs.Write(bytes, 0, bytes.Length);
fs.Flush();
return new SyncLock(path, fs);
}
catch (IOException) { return null; } // existiert schon
catch (UnauthorizedAccessException) { return null; }
}
private static int GetPid()
{
try { return Process.GetCurrentProcess().Id; } catch { return 0; }
}
private static string ReadOwner(string path)
{
try
{
// Eigene Freigabe, falls der Halter die Datei nur zum Schreiben offen hat.
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var sr = new StreamReader(fs))
{
var raw = sr.ReadToEnd();
var parts = raw.Split('|');
return parts.Length >= 1 ? parts[0] : null;
}
}
catch { return null; }
}
private static bool IsStale(string path)
{
try
{
var age = DateTime.UtcNow - File.GetLastWriteTimeUtc(path);
return age > StaleAfter;
}
catch { return false; }
}
public void Dispose()
{
try { _stream?.Dispose(); } catch { }
_stream = null;
if (!string.IsNullOrEmpty(_path))
{
try { File.Delete(_path); } catch { }
}
}
}
}
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle> <AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company> <Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product> <Product>Starface Outlook Sync</Product>
<Version>0.0.0.25</Version> <Version>0.0.0.27</Version>
<AssemblyVersion>0.0.0.25</AssemblyVersion> <AssemblyVersion>0.0.0.27</AssemblyVersion>
<FileVersion>0.0.0.25</FileVersion> <FileVersion>0.0.0.27</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description> <Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright> <Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.0.25", Text = "Version 0.0.0.27",
Left = 0, Top = 56, Width = 340, Height = 20, Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray ForeColor = Color.Gray
+71 -4
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using System.Windows.Forms; using System.Windows.Forms;
@@ -22,7 +23,9 @@ namespace StarfaceOutlookSync.UI
private StatusStrip _statusBar; private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel; private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer; private Timer _autoSyncTimer;
private volatile bool _syncRunning = false; // 0 = frei, 1 = Sync laeuft. Per Interlocked atomar geschaltet, damit
// ein manueller Sync und der Auto-Sync-Timer nicht gleichzeitig starten.
private int _syncRunning = 0;
public MainForm() public MainForm()
{ {
@@ -337,15 +340,25 @@ namespace StarfaceOutlookSync.UI
private async Task RunSync(SyncProfile profile) private async Task RunSync(SyncProfile profile)
{ {
if (_syncRunning) // Atomar pruefen-und-setzen: verhindert, dass manueller Sync und
// Auto-Sync-Timer gleichzeitig denselben/einen Sync starten.
if (Interlocked.CompareExchange(ref _syncRunning, 1, 0) != 0)
{ {
SetStatus("Sync laeuft bereits, bitte warten..."); SetStatus("Sync laeuft bereits, bitte warten...");
return; return;
} }
_syncRunning = true; SyncLock crossLock = null;
try try
{ {
// Clientuebergreifende Sperre (falls gemeinsames Verzeichnis konfiguriert).
crossLock = await AcquireCrossClientLock();
if (crossLock == null)
{
SetStatus("Anderer Arbeitsplatz synchronisiert gerade - uebersprungen.");
return;
}
SetStatus($"Synchronisiere '{profile.Name}'..."); SetStatus($"Synchronisiere '{profile.Name}'...");
_trayIcon.ShowBalloonTip(2000, "Starface Sync", _trayIcon.ShowBalloonTip(2000, "Starface Sync",
$"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info); $"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info);
@@ -354,10 +367,22 @@ namespace StarfaceOutlookSync.UI
var msg = $"{profile.Name}: {result.Created} erstellt, {result.Updated} aktualisiert"; var msg = $"{profile.Name}: {result.Created} erstellt, {result.Updated} aktualisiert";
if (result.Errors > 0) msg += $", {result.Errors} Fehler"; if (result.Errors > 0) msg += $", {result.Errors} Fehler";
if (result.Conflicts.Count > 0) msg += $", {result.Conflicts.Count} Konflikt(e)";
_trayIcon.ShowBalloonTip(3000, "Starface Sync", msg, _trayIcon.ShowBalloonTip(3000, "Starface Sync", msg,
result.Errors > 0 ? ToolTipIcon.Warning : ToolTipIcon.Info); result.Errors > 0 ? ToolTipIcon.Warning : ToolTipIcon.Info);
// Echte Feld-Konflikte gesondert melden, damit der Benutzer weiss,
// dass ein Wert ueberschrieben wurde.
if (result.Conflicts.Count > 0)
{
var detail = string.Join("\n", result.Conflicts.Take(5).Select(c => c.ToString()));
if (result.Conflicts.Count > 5)
detail += $"\n... und {result.Conflicts.Count - 5} weitere";
_trayIcon.ShowBalloonTip(10000,
$"Konflikt bei {result.Conflicts.Count} Kontakt(en)", detail, ToolTipIcon.Warning);
}
SetStatus(msg); SetStatus(msg);
} }
catch (Exception ex) catch (Exception ex)
@@ -368,7 +393,49 @@ namespace StarfaceOutlookSync.UI
} }
finally finally
{ {
_syncRunning = false; crossLock?.Dispose();
Interlocked.Exchange(ref _syncRunning, 0);
}
}
/// <summary>
/// Holt die clientuebergreifende Lock-Datei aus dem konfigurierten
/// gemeinsamen Verzeichnis. Wartet bis zu 2 Minuten, falls ein anderer
/// Arbeitsplatz gerade synct. Gibt null zurueck, wenn die Sperre nicht
/// erlangt werden konnte (-> diesen Lauf ueberspringen).
/// Ist kein Verzeichnis konfiguriert oder es ist nicht erreichbar, wird
/// ohne clientuebergreifende Sperre fortgefahren (lokaler Schutz bleibt).
/// </summary>
private async Task<SyncLock> AcquireCrossClientLock()
{
var dir = UserSettings.Load().SharedDirectory;
if (string.IsNullOrWhiteSpace(dir))
return SyncLock.NoOp();
try
{
// Legt das Verzeichnis bei Bedarf an (idempotent). Funktioniert mit
// UNC-Pfaden (\\server\freigabe\...) - kein Netzlaufwerk noetig.
System.IO.Directory.CreateDirectory(dir);
}
catch
{
SetStatus("Gemeinsames Verzeichnis nicht erreichbar - synce ohne Sperre.");
return SyncLock.NoOp();
}
var deadline = DateTime.UtcNow.AddMinutes(2);
int attempt = 0;
while (true)
{
var l = SyncLock.TryAcquire(dir, out var heldBy);
if (l != null) return l;
if (DateTime.UtcNow >= deadline) return null;
attempt++;
SetStatus($"Warte auf anderen Arbeitsplatz ({heldBy ?? "unbekannt"})... ({attempt})");
await Task.Delay(3000);
} }
} }
@@ -22,6 +22,7 @@ namespace StarfaceOutlookSync.UI
private NumericUpDown _numAutoSync; private NumericUpDown _numAutoSync;
private Button _btnTest, _btnLoadBooks, _btnSave, _btnCancel; private Button _btnTest, _btnLoadBooks, _btnSave, _btnCancel;
private Label _lblTestResult; private Label _lblTestResult;
private Label _lblDirectionHint;
private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>(); private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>();
private List<string> _outlookFolderPaths = new List<string>(); private List<string> _outlookFolderPaths = new List<string>();
@@ -101,7 +102,18 @@ namespace StarfaceOutlookSync.UI
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList }; _cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
_cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Starface", "Starface -> Outlook" }); _cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Starface", "Starface -> Outlook" });
_cmbDirection.SelectedIndex = 0; _cmbDirection.SelectedIndex = 0;
panel.Controls.Add(_cmbDirection); y += 32; panel.Controls.Add(_cmbDirection); y += 28;
_lblDirectionHint = new Label
{
Left = 12, Top = y, Width = 360, Height = 32,
ForeColor = Color.FromArgb(150, 80, 0),
Font = new Font("Segoe UI", 8.25f)
};
_cmbDirection.SelectedIndexChanged += (s, e) => UpdateDirectionHint();
panel.Controls.Add(_lblDirectionHint);
UpdateDirectionHint();
y += 36;
panel.Controls.Add(MakeLabel("Auto-Sync Intervall (Minuten, 0 = manuell):", 12, y)); y += 22; panel.Controls.Add(MakeLabel("Auto-Sync Intervall (Minuten, 0 = manuell):", 12, y)); y += 22;
_numAutoSync = new NumericUpDown { Left = 12, Top = y, Width = 80, Minimum = 0, Maximum = 1440, Value = 0 }; _numAutoSync = new NumericUpDown { Left = 12, Top = y, Width = 80, Minimum = 0, Maximum = 1440, Value = 0 };
@@ -122,6 +134,22 @@ namespace StarfaceOutlookSync.UI
CancelButton = _btnCancel; CancelButton = _btnCancel;
} }
private void UpdateDirectionHint()
{
switch (_cmbDirection.SelectedIndex)
{
case 1: // Outlook -> Starface
_lblDirectionHint.Text = "Achtung: Das Starface-Adressbuch wird zur exakten Kopie von Outlook.\nKontakte, die nur in Starface existieren, werden geloescht.";
break;
case 2: // Starface -> Outlook
_lblDirectionHint.Text = "Achtung: Der Outlook-Ordner wird zur exakten Kopie von Starface.\nKontakte, die nur in Outlook existieren, werden geloescht.";
break;
default: // Bidirektional
_lblDirectionHint.Text = "Aenderungen werden in beide Richtungen abgeglichen.";
break;
}
}
private Label MakeLabel(string text, int x, int y) private Label MakeLabel(string text, int x, int y)
{ {
return new Label { Text = text, Left = x, Top = y, AutoSize = true }; return new Label { Text = text, Left = x, Top = y, AutoSize = true };
+49 -5
View File
@@ -7,6 +7,8 @@ namespace StarfaceOutlookSync.UI
public class SettingsForm : Form public class SettingsForm : Form
{ {
private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook; private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook;
private TextBox _txtSharedDir;
private Button _btnBrowseShared;
private Button _btnSave, _btnCancel; private Button _btnSave, _btnCancel;
private readonly UserSettings _settings; private readonly UserSettings _settings;
@@ -54,35 +56,77 @@ namespace StarfaceOutlookSync.UI
var lblHint = new Label var lblHint = new Label
{ {
Text = hintText, Text = hintText,
Left = 38, Top = 102, Width = 310, Height = 36, Left = 38, Top = 102, Width = 320, Height = 36,
ForeColor = UserSettings.IsOutlookSecurityLockedByPolicy() ? Color.OrangeRed : Color.Gray, ForeColor = UserSettings.IsOutlookSecurityLockedByPolicy() ? Color.OrangeRed : Color.Gray,
Font = new Font("Segoe UI", 8) Font = new Font("Segoe UI", 8)
}; };
var lblShared = new Label
{
Text = "Gemeinsames Verzeichnis fuer Sync-Sperre (Mehrplatz, optional):",
Left = 20, Top = 150, AutoSize = true
};
_txtSharedDir = new TextBox
{
Left = 20, Top = 172, Width = 250,
Text = _settings.SharedDirectory
};
_btnBrowseShared = new Button
{
Text = "...", Left = 274, Top = 171, Width = 36, Height = 24
};
_btnBrowseShared.Click += (s, e) => BrowseSharedDir();
var lblSharedHint = new Label
{
Text = "Netzlaufwerk/UNC, das alle Arbeitsplaetze erreichen. Leer = keine\n" +
"clientuebergreifende Sperre (nur Schutz auf diesem PC).",
Left = 20, Top = 198, Width = 330, Height = 32,
ForeColor = Color.Gray, Font = new Font("Segoe UI", 8)
};
_btnSave = new Button _btnSave = new Button
{ {
Text = "Speichern", Left = 95, Top = 170, Width = 85, Height = 28, Text = "Speichern", Left = 95, Top = 240, Width = 85, Height = 28,
DialogResult = DialogResult.None DialogResult = DialogResult.None
}; };
_btnSave.Click += (s, e) => Save(); _btnSave.Click += (s, e) => Save();
_btnCancel = new Button _btnCancel = new Button
{ {
Text = "Abbrechen", Left = 189, Top = 170, Width = 85, Height = 28, Text = "Abbrechen", Left = 189, Top = 240, Width = 85, Height = 28,
DialogResult = DialogResult.Cancel DialogResult = DialogResult.Cancel
}; };
Size = new Size(380, 260); Size = new Size(380, 330);
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint, _btnSave, _btnCancel }); Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint,
lblShared, _txtSharedDir, _btnBrowseShared, lblSharedHint, _btnSave, _btnCancel });
AcceptButton = _btnSave; AcceptButton = _btnSave;
CancelButton = _btnCancel; CancelButton = _btnCancel;
} }
private void BrowseSharedDir()
{
using (var dlg = new FolderBrowserDialog())
{
dlg.Description = "Gemeinsames Verzeichnis fuer die Sync-Sperre waehlen";
if (!string.IsNullOrWhiteSpace(_txtSharedDir.Text))
{
try { dlg.SelectedPath = _txtSharedDir.Text; } catch { }
}
if (dlg.ShowDialog(this) == DialogResult.OK)
_txtSharedDir.Text = dlg.SelectedPath;
}
}
private void Save() private void Save()
{ {
_settings.StartMinimized = _chkStartMinimized.Checked; _settings.StartMinimized = _chkStartMinimized.Checked;
_settings.SyncOnStart = _chkSyncOnStart.Checked; _settings.SyncOnStart = _chkSyncOnStart.Checked;
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked; _settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
_settings.SharedDirectory = _txtSharedDir.Text.Trim();
_settings.Save(); _settings.Save();
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();