1 #============================================================= -*-perl-*-
3 # BackupPC::Storage::Text package
7 # This library defines a BackupPC::Storage::Text class that implements
8 # BackupPC's persistent state storage (config, host info, backup
9 # and restore info) using text files.
12 # Craig Barratt <cbarratt@users.sourceforge.net>
15 # Copyright (C) 2004 Craig Barratt
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
31 #========================================================================
33 # Version 3.0.0, released 28 Jan 2007.
35 # See http://backuppc.sourceforge.net.
37 #========================================================================
39 package BackupPC::Storage::Text;
50 my($flds, $paths) = @_;
64 foreach my $v ( keys(%$paths) ) {
65 $class->{$v} = $paths->{$v};
72 local(*BK_INFO, *LOCK);
75 flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
76 if ( open(BK_INFO, "$s->{TopDir}/pc/$host/backups") ) {
80 next if ( !/^(\d+\t(incr|full|partial).*)/ );
82 @{$Backups[@Backups]}{@{$s->{BackupFields}}} = split(/\t/);
88 # Default the level and version fields if not present
90 for ( my $i = 0 ; $i < @Backups ; $i++ ) {
91 if ( defined($Backups[$i]{level}) ) {
92 if ( !defined($Backups[$i]{version}) ) {
93 $Backups[$i]{version} = "3.0.0";
96 $Backups[$i]{level} = $Backups[$i]{type} eq "incr" ? 1 : 0;
97 $Backups[$i]{version} = "2.1.2";
105 my($s, $host, @Backups) = @_;
106 my($i, $contents, $fileOk);
109 # Generate the file contents
111 for ( $i = 0 ; $i < @Backups ; $i++ ) {
112 my %b = %{$Backups[$i]};
113 $contents .= join("\t", @b{@{$s->{BackupFields}}}) . "\n";
119 return $s->TextFileWrite("$s->{TopDir}/pc/$host/backups", $contents);
125 local(*RESTORE_INFO, *LOCK);
128 flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
129 if ( open(RESTORE_INFO, "$s->{TopDir}/pc/$host/restores") ) {
130 binmode(RESTORE_INFO);
131 while ( <RESTORE_INFO> ) {
133 next if ( !/^(\d+.*)/ );
135 @{$Restores[@Restores]}{@{$s->{RestoreFields}}} = split(/\t/);
145 my($s, $host, @Restores) = @_;
146 local(*RESTORE_INFO, *LOCK);
147 my($i, $contents, $fileOk);
150 # Generate the file contents
152 for ( $i = 0 ; $i < @Restores ; $i++ ) {
153 my %b = %{$Restores[$i]};
154 $contents .= join("\t", @b{@{$s->{RestoreFields}}}) . "\n";
160 return $s->TextFileWrite("$s->{TopDir}/pc/$host/restores", $contents);
166 local(*ARCHIVE_INFO, *LOCK);
169 flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
170 if ( open(ARCHIVE_INFO, "$s->{TopDir}/pc/$host/archives") ) {
171 binmode(ARCHIVE_INFO);
172 while ( <ARCHIVE_INFO> ) {
174 next if ( !/^(\d+.*)/ );
176 @{$Archives[@Archives]}{@{$s->{ArchiveFields}}} = split(/\t/);
186 my($s, $host, @Archives) = @_;
187 local(*ARCHIVE_INFO, *LOCK);
188 my($i, $contents, $fileOk);
191 # Generate the file contents
193 for ( $i = 0 ; $i < @Archives ; $i++ ) {
194 my %b = %{$Archives[$i]};
195 $contents .= join("\t", @b{@{$s->{ArchiveFields}}}) . "\n";
201 return $s->TextFileWrite("$s->{TopDir}/pc/$host/archives", $contents);
205 # Write a text file as safely as possible. We write to
206 # a new file, verify the file, and the rename the file.
207 # The previous version of the file is renamed with a
212 my($s, $file, $contents) = @_;
216 (my $dir = $file) =~ s{(.+)/(.+)}{$1};
218 mkpath($dir, 0, 0775) if ( !-d $dir );
219 if ( open(FD, ">", "$file.new") ) {
226 if ( open(FD, "<", "$file.new") ) {
228 if ( join("", <FD>) ne $contents ) {
229 return "TextFileWrite: Failed to verify $file.new";
239 if ( open(LOCK, "$dir/LOCK") || open(LOCK, ">", "$dir/LOCK") ) {
241 flock(LOCK, LOCK_EX);
244 unlink("$file.old") if ( -f "$file.old" );
245 rename("$file", "$file.old") if ( -f "$file" );
247 unlink("$file") if ( -f "$file" );
249 rename("$file.new", "$file") if ( -f "$file.new" );
250 close(LOCK) if ( $lock );
252 return "TextFileWrite: Failed to write $file.new";
261 return "$s->{ConfDir}/config.pl" if ( !defined($host) );
262 if ( $s->{useFHS} ) {
263 return "$s->{ConfDir}/pc/$host.pl";
265 return "$s->{TopDir}/pc/$host/config.pl"
266 if ( -f "$s->{TopDir}/pc/$host/config.pl" );
267 return "$s->{ConfDir}/$host.pl"
268 if ( $host ne "config" && -f "$s->{ConfDir}/$host.pl" );
269 return "$s->{ConfDir}/pc/$host.pl";
276 my($ret, $mesg, $config, @configs);
282 my $configPath = $s->ConfigPath($host);
284 push(@configs, $configPath) if ( -f $configPath );
285 foreach $config ( @configs ) {
287 if ( !defined($ret = do $config) && ($! || $@) ) {
288 $mesg = "Couldn't open $config: $!" if ( $! );
289 $mesg = "Couldn't execute $config: $@" if ( $@ );
290 $mesg =~ s/[\n\r]+//;
291 return ($mesg, $conf);
293 %$conf = ( %$conf, %Conf );
297 # Promote BackupFilesOnly and BackupFilesExclude to hashes
299 foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
300 next if ( !defined($conf->{$param}) || ref($conf->{$param}) eq "HASH" );
301 $conf->{$param} = [ $conf->{$param} ]
302 if ( ref($conf->{$param}) ne "ARRAY" );
303 $conf->{$param} = { "*" => $conf->{$param} };
307 # Handle backward compatibility with defunct BlackoutHourBegin,
308 # BlackoutHourEnd, and BlackoutWeekDays parameters.
310 if ( defined($conf->{BlackoutHourBegin}) ) {
311 push(@{$conf->{BlackoutPeriods}},
313 hourBegin => $conf->{BlackoutHourBegin},
314 hourEnd => $conf->{BlackoutHourEnd},
315 weekDays => $conf->{BlackoutWeekDays},
318 delete($conf->{BlackoutHourBegin});
319 delete($conf->{BlackoutHourEnd});
320 delete($conf->{BlackoutWeekDays});
324 # Make sure IncrLevels is defined
326 $conf->{IncrLevels} = [1] if ( !defined($conf->{IncrLevels}) );
328 return (undef, $conf);
333 my($s, $host, $newConf) = @_;
335 my $configPath = $s->ConfigPath($host);
337 my($err, $contents) = $s->ConfigFileMerge("$configPath", $newConf);
338 if ( defined($err) ) {
344 return $s->TextFileWrite($configPath, $contents);
350 my($s, $inFile, $newConf) = @_;
352 my($contents, $skipExpr, $fakeVar);
357 # Match existing settings in current config file
360 || return ("ConfigFileMerge: can't open/read $inFile", undef);
364 if ( /^\s*\$Conf\{([^}]*)\}\s*=(.*)/ ) {
366 $skipExpr = "\$fakeVar = $2\n";
367 if ( exists($newConf->{$var}) ) {
368 my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
371 my $value = $d->Dump;
372 $value =~ s/(.*)\n/$1;\n/s;
373 $contents .= "\$Conf{$var} = " . $value;
376 } elsif ( defined($skipExpr) ) {
381 if ( defined($skipExpr)
382 && ($skipExpr =~ /^\$fakeVar = *<</
383 || $skipExpr =~ /;[\n\r]*$/) ) {
385 # if we have a complete expression, then we are done
386 # skipping text from the original config file.
388 $skipExpr = $1 if ( $skipExpr =~ /(.*)/s );
390 $skipExpr = undef if ( $@ eq "" );
397 # Add new entries not matched in current config file
399 foreach my $var ( sort(keys(%$newConf)) ) {
400 next if ( $done->{$var} );
401 my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
404 my $value = $d->Dump;
405 $value =~ s/(.*)\n/$1;\n/s;
406 $contents .= "\$Conf{$var} = " . $value;
409 return (undef, $contents);
413 # Return the mtime of the config file
418 return (stat($s->ConfigPath()))[9];
422 # Returns information from the host file in $s->{ConfDir}/hosts.
423 # With no argument a ref to a hash of hosts is returned. Each
424 # hash contains fields as specified in the hosts file. With an
425 # argument a ref to a single hash is returned with information
426 # for just that host.
431 my(%hosts, @hdr, @fld);
432 local(*HOST_INFO, *LOCK);
434 flock(LOCK, LOCK_EX) if open(LOCK, "$s->{ConfDir}/LOCK");
435 if ( !open(HOST_INFO, "$s->{ConfDir}/hosts") ) {
436 print(STDERR "Can't open $s->{ConfDir}/hosts\n");
441 while ( <HOST_INFO> ) {
445 next if ( /^\s*$/ || !/^([\w\.\\-]+\s+.*)/ );
447 # Split on white space, except if preceded by \
448 # using zero-width negative look-behind assertion
449 # (always wanted to use one of those).
451 @fld = split(/(?<!\\)\s+/, $1);
459 if ( defined($host) ) {
460 next if ( lc($fld[0]) ne lc($host) );
461 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
466 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
478 # Writes new hosts information to the hosts file in $s->{ConfDir}/hosts.
479 # With no argument a ref to a hash of hosts is returned. Each
480 # hash contains fields as specified in the hosts file. With an
481 # argument a ref to a single hash is returned with information
482 # for just that host.
487 my($gotHdr, @fld, $hostText, $contents);
490 if ( !open(HOST_INFO, "$s->{ConfDir}/hosts") ) {
491 return "Can't open $s->{ConfDir}/hosts";
493 foreach my $host ( keys(%$hosts) ) {
494 my $name = "$hosts->{$host}{host}";
495 my $rest = "\t$hosts->{$host}{dhcp}"
496 . "\t$hosts->{$host}{user}"
497 . "\t$hosts->{$host}{moreUsers}";
500 $hostText->{$host} = $name . $rest;
503 while ( <HOST_INFO> ) {
505 if ( /^\s*$/ || /^\s*#/ ) {
506 $contents .= $_ . "\n";
510 $contents .= $_ . "\n";
514 @fld = split(/(?<!\\)\s+/, $1);
521 if ( defined($hostText->{$fld[0]}) ) {
522 $contents .= $hostText->{$fld[0]} . "\n";
523 delete($hostText->{$fld[0]});
526 foreach my $host ( sort(keys(%$hostText)) ) {
527 $contents .= $hostText->{$host} . "\n";
528 delete($hostText->{$host});
533 # Write and verify the new host file
535 return $s->TextFileWrite("$s->{ConfDir}/hosts", $contents);
539 # Return the mtime of the hosts file
544 return (stat("$s->{ConfDir}/hosts"))[9];