![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
||||||||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|||||||
![]() |
|||||||
![]() |
|
||||||
![]() |
Um auf perlmeister.com üblicherweise etwas zu verändern, schicke ich Skripts und Dateien, die ich lokal getestet habe, per FTP an den Rechner bei meinem Internetprovider. Andererseits darf ich dort auch per telnet zugreifen, und so loggt man sich schon mal schnell ein, um kleine Fehler zu korrigieren. Das hat natürlich Inkonsistenzen zur Folge, Live- und Testversion driften mit der Zeit auseinander.
Ordnung stellt da ein Spiegelprogramm her, das schlau genug ist, zu erkennen, ob Dateien auf beiden Rechnern in unterschiedlichen Versionen vorliegen und auch, welche zuletzt verändert wurden. Ist's die Testversion, muß die Live-Version nachgezogen werden, wurde die Live-Version eigenmächtig verändert, muss der Fix wieder in die Test-Version einfließen. Dabei sollten möglichst wenig Datenbytes durch die Leitung rauschen, da die Verbindung aus einer guten alten Telefonleitung mit Modemanschluss besteht.
Lösen lässt sich dieses Problem mit jeweils einer Zustandsdatei, die für alle Dateien unter einem Verzeichnisbaum deren Checksummen auflistet -- zusammen mit dem Datum, an dem die Dateien zuletzt modifiziert wurden. Diese Zustandsdatei wird in zwei Versionen erzeugt, eine für's Live- und eine für's Testsystem. Stellt man beide gegenüber, wird schnell klar, welche Dateien wohin wandern müssen um Konsistenz zu erzielen. Hier ein Auszug einer aktuell auf perlmeister.com gewonnenen Datei:
KdAcNrPHbMAlXEebDTifMw 946759666 HTML/perl/index.html pn9+7O+DmVZMMpcbutYcBg 901438268 HTML/images/perlmeister.jpg I1cKtvJijHAiQGfBvz1M2A 915775874 HTML/perlpower/cdrom/scripts/basehtml.pl ...
Das erste Feld jeder Zeile der Synchronisationsdatei ist ein nach dem MD5-Verfahren generierter Digest-Stempel aus dem Inhalt der Datei, deren Name und Pfad im dritten Feld abgelegt sind. Zwei Dateien verschiedenen Inhalts liefern (fast) garantiert zwei verschiedene 16-Byte-Checksummen, die hier Base64-kodiert abgelegt sind. Im zweiten Feld jeder Zeile der Synchronisationsdatei steht das Datum der letzten Dateimodifikation, gemessen in den Unix-üblichen Sekunden seit 1970.
Zeigen nun die zwei Statusdateien von Live- und Testsystem zwei verschiedene MD5-Stempel einer Datei, so liegt sie offensichtlich auf den Rechnern A und B in unterschiedlichen Versionen vor. Weist das Modifikationsdatum der Datei auf Rechner A auf einen späteren Zeitpunkt als das der Datei auf Rechner B, wurde die Datei offensichtlich auf Rechner A zuletzt verändert und muss zur Synchronisation von A nach B kopiert werden. Liegt umgekehrt der Zeitstempel der Datei auf Rechner B weiter vorn, muss die B-Version nach A kopiert werden, um beide Verzeichnisbäume auf dem neuesten Stand zu halten.
Das in Listing Sync.pm dargestellte Modul Sync bietet nun eine objektorientierte Schnittstelle, um diese Statusdateien zu erzeugen, wieder auszulesen und mit dem Verzeichnisbaum abzugleichen. Die erste Version der Statusdatei erzeugt Sync, indem es den Verzeichnisbaum traversiert, zu jeder Datei einen MD5-Stempel erzeugt und ihn zusammen mit dem ebenfalls leicht verfügbaren letzten Modifikationsdatum ablegt. Allerdings ist die Erzeugung eines MD5-Digests eine aufwendige Angelegenheit: Jede Datei muss geöffnet, vollständig ausgelesen und die Daten wüsten Berechnungen unterworfen werden. Das kann sich bei unter Umständen tausenden von Dateien schon länger hinziehen. Geschickter ist es, eine schon bestehende Statusdatei dazu zu verwenden, nur die MD5-Stempel derjenigen Dateien zu berechnen, die sich seit dem letzten Lauf geändert haben. Da dieser Datumsstempel sowohl in der Statusdatei als auch (ohne die Datei zu öffnen) im Dateisystem erhältlich ist, kann so die Rechenzeit beim synchronisieren drastisch reduziert werden. Hierbei gibt es vier Möglichkeiten:
Das Modul Sync bietet die Schnittstelle, um auf einem lokalen Rechner eine wie oben aufgelistete Statusdatei für einen Verzeichnisbaum zu erzeugen:
use Sync; my $status = Sync->new(-basedir => "/usr/bin");
erzeugt ein neues Objekt vom Typ Sync, welches den Verzeichnisbaum unterhalb von /usr/bin kontrolliert. Existiert bereits eine Statusdatei von früheren Läufen her, kann diese zur beschleunigten Bearbeitung mit
$status->read_status_file() or warn "No status file";
eingelesen werden. Sie liegt, falls vorhanden, definitionsgemäß als .syncstatus im Top-Verzeichnis des vorher angegebenen Dateibaums vor. Das Sync-Objekt, auf das $status zeigt, speichert die eingelesenen Werte intern. Um nun den Zustand des Verzeichnisbaumes zu kontrollieren, rattert die update_status-Methode
$status->update_status();
durch den Baum und bringt das Sync-Objekt auf den neuesten Stand. Erst
$status->write_status_file();
schreibt die gesammelten Werte wieder in die ursprüngliche Statusdatei zurück. Um deren Zustand mit einer weiteren Statusdatei zu vergleichen und eventuelle Synchronisierungsmaßnahmen abzuleiten, erzeugt man einfach ein zweites Sync-Objekt mit
my $remote_status = Sync->new(); $remote->read_status_file();
welches in diesem Fall wegen des im Konstrukturaufruf fehlenden -basedir-Parameters die Datei .syncstatus im aktuellen Verzeichnis sucht und einliest. Der anschließende Vergleich mit
@actions = $status->compare($remote);
liefert eine Liste mit Vorschlägen zurück, um die festgestellten Inkonsistenzen beheben. Sync selbst führt diese Aktionen nicht aus, sondern gibt nur die für den Abgleich nötigen Informationen an Sync-Nutzer weiter -- wie das unten vorgestellte Skript sync.pl, welches nach Bedarf die passenden FTP-Befehle in Gang setzt.
Listing syncserver.pl zeigt eine einfache Anwendung von Sync.pm, die auf dem Remote-Server läuft: syncserver.pl erstellt eine Statusdatei des Dateibaums unterhalb von /home/mschilli. Da es vorkommen kann, dass bestimmte Zweige des Baums uninteressant sind, erlaubt es das Sync-Modul, dem Konstruktor eine Liste von regulären Ausdrücken mitzugeben. Der Parameter -exclude nimmt eine Referenz auf eine Liste entgegen, die die regulären Ausdrücke als Elemente im Stringformat enthält. Passt auch nur ein Ausdruck auf einen Teil eines Pfades im Dateibaum, verfolgt Sync diesen nicht weiter, sondern fährt im Nachbarpfad fort. syncserver.pl definiert in den Zeilen 6 bis 9 einen Array @EXCLUDE, der die Elemente .htaccess, .htpasswd und HTML/_ enthält, denn die Apache-Kontrolldateien .ht* möchte ich nicht synchronisieren und auch die Pfade HTML/_vti_bin, HTML/_vti_log etc. sollen keine Rolle spielen, darum HTML/_, das passt auf alle.
Listing 1: syncserver.pl |
01 #!/usr/bin/perl -w 02 03 use Sync; 04 05 my $LOCAL_DIR = "/home/mschilli"; 06 my @EXCLUDE = qw( .htaccess 07 .htpasswd 08 HTML/_ 09 ); 10 11 $local = Sync->new(-basedir => $LOCAL_DIR, 12 -exclude => \@EXCLUDE); 13 $local->read_status_file() or warn "No status file found"; 14 $local->update_status(); 15 $local->write_status_file(); |
Zeile 11 erzeugt ein Sync-Objekt, in dem es dem Konstruktor den Namen des Dateibaumes und eine Referenz auf den Array mit den Ausnahmeverzeichnissen mitgibt. Zeile 13 liest eine eventuell schon vorhandene Statusdatei ein, um eine höhere Verarbeitungsgeschwindigkeit zu erzielen. Falls diese nicht existiert, wird kein großes Rambazamba veranstaltet, sondern nur eine kleine Warnung ausgegeben -- beim ersten Aufruf von syncserver.pl ist das normal. Zeile 14 führt den Abgleich mit dem Dateibaum durch, Zeile 15 schreibt eine aufgefrischte Version der .syncstatus-Datei nach /home/schilli -- das war's!
Das in Listing sync.pl vorgestellte Skript macht nun auf der lokalen Maschine folgendes, um den Rechner mit einem entfernten Server zu synchronisieren: Es ruft über telnet das Skript syncserver.pl auf der Remote-Maschine auf, welches dort den Dateibaum analysiert und eine neue Statusdatei .syncstatus anlegt. Anschließend startet die lokale Maschine einen FTP-Prozess, der sich diese Statusdatei von der Remote-Maschine holt. Einmal eingetroffen, wird sie von einem Sync-Objekt eingelesen und dieses anschließend mit einem zweiten Sync-Objekt, das den lokalen Zustand der Maschine widerspiegelt, verglichen.
Entsprechend den oben dargelegten Ergebnissen des MD5-Stempel- und Datumsvergleichs wird es nun eventuell notwendig, eine Reihe von Dateien zwischen Local- und Remote-Rechner hin- oder herzukopieren. Da sich dies unter Umständen kritisch auswirken kann, bietet sync.pl eine interaktive Schnittstelle an, die den Anwender auswählen lässt, ob er
will. Dabei schlägt sync.pl das entsprechend seinen Untersuchungen Vernünftigste als Default-Eintrag vor, so dass der Anwender in den meisten Fällen nur die Return-Taste drücken muss, um den richtigen Vorgang einzuleiten. Eine typische Session mit sync.pl sieht folgendermaßen aus:
$ sync.pl Running sync.pl on remote.host.com ... Grabbing .syncstatus from remote.host.com ... Reading in remote .syncstatus ... 4 actions necessary. ... bin/myscript.pl -- Local newer: [G]et Remote [P]ush Local [I]gnore [D]elete local/remote [L]ocal delete [R]emote delete [P]> _
sync.pl hat also festgestellt, dass die Datei bin/myscript.pl (Pfad relativ zum überwachten Verzeichnisbaum) lokal in einer aktuelleren Version (dem Zeitstempel nach zu urteilen) vorliegt als auf dem Remote-Server und meldet dies mit "Local newer". Tippte der Benutzer jetzt G für Get Remote (Groß- und Kleinbuchstaben werden gleichermaßen anerkannt), gefolgt von der Return-Taste, kopierte sync.pl die Server-Version auf den lokalen Rechner -- doch das wäre im vorliegenden Fall falsch, da ja, wie gemeldet, die lokale Version die neuere ist, die es zu propagieren gilt. Mit P liesse sich der ``richtige'' Vorgang, der Push der lokalen Datei auf den Server, einleiten. Die Eingabe L veranlasst sync.pl, die lokale Version zu löschen, mit R annulliert es die Remote-Version. Mit D für Delete radiert das Spiegelprogramm beide Versionen aus. Mit I wird die Inkonsistenz ignoriert und mit der nächsten notwendigen Aktion fortgefahren. Wie oben ersichtlich, hat sync.pl in der letzten Zeile der Ausgabe bereits mit P die vernünftigste Lösung vorgeschlagen -- drückt der Anwender lediglich die Return-Taste, wandert eine Kopie der neuen lokalen Version auf den Server und der Abgleich ist -- höchstwahrscheinlich zur Zufriedenheit aller -- durchgeführt.
Die Konfigurationssektion in sync.pl zwischen den Zeilen 4 und 16 legt eine Reihe von Parametern fest: Das lokale Verzeichnis des zu spiegelnden Verzeichnisbaums ($LOCAL_DIR), eine Liste von regulären Ausdrücken von Zweigen, die wir vom Spiegeln ausschließen wollen (@EXCLUDE), den Remote-Rechner und das zu spiegelnde Verzeichnis dort ($REMOTE_HOST und $REMOTE_DIR), Benutzername und Passwort dort ($USERNAME, $PASSWD), einen regulären Ausdruck für den dort erwarteten Shell-Prompt, den Namen des Skripts, das auf dem Remote-Server die Status-Datei erzeugt ($SERVERSYNC), den Namen des GNU-Zip-Programms dort ($GZIP) und eine Konstante für den Timeout einer Telnet-Session in Sekunden ($TELNET_TO).
Die Zeilen 19 bis 23 ziehen benötigte Zusatzmodule herein: Das oben schon erwähnte und weiter unten ausführlicher beschriebene Sync zur Synchronisation zweier Verzeichnisbäume, sowie die Helfer Net::FTP und Net::Telnet, die den Netzwerkverkehr über telnet und ftp regeln.
File::Basename stellt die basename- und dirname-Funktionen parat, die wie ihre Verwandten in der Shell funktionieren und Datei- und Pfad aus einer vollständigen Pfadangabe extrahieren. File::Path ist in der Lage, beliebig verschachtelte Verzeichnisse auf einen Schlag anzulegen, die exportierte Funktion mkpath ist ein mkdir mit Tiefenwirkung.
Es folgt die Erzeugung der Statusdatei des lokalen Baums in den Zeilen 28 bis 32. Zeile 33 legt den im Modul Sync.pm versteckten Namen der Statusdatei in der Variablen $stat_file für später ab.
Sodann muss der Server ebenfalls eine Statusdatei erstellen. Zeile 39 erzeugt ein Telnet-Objekt, dann werden die Verbindung geöffnet, Benutzername und Passwort zum Einloggen gesendet und in der sich öffnenden Shell das Kommando sync.pl abgesetzt, welches wiederum auf dem Remote-Server den Verzeichnisbaum durchstöbert und nach getaner Arbeit eine Status-Datei anlegt. Das gzip-Programm auf dem Server komprimiert diese und die Zeilen 54 bis 64 nutzen das Net::FTP-Modul, um sie auf die lokale Maschine zu ziehen, wo sie ein Sync-Objekt mit dem Namen $remote einliest. Zeile 77 lässt die compare-Methode des Sync-Objekts die Aktionen bestimmen, die sich aus dem Vergleich beider Statusdateien ergeben.
Ab Zeile 82 wird wieder per FTP am Server angedockt, um die notwendigen Dateiabgleiche durchzuführen -- nicht ohne vorher mit dem Anwender Rücksprache zu halten, freilich.
Wie sich später im Modul Sync zeigen wird, besteht eine Aktion aus einer Referenz auf eine Liste mit drei Elementen: Die (relative) Pfadangabe der betreffenden Datei, ein String mit Buchstaben für die erlaubten Aktionen ("GPI" bedeutet z.B. Get, Push, Ignore) und eine erklärenden Nachricht.
Zeile 98 druckt die Einleitung aus, die dem Benutzer hilft, sich für eine Aktion zu entscheiden, Zeile 99 ruft mit ask_choice eine in Zeile 131 definierte Funktion auf, die eine Reihe von Aktionen zur Auswahl stellt, die erste Aktion im String zum Default-Wert macht und dem Benutzer eine Entscheidung abverlangt.
Den Rückgabewert von ask_choice erhält $choice zugewiesen. Es ist einer der Buchstaben g, p, r, l, d oder i, der, wie oben beschrieben, indiziert, wie der Abgleich durchzuführen ist (g: Get, p: Push, etc.).
Listing 2: sync.pl |
001 #!/usr/bin/perl -w 002 003 ################################################## 004 my $LOCAL_DIR = "/my/local/directory"; 005 my @EXCLUDE = qw( .htaccess 006 .htpasswd 007 HTML/_ 008 ); 009 my $REMOTE_HOST = "remote.host.com"; 010 my $REMOTE_DIR = "."; 011 my $USERNAME = "ratbert"; 012 my $PASSWD = "nixgibts!"; 013 my $PROMPT = '/\$/'; 014 my $SERVERSYNC = "syncserver.pl"; 015 my $GZIP = "gzip"; 016 my $TELNET_TO = 300; 017 ################################################## 018 019 use Sync; 020 use Net::FTP; 021 use Net::Telnet; 022 use File::Basename; 023 use File::Path; 024 025 ################################################## 026 # Read local tree 027 ################################################## 028 $local = Sync->new(-basedir => $LOCAL_DIR, 029 -exclude => \@EXCLUDE); 030 $local->read_status_file() or warn "No status file"; 031 $local->update_status(); 032 $local->write_status_file(); 033 my $stat_file = $local->stat_file(); 034 035 ################################################## 036 # First, run the sync.pl script on the server 037 ################################################## 038 print "Running $SERVERSYNC on $REMOTE_HOST ...\n"; 039 $telnet = new Net::Telnet (Timeout => $TELNET_TO, 040 Prompt => $PROMPT); 041 $telnet->open($REMOTE_HOST) or 042 die "Cannot open $REMOTE_HOST"; 043 $telnet->login($USERNAME, $PASSWD) or 044 die "Cannot login"; 045 $telnet->prompt($PROMPT); 046 $telnet->cmd("cd $REMOTE_DIR; $SERVERSYNC"); 047 $telnet->cmd("$GZIP -9c $stat_file >$stat_file.gz"); 048 $telnet->close; 049 050 ################################################## 051 # Now, fetch the .syncstatus file from the server 052 ################################################## 053 print "Grabbing $stat_file from $REMOTE_HOST ...\n"; 054 $ftp = Net::FTP->new($REMOTE_HOST) or 055 die "Cannot connect to $REMOTE_HOST"; 056 $ftp->login($USERNAME, $PASSWD) or 057 die "Cannot login"; 058 $ftp->cwd($REMOTE_DIR) or 059 die "Cannot chddir to $REMOTE_DIR"; 060 $ftp->binary; 061 $ftp->get("$stat_file.gz") or 062 die "Cannot get $stat_file.gz"; 063 system("$GZIP -df $stat_file.gz"); 064 $ftp->quit; 065 066 ################################################## 067 # Read remote tree via status file 068 ################################################## 069 print "Reading in remote $stat_file ...\n"; 070 $remote = Sync->new(-exclude => \@EXCLUDE); 071 $remote->read_status_file() or 072 warn "No status file found"; 073 074 ################################################## 075 # Compare and derive actions 076 ################################################## 077 @actions = $local->compare($remote); 078 079 ################################################## 080 # Put/Get files according to actions defined 081 ################################################## 082 $ftp = Net::FTP->new($REMOTE_HOST) or 083 die "Cannot connect to $REMOTE_HOST"; 084 $ftp->login($USERNAME, $PASSWD) or 085 die "Cannot login"; 086 $ftp->cwd($REMOTE_DIR) or 087 die "Cannot chdir to $REMOTE_DIR"; 088 $ftp->binary; 089 090 print scalar @actions, " actions necessary.\n"; 091 092 foreach my $action (@actions) { 093 my ($path, $choices, $reason) = @$action; 094 095 my $local_dir = dirname("$LOCAL_DIR/$path"); 096 my $local_file = basename($path); 097 098 print "$path -- $reason\n"; 099 my $choice = ask_choice($choices); 100 101 $choice eq 'g' and do { 102 print "GET $path\n"; 103 mkpath($local_dir) unless -d $local_dir; 104 $ftp->get($path, "$local_dir/$local_file") or 105 die "GET failed" }; 106 107 $choice eq 'p' and do { 108 print "PUT $path\n"; 109 $ftp->mkdir(dirname($path), 1); 110 $ftp->put("$local_dir/$local_file", $path) or 111 die "PUT failed" }; 112 113 $choice eq 'r' || $choice eq 'd' and do { 114 print "DELETE remote $path\n"; 115 $ftp->delete($path) or die "Delete failed" }; 116 117 $choice eq 'l' || $choice eq 'd' and do { 118 print "DELETE local $path\n"; 119 unlink "$local_dir/$local_file" or 120 die "Unlink failed"; 121 }; 122 123 $choice eq 'i' and do { print "Ignoring\n" }; 124 125 print "\n\n"; 126 } 127 128 $ftp->quit; 129 130 ################################################## 131 sub ask_choice { 132 ################################################## 133 my $choices = shift; 134 my $default_action = substr($choices, 0, 1); 135 136 local $| = 1; 137 138 my %messages = ( 139 G => "[G]et Remote", 140 P => "[P]ush Local", 141 R => "[R]emote delete", 142 L => "[L]ocal delete", 143 D => "[D]elete local/remote", 144 I => "[I]gnore" ); 145 146 { while($choices =~ /(.)/g) { 147 print $messages{$1}, "\n"; 148 } 149 print "[$default_action]> "; 150 151 chop($word = <STDIN>); 152 $word = $default_action unless $word; 153 redo unless exists $messages{uc($word)}; 154 return lc($word); 155 } 156 } |
Mit dem FTP-Abgleich sind einige Beschränkungen verbunden: So kümmert sich sync.pl nicht um die Benutzerrechte von Dateien und man muss, falls notwendig, beim ersten Mal von Hand die Ausführungsrechte ändern. Das Synchronisationsverfahren geht auch davon aus, dass die lokale und die Remote-Maschine in etwa dieselbe Uhrzeit fahren, andernfalls ist nicht klar, welche Version einer Datei zuletzt verändert wurde. Und, eine Beschränkung von dem weiter unten in Sync.pm verwendeten Modul File::Find: Es verfolgt keine symbolischen Links.
Dreh- und Angelpunkt ist freilich das Modul Sync aus Listing Sync.pm, das drei Zusatzmodule verwendet: IO::File für neumodische File-Handles, die man ohne Glob-Jonglierung in normalen Variablen speichern kann (schön für Filehandles als Instanzvariablen für Objekte), weiter File::Find, um Dateibäume zu durchsuchen und schließlich Digest::MD5, das Modul, das MD5-Stempel erzeugt. Der Konstruktor new ab Zeile 16 nimmt mit dem Hash %hash die flexiblen Parameter entgegen, so dass auf
Sync->new(-basedir => "abc", -exclude => ["/tmp"]);
der Hash %hash unter den Keys -basedir und -exclude die entsprechenden Werte führt. Zeile 24 schiebt den Namen der Statusdatei auf die exclude-Liste, um diese automatisch vom Abgleich auszuschließen. Zeile 27 compiliert die als Strings gegebenen regulären Ausdrücke mit dem in perl 5.005_03 brandneuen qr-Operator in reguläre Ausdrücke und schiebt diese auf eine Liste, die unter der Instanzvariablen exclude aufgehängt ist.
read_status_file ab Zeile 35 liest die Statusdatei ein, die im obersten Verzeichnis des zu spiegelnden Verzeichnisbaums liegt. Jede Zeile der Datei führt den MD5-Hash, den Zeitpunkt der letzten Modifikation und die Datei selbst mitsamt relativer Pfadangabe, vom Verzeichnisbaum aus gerechnet.
Den Zustand des überwachten Dateibaums hält das Sync-Objekt in $self->{status} vor, eine Referenz auf einen Hash, der zu jeder Pfadangabe auf eine Liste verweist, die den MD5-Hash und den Modifizierungszeitstempel der jeweiligen Datei enthält.
update_status ab Zeile 56 ruft die find-Funktion des File::Find-Moduls auf, die für alles Gefundene unter dem basedir-Verzeichnis die finder-Funktion aufruft und in $_ (eine Schrulle des File::Find-Moduls) den jeweiligen Dateisystemeintrag übergibt. Dort geht's in Zeile 78 gleich wieder zurück, falls das Gefundene keine Datei ist. Die Zeilen 80 und 81 legen in $path den Pfad dorthin ab -- relativ zum Basis-Verzeichnis, das in der Instanzvariablen basedir liegt.
Die foreach-Schleife zwischen 83 und 87 probiert, ob einer der für uninteressante Pfade angegebenen regulären Ausdrücke passt und, falls ja, wird $_ auf den ursprünglichen Wert zurückgesetzt (das will Find::Find so) und zurückgesprungen. Zeile 89 bestimmt das Modifikationsdatum der Datei und legt es in $mod_time ab. Wenn der finder über eine Datei stolpert, die nicht im Hash unter $self->{status} hängt, also noch nie bearbeitet wurde, oder eine Datei zwar dort registriert ist, aber ihr Zeitstempel anzeigt, dass sie zwischenzeitlich modifiziert wurde, konstruiert Zeile 96 ein neues Digest::MD5-Objekt, dessen addfile-Methode einen Filehandle-Glob einer geöffneten Datei entgegennimmt, die Daten einliest und den MD5-Wert bestimmt. Die b64digest-Methode macht einen Base64-codierten String daraus, der zusammen mit dem Zeitstempel in eine Liste wandert, die später wieder unter dem Pfadnamen in $self->{status}->{Pfadname} auffindbar ist. Zeile 105 setzt, File::Find zuliebe, $_ wieder auf den Wert zurück, den es beim Aufruf der finder-Funktion hatte.
Wenn update_status also aus der find-Funktion zurückgekehrt, haben alle neuen und veränderten Dateien ihren Weg ins Sync-Objekt gefunden -- doch was ist mit Dateien, die das Sync-Objekt nach dem Lesen der Statusdatei kannte, die aber zwischenzeitlich aus dem Dateibaum verschwunden sind? Eine Hilfsinstanzvariable status_chk des Sync-Objekts wurde vor dem Aufruf von find in Zeile 60 initialisiert, um auf einen Hash zu zeigen, der alle zu diesem Zeitpunkt bekannten Dateipfade als Schlüssel enthält. In finder werden aus diesem Hilfs-Hash in Zeile 91 alle Dateipfade gelöscht, die auch tatsächlich im Dateisystem gefunden wurden. Kommt update_status aus find zurück, bleiben in $self->{status_chk} nur Dateien übrig, die verschwunden sind und deshalb befreit die foreach-Schleife in 67 bis 69 den Status-Hash von ihnen.
Die compare-Methode des Sync-Objekts ab Zeile 130 vergleicht zwei Sync-Objekte und schlägt Aktionen vor, die Dateibäume zu bereinigen. Eine Aktion besteht aus der Pfadangabe der Datei, einem String, dessen Zeichen geeignete Kopier/Löschaktionen symbolisieren und einer kurzen Meldung, die den Zustand beschreibt. Stellt compare also fest, dass z.B. die zwei MD5-Stempel einer Datei ungleich, der lokale Zeitstempel aber weiter in der Zukunft liegt als der der Remote-Server-Version, schließt es messerscharf, dass die Datei lokal zuletzt verändert wurde und schlägt, wie in den Zeilen 152-153, "PGIDLR" vor: Das vernünftigste scheint der Push der lokalen Datei auf die Remote-Maschine zu sein, dann kommt der Get, Ignore, Delete, Local delete, Remote delete. Dateien, die nur in der Statusdatei des Remote-Servers auftauchen aber nicht in der des lokalen Rechners, behandelt die erste foreach-Schleife nicht, deswegen kommt in Zeile 164 eine zweite Schleife zum Einsatz, die nach Dateien sucht, die ausschließlich auf dem Remote-Server gefunden wurden. Zeile 169 eliminiert wieder unerwünschte Pfade (für den Fall, dass sie syncserver.pl durchschlüpfen ließ) und Zeile 176 gibt schließlich @actions zurück, eine Liste mit Aktionen als Elementen, sortiert nach den Dateipfaden, die jeweils als erstes Element in den Unter-Listen stehen (deswegen $a->[0] und $b->[0]).
write_status_file ab Zeile 130 schreibt einfach den Status-Hash in die Statusdatei, ein Eintrag pro Zeile, und die Einzelfelder durch Leerzeichen getrennt:
MD5-Stempel Zeitstempel Dateipfad ...
stat_file ab Zeile 180 ist nur eine Accessor-Funktion für die Klassenvariable $STAT_FILE, die den Namen der Statusdatei festlegt.
Net::Telnet, Net::FTP und Digest::MD5 gibt's auf dem CPAN. Sie lassen sich am einfachsten mit der CPAN-Shell installieren:
perl -MCPAN -eshell cpan> install Net::Telnet cpan> install Net::FTP cpan> install Digest::MD5
Der Rest der verwendeten Module ist bei perl 5.005_03 dabei, mit dem das Skript wegen dem verwendeten qr-Konstrukt auch laufen muss. syncserver.pl kommt auf die Remote-Maschine, in einen Pfad, unter dem die telnet-Shell das Skript auch findet. Der ``Shebang'', also die erste Zeile, die den Interpreter festlegt, muss, falls der Perl-Interpreter dort nicht unter /usr/bin/perl wartet, korrigiert werden. Ausserdem müssen auf der Remote-Maschine die Module Sync.pm und Digest::MD5 verfügbar sein. Die lokale Maschine braucht sync.pl, Sync.pm Net::Telnet, Net::FTP und Digest::MD5. Dann noch schnell die Zeilen 9 bis 16 in sync.pl an die lokalen Gegebenheiten angepasst -- und schon kann der Abgleich mit sync.pl beginnen -- bis zum nächsten Mal, spiegelt fleissig!
Listing 3: Sync.pm |
001 package Sync; 002 ################################################## 003 # mschilli1@aol.com, 2000 004 ################################################## 005 006 use IO::File; 007 use File::Find; 008 use Digest::MD5 qw(md5_base64); 009 010 my $STAT_FILE = ".syncstatus"; 011 012 ################################################## 013 # $status = Sync->new(-basedir => "base/dir", 014 # -exclude => ["/excluded", '/dir/.*\.gif']); 015 ################################################## 016 sub new { 017 ################################################## 018 my ($class, %hash) = @_; 019 020 my $self = { basedir => $hash{-basedir} || ".", 021 status => {} 022 }; 023 024 push( @{$hash{-exclude}}, $STAT_FILE); 025 026 if(exists $hash{-exclude}) { 027 $self->{exclude} = 028 [map qr($_), @{$hash{-exclude}}]; 029 } 030 031 bless $self, $class; 032 } 033 034 ################################################## 035 sub read_status_file { 036 ################################################## 037 my $self = shift; 038 039 my $file = "$self->{basedir}/$STAT_FILE"; 040 041 open STAT, "<$file" or return 0; 042 043 while(<STAT>) { 044 # MD5-Hash, timestamp, path 045 if(/(\S+) (\d+) (.*)/) { 046 $self->{status}->{$3} = [$1, $2]; 047 } 048 } 049 050 close STAT; 051 052 return 1; 053 } 054 055 ################################################## 056 sub update_status { 057 ################################################## 058 my($self) = @_; 059 060 $self->{status_chk} = { %{$self->{status}} }; 061 062 find( sub { $self->finder }, $self->{basedir}); 063 064 # Everything that's left in status_chk is a 065 # ref in the .syncstatus file without a 066 # corresponding real file -- axe them all! 067 foreach my $path (keys %{$self->{status_chk}}) { 068 delete $self->{status}->{$path}; 069 } 070 } 071 072 ################################################## 073 sub finder { 074 ################################################## 075 my ($self) = @_; 076 my $file = $_; 077 078 return unless -f $file; 079 080 my $path = "$File::Find::dir/$file"; 081 $path =~ s#^$self->{basedir}/##; 082 083 foreach my $regex (@{$self->{exclude}}) { 084 if($path =~ $regex) { 085 $_ = $file; return 1; 086 } 087 } 088 089 my $mod_time = (stat($file))[9]; 090 091 delete $self->{status_chk}->{$path} if 092 exists $self->{status}->{$path}; 093 094 if(!exists $self->{status}->{$path} || 095 $mod_time > $self->{status}->{$path}->[1]) { 096 my $ctx = Digest::MD5->new(); 097 open FILE, "<$file" or 098 die "Cannot open $file"; 099 $ctx->addfile(*FILE); 100 close FILE; 101 $self->{status}->{$path} = 102 [$ctx->b64digest, $mod_time]; 103 } 104 105 $_ = $file; 106 } 107 108 109 ################################################## 110 sub write_status_file { 111 ################################################## 112 my $self = shift; 113 114 my $path = "$self->{basedir}/$STAT_FILE"; 115 116 my $sfh = IO::File->new("> $path") or 117 die "Cannot open $path"; 118 119 foreach my $path (keys %{$self->{status}}) { 120 print $sfh "@{$self->{status}->{$path}}" . 121 " $path\n"; 122 } 123 124 $sfh->close; 125 } 126 127 ################################################## 128 # Compare 129 ################################################## 130 sub compare { 131 my ($self, $remote) = @_; 132 133 my @actions = (); 134 135 foreach $path (keys %{$self->{status}}) { 136 137 if(exists $remote->{status}->{$path}) { 138 139 my ($local_md5, $local_time) = 140 @{$self->{status}->{$path}}; 141 my ($remote_md5, $remote_time) = 142 @{$remote->{status}->{$path}}; 143 144 next if $remote_md5 eq $local_md5; 145 146 if($remote_time > $local_time) { 147 # Server got a newer version 148 push(@actions, 149 [$path, "GPIDLR", "Remote newer"]); 150 } else { 151 # Client got a newer version 152 push(@actions, 153 [$path, "PGIDLR", "Local newer"]); 154 } 155 } else { 156 # File's not on remote -- local only 157 push(@actions, 158 [$path, "PLI", "Local only"]); 159 } 160 } 161 162 # Files on the remote server but not local: 163 REMOTE: 164 foreach $path (keys %{$remote->{status}}) { 165 166 next if exists $self->{status}->{$path}; 167 168 foreach my $regex (@{$self->{exclude}}) { 169 next REMOTE if $path =~ $regex; 170 } 171 172 push(@actions, 173 [$path, "GRI", "Remote only"]); 174 } 175 176 sort { $a->[0] cmp $b->[0] } @actions; 177 } 178 179 ################################################## 180 sub stat_file { 181 ################################################## 182 return $STAT_FILE; 183 } 184 185 1; |
Der Autor |
Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, Kalifornien. Er ist Autor des 1998 bei Addison-Wesley erschienenen (und 1999 für den englischsprachigen Markt als "Perl Power" herausgekommenen) Buches "GoTo Perl 5" und unter michael@perlmeister.com oder http://perlmeister.com zu erreichen. |
Copyright © 2000 Linux New Media AG