Ein Skript räumt alte Backup-Dateien auf und etabliert dabei tägliche, wöchentliche und monatliche Sicherungszyklen.
Statt mit magnetischen Bändern zu jonglieren, kann man Backups auch einfach
auf eine zusätzliche Festplatte schreiben. Das geht nicht nur superschnell
sondern auch billig -- 30-Gigabyte-Platten bekommt man in den USA heutzutage
schon für unter $100.
Ein Skript wie backup.pl
sichert dann einfach per tar
-Befehl
eine Reihe von Verzeichnissen (in @DIRS
) und legt deren Inhalt in einer
komprimierten tar
-Datei unter einem neuen Namen ab:
Am 21.9.2001 steht dann da beispielsweise
backup.20010921.tgz
und wartet auf den aufgeregten Systemadministrator,
falls am 22. September die andere Festplatte kracht.
Diese täglichen tar
-Bälle sammeln sich kontinuierlich an und
irgendwann steht der Administrator vor dem Problem, alte Backups wegzuwerfen
-- nur welche?
01 #!/usr/bin/perl 02 ################################################## 03 # backup -- Mike Schilli, 2001 (m@perlmeister.com) 04 ################################################## 05 use warnings; 06 use strict; 07 08 my $TAR = "/bin/tar"; 09 my @DIRS = qw(/u/mschilli/dev /data/scripts); 10 my $BAK_DIR = "/data/backups"; 11 12 my ($y, $m, $md) = (localtime(time))[5,4,3]; 13 14 my $date = sprintf "%d%02d%02d", 15 1900+$y, $m+1, $md; 16 17 system($TAR, "zcfv", "$BAK_DIR/backup.$date.tgz", 18 @DIRS) and die "$TAR failed ($!)";
Abbildung 1 zeigt einen typischen Backup-Kalender. Der rote Ring markiert das aktuelle Datum, die grünen Häkchen bezeichnen die Tage, von denen wir gerne ein Backup hätten: Von jedem Tag der vergangenen Woche, dann jeweils wöchentlich am Montag für die letzten vier Wochen und schließlich monatlich jeweils am Ersten für die letzten drei Monate.
Abbildung 1: Typische Backup-Zeiten im Kalender |
Das heute vorgestellte Skript mop.pl
sucht in einem vorgegebenen
Verzeichnis nach Backup-Dateien, ermittelt, welche davon
nach dem Backup-Kalender tatsächlich gebraucht werden und putzt alles
Veraltete weg.
Die Zeilen 6, 7 und 8 in mop.pl
fordern mindestens perl
Version 5.6.0 an, schalten den Warnungsmodus an
und bestehen auf strengen Richtlinien für nicht deklariere Variablen
und anderen Schnickschnack, den Perl in seiner unendlichen Güte sonst
durchlässt, der aber unsere lieben Anfänger rechts und links
herumschleudert.
Anschließend zieht es File::Basename
zur Pfadbehandlung,
Getopt::Std
zur Bearbeitung von Kommandozeilenoptionen und
Date::Calc
für Kalenderberechnungen herein -- letzteres
holen wir einfach mittels folgendem Kommando schnell vom CPAN:
perl -MCPAN -shell cpan> install Date::Calc
In Zeile 15 steht mit @BAK_DIR
als konfigurierbarem
Parameter das Verzeichnis,
in dem backup.pl
die Backup-Dateien ablegte.
mop.pl
nimmt zwei optionale Kommandozeilenparameter entgegen: -v
veranlasst das sonst sehr schweigsame Skript, dem Benutzer auf
der Standardausgabe genau zu melden, was genau vor sich geht.
Die Option -n
hingegen veranlasst mop.pl
, nur so zu tun
als ob. Es löscht die überflüssigen Backup-Dateien also nicht wirklich,
sondern meldet statt dessen nur xxx can go away. Praktisch zum Testen.
Die in Zeile 20 aufgerufene Funktion getopts()
aus dem Modul
Getopt::Std
nimmt sich der Kommandozeilenparameter
an und setzt im Hash %OPTS
die Einträge n
oder v
, wenn mop.pl
mit -v
oder -n
aufgerufen wurde.
getopts()
hat nur den Haken, dass es das Programm abbricht, falls
eine nicht unterstützte Option (z.B. -x
) vorliegt. Deshalb
bilden die Zeilen 19 bis 21 einen eval
-Block, der den getopts()
-Befehl
in einem Sandkasten ablaufen lässt. Falls das Programm auf einen mit
die()
ausgelösten tödlichen Fehler läuft, schnappt sich eval
diesen,
unterbricht die eval
-Verarbeitung, und es liefert einen falschen
Wert zurück. Gemeinerweise gibt getopts()
auch noch eine
mit warn()
ausgelöste Warnung aus, die allerdings
der mit $SIG{__WARN__}
definierte leere
Signal-Handler unterdrückt. mop.pl
nimmt alles selbst in die
Hand, gibt im Fehlerfall mit usage()
eine Meldung
samt Bedienungsanleitung aus und beendet in Zeile 124 das Programm
mit einem Fehlercode.
Zeile 27 ruft das weiter unten definierte calc_dates()
auf, das eine
Referenz auf den Hash %dates_needed
erwartet. Dessen Schlüssel setzt
calc_dates()
auf die Datumsangaben der Tage, von denen wir gerne
-- wie im Kalender in Abbildung 1 grün markiert --
ein Backup hätten. Existiert zum Beispiel
$dates_needed{"2001 4 17"}
, bleibt die Backupdatei vom 17.4.2001
später unangetastet.
Zeile 30 iteriert anschließend mit einem glob
-Konstrukt
über alle Dateien im Backup-Verzeichnis
und füllt den Hash %files
jeweils mit den Dateinamen als Schlüssel und
deren letztem Modifikationsdatum als Wert.
Diese Unix-üblichen Sekunden seit 1970, die die Funktion stat()
auf Indexposition 9 der von ihr zurückgegebenen Liste liefert, geben
einen Hinweis darauf, wann die Backup-Datei erstellt wurde.
Die for
-Schleife ab Zeile 37 braucht dann nur noch durch die nach
Datum sortierten Einträge dieses Hashes zu laufen und zu untersuchen,
ob die jeweilige Datei gemäß dem Backup-Fahrplan noch gebraucht wird oder
nicht. Die sort
-Bedingung in Zeile 37 sortiert deswegen numerisch
aufsteigend nach dem Modifikationsdatum der Datei in Sekunden seit 1970
-- so sind die Hashwerte die Sortierkriterien für die Schlüssel!
Die Zeilen 39 bis 42 nutzen die Perl-Funktion localtime()
und
den Formatierer sprintf
, um die Unix-Sekunden-Zeit in das
Kalenderformat (Jahr Monat Tag) umzuwandeln. Zu beachten ist, dass
localtime()
den Monat um 1
und das Jahr um 1900 verkürzt zurückgibt --
das wird kurzerhand korrigiert.
Zeile 44 prüft, ob die Datei im Backup-Fahrplan steht
(Eintrag in %dates_needed
vorhanden) oder aber von heute ist --
in diesen Fällen muß der Wischmop die Finger von
ihr lassen.
In diesem Fall löscht Zeile 49 den Eintrag in
%dates_needed
, denn: Produziert ein handgestricktes Sicherungsskript
mehrere Backup-Dateien am gleichen Tag,
wählt der Algorithmus so nur die erste an diesem Tag erstellte zum
Überleben aus.
Bleibt die Datei bestehen, gibt Zeile 46 eine informative
Meldung aus, falls die Kommandozeilenoption -v
(für verbose) gesetzt war.
Vorsicht, übrigens: mop.pl
ermittelt das Erstellungsdatum einer Backup-Datei
aus derem letzten Modifikationsdatum. Wer also zu einem späteren Zeitpunkt
die Backup-Dateien manipuliert, bringt den Algorithmus ins Schleudern.
Wer dergleichen nicht ausschließen kann, muss das Datum woanders her
beziehen -- zum Beispiel aus dem Dateinamen, den das Backup-Skript
backup.pl
auf das aktuelle Sicherungsdatum setzt.
Wird's ernst, löscht Zeile 55 die Datei mit unlink
-- aber nur, falls
nicht die Kommandozeilenoption -n
zum Simulieren vorliegt. Für
die Meldung in Zeile 58 im -v
-Modus
extrahiert die Funktion basename()
des File::Basename
-Pakets
aus der langen Pfadangabe nur den Dateinamen.
Um zu ermitteln, welche Sicherungsdateien der Backup-Fahrplan einfordert,
setzt calc_dates()
das Modul Date::Calc
von Steffen Beyer ein,
der, wie aus dem Autorenhinweis von Date::Calc
hervorgeht, noch
immer in München in der Ainmillerstraße 5, Appartment 513, wohnt!
Sein Date::Calc
ist jedenfalls, wie schon einmal an
dieser Stelle gesagt ([2]), die Wucht in Tüten.
Zeile 12 von mop.pl
forderte Date::Calc
schon auf, die
Funktionen Today()
, Add_Delta_Days()
, Add_Delta_YMD()
und Day_of_Week()
in den Namensraum von mop.pl
zu exportieren.
Today()
in Zeile 70 liefert dann das heutige Datum als Liste mit
den Elementen Jahr, Monat, Tag zurück.
Ab Zeile 73 berechnet mop.pl
das Datum der sieben vorhergehenden
Tage einfach dadurch, dass es mittels Add_Delta_Days
aus Date::Calc
vom heutigen Datum 1, 2, ..., 7 Tage abzieht.
Alle errechneten Daten speichert calc_dates()
im Format "Jahr Monat Tag"
als Schlüssel im Hash %dates_needed
des Hauptprogramms, auf den innerhalb der Mauern von calc_dates()
die Referenz $needed
zeigt.
Die Datumsangaben der verstrichenen 4 letzten Montage erhält man, indem man
mit der Funktion Day_of_Week
den heutigen Wochentag als Zahl von
1
(Montag) bis 7
(Sonntag) ausrechnet, und dann entweder eine
Woche zurückgeht (falls gerade Montag ist) oder den Differenzbetrag
zwischen 1 und dem heutigen Wochentag mit Add_Delta_Days
(negative
Tageszahl!) zurückschreitet. Dann geht mop.pl
ab Zeile 89
vier mal 7 Tage zurück und sammelt so die Montagsdaten ein.
Das Datum der letzten 3 Monatsersten hängt davon ab, ob heute der Erste
ist oder nicht. Falls ja, starten wir einen Monat in der Vergangenheit,
was Add_Delta_YMD()
einfach ausrechnet, indem wir ihm das heutige Datum
und die Liste (0, -1, 0)
(0 Jahre, -1 Monate, 0 Tage) zum Zurückschreiten
geben. Falls heute nicht der Erste ist, setzt Zeile 103 einfach den
Tagesanteil von @today
auf 1
und schreitet von dort an ab
Zeile 105 dreimal jeweils einen Monat zurück.
Ab Zeile 113 steht die usage()
-Funktion, die aus dem Hauptprogramm
dann aufgerufen wird, falls letzteres
mit einer unbekannten Option gestartet wurde.
usage()
gibt nicht nur die ihr übergebene Fehlermeldung aus sondern
teilt dem Benutzer auch gleich noch mit, welche Parameter mop.pl
versteht.
Anschließend bricht es mit dem exit
-Code 1
ab.
Stellt also ein täglicher Cronjob sicher, dass ein Skript wie
backup.pl
alle zu sichernden Verzeichnisse zu einer Backup-Datei im
Backup-Verzeichnis zusammenzurrt,
brauchen wir nur den Wischlappen mop.pl
hinterherzujagen, um die veralteten Backups vergangener Tage wegzuputzen.
So stellen wir sicher, dass
jederzeit eine hilfreiche Backup-Sammlung für den Notfall
bereit steht, ohne dass Datenmüll die Platte unnötig vollpflastert.
Fröhliches Moppen!
001 #!/usr/bin/perl 002 ################################################## 003 # mop -- Überflüssige Backup-Dateien löschen 004 # Mike Schilli, 2001 (m@perlmeister.com) 005 ################################################## 006 use 5.6.0; 007 use warnings; 008 use strict; 009 010 use File::Basename; 011 use Getopt::Std; 012 use Date::Calc qw(Today Add_Delta_Days 013 Add_Delta_YMD Day_of_Week); 014 015 my $BAK_DIR = "/data/backups"; 016 017 # Kommandozeilenoptionen einsammeln 018 my %OPTS; 019 eval { local $SIG{__WARN__} = sub {}; 020 getopts('nv', \%OPTS); 021 } or usage("Bad option"); 022 023 my %files = (); 024 my %dates_needed = (); 025 026 # Gewünschte Backup-Tage errechnen 027 calc_dates(\%dates_needed); 028 029 # Backup-Dateien analysieren 030 for my $file (<$BAK_DIR/*>) { 031 # mtime als Hash-Value speichern 032 $files{$file} = (stat $file)[9]; 033 } 034 035 # Nach letztem Modifikationszeitpunkt 036 # sortiert durchlaufen 037 for my $file (sort { $files{$a} <=> $files{$b} } 038 keys %files) { 039 my $mtime = $files{$file}; 040 my ($md, $m, $y) = (localtime($mtime))[3..5]; 041 my $date = sprintf "%d %d %d", 042 $y + 1900, $m+1, $md; 043 044 if(exists $dates_needed{$date} or 045 $date eq join ' ', Today()) { 046 print "Keeping $file\n" if $OPTS{v}; 047 # Datum entfernen -- nur erste Datei 048 # eines Tages wird aufgehoben 049 delete $dates_needed{$date}; 050 } else { 051 if($OPTS{n}) { 052 print basename($file), 053 " can go away.\n"; 054 } else { 055 unlink $file or 056 die "Cannot unlink $file ($!)"; 057 058 print basename($file), 059 " deleted.\n" if $OPTS{v}; 060 } 061 } 062 } 063 064 ################################################## 065 sub calc_dates { 066 ################################################## 067 my ($needed) = @_; 068 069 # Erwünschte Backup-Tage errechnen 070 my @today = Today(); 071 072 # Tage letzter Woche 073 for my $delta (1..7) { 074 my @date = Add_Delta_Days(@today, -$delta); 075 $needed->{"@date"} = 1; 076 } 077 078 # Die letzten 4 Montage 079 my $current_dow = Day_of_Week(@today); 080 my @last_monday; 081 if($current_dow == 1) { 082 # Heute ist Montag 083 @last_monday = Add_Delta_Days(@today, -7); 084 } else { 085 # Heute ist nicht Montag 086 @last_monday = Add_Delta_Days(@today, 087 1-$current_dow); 088 } 089 for my $weeks_back (0..3) { 090 my @date = Add_Delta_Days(@last_monday, 091 $weeks_back * -7); 092 $needed->{"@date"} = 1; 093 } 094 095 # Die letzten 3 Monatsersten 096 my @last_first; 097 if($today[2] == 1) { 098 # Heute ist der Monatserste 099 @last_first = Add_Delta_YMD(@today, 100 0, -1, 0); 101 } else { 102 # Heute ist nicht der Monatserste 103 @last_first = ($today[0], $today[1], 1); 104 } 105 for my $months_back (0..2) { 106 my @date = Add_Delta_YMD(@last_first, 0, 107 -1 * $months_back, 0); 108 $needed->{"@date"} = 1; 109 } 110 } 111 112 ################################################## 113 sub usage { 114 ################################################## 115 my ($message) = @_; 116 my $program = basename($0); 117 118 print <<EOT; 119 $program: $message 120 usage: $program [-nv] 121 -n: Don't actually delete 122 -v: Print informative messages 123 EOT 124 exit 1; 125 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |