Die Extension h2_contentsync ermöglicht es, beliebige* Datensätze von einem Master-TYPO3 auf ein Slave-TYPO3 zu übertragen und synchron zu halten.
h2_contentsync wurde zwar für den Einsatz im SBV-Intranet entwickelt, ist aber möglichst allgemeingültig umgesetzt, enthält keine besonderen Abhängigkeiten und sollte ohne größere Probleme in anderen Seiten verwendet werden können.
Allgemein
Der Master stellt die Datenquelle dar. Der Slave ruft die Daten des Masters ab.
Im Master- und im Slave-TYPO3 ist jeweils eine Instanz von h2_contentsync installiert. Die Konfiguration geschieht über Contentsync-Master- bzw. Contentsync-Slave-Datensätze.
- Auf dem Master gibt es einen Slave-Datensatz:
- Adresse des Slaves und wie er sich identifizieren muss
- Eigene Adresse dem Slave gegenüber
- Freigegebene Tabellen für den Slave, und jeweils welche PIDs
- Weitere Infos zu freigegebenen Daten, z.B. Spalten mit Dateireferenzen, Spalten mit RTE-Feldern, zugehörige MM-Tabellen
- Auf dem Slave gibt es einen Master-Datensatz:
- Adresse des Masters, und wie sich der Slave dem Master gegenüber identifiziert
- Eigene Adresse dem Master gegenüber
- Tabellen, die vom Master abgeholt werden sollen
- Diverse Einstellungen zur weiteren Verarbeitung der vom Master gelieferten Daten
Der Großteil dieser Konfiguration wird über eine TypoScript-basierte Konfiguration in den Contentsync-Master- bzw. Contentsync-Slave-Datensätzen erledigt.
Weil Daten niemals 1:1 zwischen beiden TYPO3s synchronisiert werden können (andere PIDs, UIDs schon besetzt,...), gibt es auf dem Slave umfangreiche Funktionen, die gelieferten Daten zu bearbeiten. Der Slave merkt sich, welchen Datensatz vom Master (Quelltabelle, -UID und -Zeitstempel) er wohin gespeichert hat (Zieltabelle und -UID), als Syncstatus-Datensatz. Mithilfe der Syncstatus-Datensätze können Dupletten bei der Synchronisierung vermieden werden, und außerdem kann der Slave dadurch Referenzen auf synchronisierte Datensätze auflösen.
Allgemeiner Ablauf
- Auf dem Slave: Ein Scheduler-Task startet den SyncCommandController. Für jede Master-Konfiguration wird eine Instanz von H2\H2contentsync\Service\Sync\Master erstellt und dort startSync() aufgerufen.
- H2\H2contentsync\Service\Sync\Master::startSync():
- Alle Tabellen ermitteln, die vom Master abgerufen werden sollen
- Syncstatus-Datensätze dieser Tabellen ermitteln: Welche Datensätze des Masters sind im Slave bereits bekannt, und welchen Zeitstempel haben sie?
- Anfrage an den Master, action = sync. Slave meldet den aus dem Syncstatus ermittelten Zustand an den Master.
- - MASTER -
- Einstiegspunkt auf dem Master ist \H2\H2contentsync\Controller\SyncController::syncAction()
- Passende Slave-Konfiguration wird herausgesucht, und H2\H2contentsync\Service\Sync\Slave::process() aufgerufen
- Anhand der Syncstatus-Daten ermitteln, welche Datensätze neu, aktualisiert oder gelöscht sind
- Neu: Auf dem Master vorhanden, aber nicht unter den Syncstatus-Daten
- Aktualisiert: Auf dem Master vorhanden, unter den Syncstatus-Daten, aber lokale Timestamp != Syncstatus-Timestamp
- Gelöscht: Unter den Syncstatus-Daten, aber nicht auf dem Master vorhanden (bzw. dort mit deleted = 1 markiert)
- Alle nötigen Daten für neue und aktualisierte Datensätze und die UIDs der zu löschenden Datensätze an den Slave zurückmelden
- - Ende MASTER -
- Vom Master gelieferte Rückmeldung auswerten:
- Für jeden neuen Datensatz:
- Referenzierte Dateien abrufen (_processFiles()) -> ggf. weitere Master-Requests
- FAL-Referenzen für RTE-Felder abrufen (_processFal()) -> ggf. weitere Master-Requests
- Link-Felder verarbeiten (_processLinkFields())
- Daten anhand des konfigurierten Schemas verändern (\H2\H2contentsync\Service\Sync\Mapper::map()) - z.B. in einen anderen Datentyp überführen
- Felder generieren, z.B. für sorting (_generateFields())
- neuen Datensatz in der Zieltabelle ablegen
- Syncstatus-Datensatz für den neuen Datensatz anlegen
- MM-Relationen einfügen (_updateMM())
- FAL-Referenzen für neuen Datensatz abrufen (_processFal()) -> ggf. weitere Master-Requests
- Das Aktualisieren der Datensätze funktioniert fast gleich - nur statt einen neuen Datensatz anzulegen, wird die UID des vorhandenen, zu aktualisierenden Datensatzes aus den Syncstatus-Daten abgerufen. Der Syncstatus-Datensatz erhält die neue, vom Master gelieferte Timestamp.
- Für jeden zu löschenden Datensatz:
- Lokale UID aus den Syncstatus-Daten ermitteln
- Datensatz löschen
- mögliche sys_file-FAL-Referenzen entfernen
- Den zugehörigen Syncstatus-Datensatz löschen
- Mögliche zugehörige MM-Relationen löschen
Erklärung einzelner Funktionen
_processFiles
Ruft Dateien vom Master ab, die in einer Spalte im Datensatz als Name stehen ("Oldschool-Dateireferenzen", z.B. in tt_news). Die Werte in der Spalte dürfen kommasepariert sein, in der Art Datei1.jpg,datei2.png,File3.gif.
Die Funktion benötigt die Info, welche Spalten Dateireferenzen enthalten.
- Für jede Spalte mit Dateireferenz(en) im Inhalt:
- Wert am Komma splitten, um die einzelnen Referenzen zu erhalten.Für jede Referenz:
- Master aufrufen: action = file
- Auf dem Master verarbeitet \H2\H2contentsync\Service\Sync\Slave::_processFile() den Aufruf, und gibt nach einer Sicherheitsprüfung den Dateiinhalt als Response zurück
- Auf dem Slave: Die Datei wird im konfigurierten Zielordner abgelegt. Falls bereits eine gleichnamige Datei vorhanden ist, wird ein neuer Name vergeben
- Wert am Komma splitten, um die einzelnen Referenzen zu erhalten.Für jede Referenz:
_processFal
Ruft Dateien aus dem Master ab, die per FAL an den Quelldatensatz gebunden sind. Damit die FAL-Daten geprüft werden, muss bei Master und Slave jeweils evaluateFal = 1 konfiguriert sein.
Es gibt zwei Modi für _processFal(), je nachdem, ob FAL-Referenzen in RTE- oder "reguläre" FAL-Referenzen ausgewertet werden. Die Funktion wird erst im RTE- und später im regulären Modus aufgerufen.
Wenn im RTE FAL-Referenzen enthalten sind, wird direkt die UID des sys_file-Datensatzes verwendet. Bei "regulären" Referenzen enthält der Datensatz selbst keinen Hinweis auf eine FAL-Referenz. Diese geschieht dort über einen Eintrag in der Tabelle sys_file_reference
Im RTE-Modus wird nur die Datei vom Master abgerufen. Weil es für Referenzen in RTE-Feldern keine sys_file_reference-Einträge gibt, muss auch die UID des lokalen Eltern-Datensatzes nicht bekannt sein. Daher geschieht der Aufruf im FAL-Modus früh in der Verarbeitung des jeweiligen Datensatzes. Im regulären Modus werden ebenfalls die sys_file_reference-Einträge aktualisiert. Dazu muss die UID des lokalen Datensatzes bekannt sein. Aus diesem Grund geschieht dieser Aufruf erst, nachdem die UID des lokalen Eltern-Datensatzes bekannt ist.
Der Master liefert in den Quelldaten bereits die allgemeinen Infos über die FAL-Referenzen, sofern vorhanden (z.B. SHA1-Fingerprint des Dateiinhalts). Für jeden FAL-Eintrag laut Master:
- Wenn kein RTE-Modus: FAL-Referenzen auf den lokalen Eltern-Datensatz abräumen
- Prüfen: Ist lokal bereits eine gleiche Datei vorhanden?
- Wenn nein:
- Master aufrufen, action = fal.
- Auf dem Master verarbeitet \H2\H2contentsync\Service\Sync\Slave::_processFal() den Aufruf, und gibt nach einer Sicherheitsprüfung den Dateiinhalt als Response zurück
- Der Slave speichert die Datei. Dabei wird versucht, die Datei in einem ähnlichen Zielordner abzulegen wie auf dem Master.
- Der Slave legt einen neuen sys_file-Eintrag an
- Wenn kein RTE-Modus: FAL-Referenzen auf den lokalen Eltern-Datensatz neu aufbauen
Aktuell ist fest verdrahtet, welcher Storage im Slave für die FAL-Operationen verwendet wird (UID 1, fileadmin-Ordner). Das geschieht in \H2\H2contentsync\Service\Sync\AbstractSync::_getFileStorage().
_processLinkFields
Verarbeitet RTE-Felder und andere Spalten, die (Typo-)Links enthalten können. Diese Spalten müssen in der Konfiguration linkFields im Master-Datensatz auf dem Slave angegeben werden. Für jedes Link-Feld wird außerdem ein Präfix definiert, welches in bestimmten Fällen zum Einsatz kommt (s.u.).
Die Funktion bearbeitet zwei unterschiedliche Szenarien:
- Eine Spalte hat einen numerischen Wert. Die Funktion geht davon aus, dass der Datensatz einen Link auf eine Master-interne Seite enthält
- Als schnelle Lösung wurde eine Präfix-Funktion umgesetzt. Der neue Wert der Spalte ist {Präfix}{alter Wert}.
- Beispiel: Wert laut Master: 124. Präfix laut Konfiguration: http://url.zum.master/?id=. Neuer Wert nach der Verarbeitung: http://url.zum.master/?id=124
- Achtung: Bei der Konfiguration muss sichergestellt werden, dass die Spalte wirklich ein Link-Feld ist, und dass die Spalte in der Zieltabelle String-Werte aufnehmen kann.
- Die Spalte hat einen Text-Inhalt. Die Funktion sucht nach <link>-Tags, wie sie in RTE-Feldern vorkommen können. Es werden zwei unterschiedliche Typen von Link-Tags verarbeitet:
- Link-Tags mit numerischem Parameter werden mit dem konfigurierten Präfix in explizite externe Links umgewandelt
- Link-Tags mit file:-Referenzen auf FAL-Einträge werden in auf die lokalen FAL-Datensätze im Slave umgeändert (Ergänzende Funktion zu _processFal())
Der Mapper
Die Klasse \H2\H2contentsync\Service\Sync\Mapper kümmert sich um Spalten, die andere Datensätze referenzieren, und die Überführung der Quelldaten in das auf dem Slave benötige Zielformat.
ignore-Felder
Der Mapper versucht, alle Master-Quelldaten im neuen Slave-Datensatz unterzubringen. Das ist aber bei einigen Spalten nicht sinnvoll, z.B. sollte die UID nie direkt übernommen werden. Diese zu ignorierenden Spalten können über ignore angegeben werden. Über ignore_on_new bzw. ignore_on_update können speziell für neue oder zu aktualisierende Beiträge weitere Ignore-Felder angegeben werden.
Referenzen
Mit Referenzen sind Spalten gemeint, die auf andere Datensätze zeigen, z.B. referenziert fe_users in der Spalte usergroup die fremde Tabelle fe_groups. Solche Referenzen können natürlich auch in die eigene Tabelle zeigen. Als Relation gilt 1:1 oder 1:n mit CSV-artiger Angabe der referenzierten UIDs (z.B. bei fe_users.usergroup). Wenn bei der Synchronisierung die referenzierten Datensätze ebenfalls übertragen werden, müssen die Referenzen im Slave aktualisiert werden, weil dort die UIDs der referenzierten Datensätze anders sind.
Über references wird angegeben, welche Spalte welche Tabelle referenziert. Über reference_defaults können Fallback-Werte angegeben werden, wenn keine lokale Referenz ermittelt werden kann.
Referenzen aktualisieren: Alle Referenz-Felder durchgehen:
- Mittels Syncstatus-Suche versuchen, die lokale UID des referenzierten Datensatzes zu ermitteln. Das klappt natürlich nur, wenn dieser Datensatz vorher synchronisiert wurde
- Falls keine UID ermittelt werden konnte, den konfigurierten Default-Wert verwenden
Mapping
Über das Mapping können Daten nachbearbeitet oder ergänzt werden, z.B. um Spalten umzubenennen, Werte zu erzeugen oder Werte fall-basiert zu setzen. Das geschieht über die Konfiguration map. Für das Mapping wird TypoScript verwendet. Die TS-Objekte können über .field auf den Original-Datensatz zugreifen (auch ignorierte Spalten sind verfügbar). Das Mapping selbst geschieht in \H2\H2contentsync\Service\Typoscript, das auf Core-Funktionen zugreift.
Zieltabelle
Wenn Daten im Slave anders als im Master gespeichert werden sollen, kann über local_table eine lokale Tabelle angegeben werden. Dadurch ist es z.B. möglich, tt_news-Quelldaten vom Master auf dem Slave in eine andere Zieltabelle zu überführen, z.B. tx_h2news_domain_model_newsbeitrag.
Damit nur Spalten in die Datenbank geschrieben werden, die dort auch vorhanden sind, greift Mapper auf die Funktion \H2\H2contentsync\Domain\Repository\ContentRepository::getTableColumns() zu, das per SQL-Kommando SHOW COLUMNS FROM {table} die vorhandenen Spalten abruft.
_generateFields
Manchmal müssen Werte von Datensätzen bei der Synchronisation neu vergeben werden, z.B. der Wert für die TYPO3-Spalte sorting. Über die Konfiguration generate können Regeln definiert werden, wie solche Spalten behandelt werden.
Es gibt zwei mögliche Regeln, nach den diese Werte neu vergeben werden können:
- shift: Der neue Eintrag soll am Anfang eingefügt werden. Dazu wird der Wert der Spalte in allen vorhandenen Datensätzen um 1 erhöht. Der neue Datensatz erhält den Spaltenwert 0.
- push: Der neue Eintrag soll am Ende eingefügt werden. Aus allen vorhandenen Datensätzen wird der höchste Wert ermittelt. Der neue Datensatz erhält den Spaltenwert {höchster Wert} +1
Neben der allgemeinen generate-Einstellung gibt es noch die Einstellungen generate_on_new und generate_on_update, die speziell für neue oder zu aktualisierende Datensätze gelten.
Die Übernahme bzw. Neuvergabe von sorting-Werten ist ein heikles Thema. Sobald auch Daten im Slave gepflegt werden, ist Fingerspitzengefühl gefragt, damit es zu keinem absoluten Durcheinander kommt. Eventuell ist eine zweistufige Vorgehensweise sinnvoll:
- Bei der Erst-Synchronisation aller vorhandenen Master-Daten auf alle generate-Konfigurationen verzichten. Das sorting wird damit 1:1 vom Master übernommen
- Danach wird die generate_on_new-Konfiguration eingefügt, um Datensätze zu berücksichtigen, die in Zukunft hinzugefügt werden.
_updateMM
Diese Funktion erzeugt MM-Relationen für synchronisierte Datensätze. Die entsprechende Konfiguration heißt mm. Damit der Master die mm-referenzierten Datensätze an den Slave übermitteln kann, ist auf dem Master ebenfalls eine mm-Konfiguration nötig.
Zum Ermitteln der lokal auf dem Slave referenzierten Datensätze unternimmt _updateMM() folgende Schritte:
- Syncstatus-Suche: Sind referenzierte Datensätze bereits hersynchronisiert worden?
- Wenn nein:
- Wenn die Konfiguration use_foreign_as_default = 1, werden die Referenzen wie vom Master geliefert übernommen
- Wenn default_foreign_uids konfiguriert ist, werden die dort angegebenen Referenzen verwendet.
default_foreign_uids kann auch per TS-Code dynamisch erzeugt werden.
Besondere Aspekte
Slave: Simulation im Dryrun-Modus
Zu Test-/Debug-Zwecken gibt es für den Slave einen "Trockenlauf"-Modus, der keine Änderungen an der Datenbank vornimmt, sich aber im Rahmen des Möglichen wie der reguläre Modus verhält. Der Dryrun-Modus wird in der Extension-Konfiguration (Erweiterungsmanager) eingeschaltet.
Im Code wird über \H2\H2contentsync\Service\Configuration::isSlaveDryRun() geprüft, ob der Dryrun-Modus aktiv ist.
Datenbank-Zugriff
Die Extension greift direkt per SQL-Code auf die Datenbank zu. Das \H2\H2contentsync\Domain\Repository\ContentRepository stellt die entsprechenden Funktionen zur Verfügung.
Kommunikation zwischen Slave und Master
Der Aufruf vom Slave an den Master geschieht generell per HTTP-POST mit JSON als Payload. In der Regel antwortet der Master auch mit JSON, bzw. beim Dateitransfer auch mit den Rohdaten der angefragten Datei. Die Kommunikation geschieht über die Klasse \H2\H2contentsync\Service\Http.
Sicherheit
Die Kommunikation geschieht aktuell unverschlüsselt. Die minimalen Sicherheitsmaßnahmen werden über die Funktionen \H2\H2contentsync\Service\Http::addValidation() und \H2\H2contentsync\Service\Http::validateParams() angewendet bzw. geprüft.
Änderungen an synchronisierten Daten auf dem Slave
Wenn hersynchronisierte Daten auf dem Slave geändert werden, werden sie bei der nächsten Synchronisation überschrieben. Die Extension hat keine Konfliktbehandlung – der Master ist eben der Master und hat immer Recht.
Zwei-Wege-Synchronisation
Wenn zwei TYPO3s sich gegenseitig abgleichen sollen, können theoretisch Probleme mit endloser Synchonisation auftreten: Datensätze von A werden zu B übertragen und gelten dort als neu - also überträgt B sie an A. Um das zu verhindern, kann in der Slave-Konfiguration eine Master-Konfiguration angegeben werden (das muss dann auf beiden Servern geschehen). Wenn Server B von Server A als Master aufgerufen wird, überspringt B alle Datensätze, die ursprünglich von A stammten.
Logfile
Im TYPO3-Rootverzeichnis befindet sich die Datei h2contentsync.log, sobald die Synchronisierung im Slave angestoßen bzw. die Installation als Master angesprochen wurde. Im Logfile werden generell die allgemeinen Aufrufe protokolliert.
Das Logging wird über die Klasse \H2\H2contentsync\Service\Utils geregelt, die ihrerseits eine Variante der FB-Klasse ist. Um das Logging komplett auszuschalten, müsste ein Eingriff in dieser Klasse geschehen.
Bitte stelle sicher, dass das Logfile h2contentsync.log per .htaccess-Konfiguration vor dem Web-Zugriff geschützt ist.
<FilesMatch "h2contentsync\.log">
Order allow,deny
Deny from all
</FilesMatch>Ausführliches Logging
In der Master- oder Slave-Konfiguration kann für jede Tabelle explizit debug = 1 angegeben werden, um ein besonders ausführliches Logging anzuschalten.
Einschränkungen
In den Haupttabellen müssen diese Spalten vorhanden sein, damit eine Synchronisation möglich ist:
- uid als ID-Spalte
- tstamp als Spalte mit Unix-Timestamp der letzten inhaltlichen Änderung
Beide Spalten sind in fast allen TYPO3-Datentabellen vorhanden.
Tipps
Backend-Modul Contentsync
Aktuell ist das Modul nicht viel mehr als ein unfertiger Rumpf für ein mögliches Diagnose- und Debugmodul für die Inhaltssynchronisierung. In \H2\H2contentsync\Controller\SyncstatusController gibt es aktuell nur die Default-Action listAction(), die (sofern im Code aktiviert!) die synchronisierten Datensätze auf der gewählten Storage-Seite und die zugehörigen Syncstatus-Datensätze löscht. Mit dem Modul lässt sich also die Inhaltssynchronisierung auf einem Slave zurücksetzen.
Alle Datensätze neu synchronisieren
Wenn der Slave alle Datensätze vom Master abrufen soll (update), lässt sich das auf dem Slave mit diesem SQL-Code erledigen:
UPDATE tx_h2contentsync_domain_model_syncstatus SET foreign_tstamp = foreign_tstamp -1
Der Slave meldet dann "alte" Timestamps für die vorhandenen Daten an den Master, sodass dieser alle Inhalte dort als neuer ansieht und damit als zu aktualisierende Daten ausliefert.