4dea60edee23b33940819e4001cc413e15475331
[BackupPC.git] / Text.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::Storage::Text package
4 #
5 # DESCRIPTION
6 #
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.
10 #
11 # AUTHOR
12 #   Craig Barratt  <cbarratt@users.sourceforge.net>
13 #
14 # COPYRIGHT
15 #   Copyright (C) 2004  Craig Barratt
16 #
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.
21 #
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.
26 #
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
30 #
31 #========================================================================
32 #
33 # Version 3.0.0, released 28 Jan 2007.
34 #
35 # See http://backuppc.sourceforge.net.
36 #
37 #========================================================================
38
39 package BackupPC::Storage::Text;
40
41 use strict;
42 use vars qw(%Conf);
43 use Data::Dumper;
44 use File::Path;
45 use Fcntl qw/:flock/;
46
47 sub new
48 {
49     my $class = shift;
50     my($flds, $paths) = @_;
51
52     my $s = bless {
53         %$flds,
54         %$paths,
55     }, $class;
56     return $s;
57 }
58
59 sub setPaths
60 {
61     my $class = shift;
62     my($paths) = @_;
63
64     foreach my $v ( keys(%$paths) ) {
65         $class->{$v} = $paths->{$v};
66     }
67 }
68
69 sub BackupInfoRead
70 {
71     my($s, $host) = @_;
72     local(*BK_INFO, *LOCK);
73     my(@Backups);
74
75     flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
76     if ( open(BK_INFO, "$s->{TopDir}/pc/$host/backups") ) {
77         binmode(BK_INFO);
78         while ( <BK_INFO> ) {
79             s/[\n\r]+//;
80             next if ( !/^(\d+\t(incr|full|partial).*)/ );
81             $_ = $1;
82             @{$Backups[@Backups]}{@{$s->{BackupFields}}} = split(/\t/);
83         }
84         close(BK_INFO);
85     }
86     close(LOCK);
87     return @Backups;
88 }
89
90 sub BackupInfoWrite
91 {
92     my($s, $host, @Backups) = @_;
93     my($i, $contents, $fileOk);
94
95     #
96     # Generate the file contents
97     #
98     for ( $i = 0 ; $i < @Backups ; $i++ ) {
99         my %b = %{$Backups[$i]};
100         $contents .= join("\t", @b{@{$s->{BackupFields}}}) . "\n";
101     }
102     
103     #
104     # Write the file
105     #
106     return $s->TextFileWrite("$s->{TopDir}/pc/$host/backups", $contents);
107 }
108
109 sub RestoreInfoRead
110 {
111     my($s, $host) = @_;
112     local(*RESTORE_INFO, *LOCK);
113     my(@Restores);
114
115     flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
116     if ( open(RESTORE_INFO, "$s->{TopDir}/pc/$host/restores") ) {
117         binmode(RESTORE_INFO);
118         while ( <RESTORE_INFO> ) {
119             s/[\n\r]+//;
120             next if ( !/^(\d+.*)/ );
121             $_ = $1;
122             @{$Restores[@Restores]}{@{$s->{RestoreFields}}} = split(/\t/);
123         }
124         close(RESTORE_INFO);
125     }
126     close(LOCK);
127     return @Restores;
128 }
129
130 sub RestoreInfoWrite
131 {
132     my($s, $host, @Restores) = @_;
133     local(*RESTORE_INFO, *LOCK);
134     my($i, $contents, $fileOk);
135
136     #
137     # Generate the file contents
138     #
139     for ( $i = 0 ; $i < @Restores ; $i++ ) {
140         my %b = %{$Restores[$i]};
141         $contents .= join("\t", @b{@{$s->{RestoreFields}}}) . "\n";
142     }
143
144     #
145     # Write the file
146     #
147     return $s->TextFileWrite("$s->{TopDir}/pc/$host/restores", $contents);
148 }
149
150 sub ArchiveInfoRead
151 {
152     my($s, $host) = @_;
153     local(*ARCHIVE_INFO, *LOCK);
154     my(@Archives);
155
156     flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
157     if ( open(ARCHIVE_INFO, "$s->{TopDir}/pc/$host/archives") ) {
158         binmode(ARCHIVE_INFO);
159         while ( <ARCHIVE_INFO> ) {
160             s/[\n\r]+//;
161             next if ( !/^(\d+.*)/ );
162             $_ = $1;
163             @{$Archives[@Archives]}{@{$s->{ArchiveFields}}} = split(/\t/);
164         }
165         close(ARCHIVE_INFO);
166     }
167     close(LOCK);
168     return @Archives;
169 }
170
171 sub ArchiveInfoWrite
172 {
173     my($s, $host, @Archives) = @_;
174     local(*ARCHIVE_INFO, *LOCK);
175     my($i, $contents, $fileOk);
176
177     #
178     # Generate the file contents
179     #
180     for ( $i = 0 ; $i < @Archives ; $i++ ) {
181         my %b = %{$Archives[$i]};
182         $contents .= join("\t", @b{@{$s->{ArchiveFields}}}) . "\n";
183     }
184
185     #
186     # Write the file
187     #
188     return $s->TextFileWrite("$s->{TopDir}/pc/$host/archives", $contents);
189 }
190
191 #
192 # Write a text file as safely as possible.  We write to
193 # a new file, verify the file, and the rename the file.
194 # The previous version of the file is renamed with a
195 # .old extension.
196 #
197 sub TextFileWrite
198 {
199     my($s, $file, $contents) = @_;
200     local(*FD, *LOCK);
201     my($fileOk);
202
203     (my $dir = $file) =~ s{(.+)/(.+)}{$1};
204
205     mkpath($dir, 0, 0775) if ( !-d $dir );
206     if ( open(FD, ">", "$file.new") ) {
207         binmode(FD);
208         print FD $contents;
209         close(FD);
210         #
211         # verify the file
212         #
213         if ( open(FD, "<", "$file.new") ) {
214             binmode(FD);
215             if ( join("", <FD>) ne $contents ) {
216                 return "TextFileWrite: Failed to verify $file.new";
217             } else {
218                 $fileOk = 1;
219             }
220             close(FD);
221         }
222     }
223     if ( $fileOk ) {
224         my $lock;
225         
226         if ( open(LOCK, "$dir/LOCK") || open(LOCK, ">", "$dir/LOCK") ) {
227             $lock = 1;
228             flock(LOCK, LOCK_EX);
229         }
230         if ( -s "$file" ) {
231             unlink("$file.old")           if ( -f "$file.old" );
232             rename("$file", "$file.old")  if ( -f "$file" );
233         } else {
234             unlink("$file") if ( -f "$file" );
235         }
236         rename("$file.new", "$file") if ( -f "$file.new" );
237         close(LOCK) if ( $lock );
238     } else {
239         return "TextFileWrite: Failed to write $file.new";
240     }
241     return;
242 }
243
244 sub ConfigPath
245 {
246     my($s, $host) = @_;
247
248     return "$s->{ConfDir}/config.pl" if ( !defined($host) );
249     if ( $s->{useFHS} ) {
250         return "$s->{ConfDir}/pc/$host.pl";
251     } else {
252         return "$s->{TopDir}/pc/$host/config.pl"
253             if ( -f "$s->{TopDir}/pc/$host/config.pl" );
254         return "$s->{ConfDir}/$host.pl"
255             if ( $host ne "config" && -f "$s->{ConfDir}/$host.pl" );
256         return "$s->{ConfDir}/pc/$host.pl";
257     }
258 }
259
260 sub ConfigDataRead
261 {
262     my($s, $host) = @_;
263     my($ret, $mesg, $config, @configs);
264
265     #
266     # TODO: add lock
267     #
268     my $conf = {};
269     my $configPath = $s->ConfigPath($host);
270
271     push(@configs, $configPath) if ( -f $configPath );
272     foreach $config ( @configs ) {
273         %Conf = ();
274         if ( !defined($ret = do $config) && ($! || $@) ) {
275             $mesg = "Couldn't open $config: $!" if ( $! );
276             $mesg = "Couldn't execute $config: $@" if ( $@ );
277             $mesg =~ s/[\n\r]+//;
278             return ($mesg, $conf);
279         }
280         %$conf = ( %$conf, %Conf );
281     }
282     #
283     # Promote BackupFilesOnly and BackupFilesExclude to hashes
284     #
285     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
286         next if ( !defined($conf->{$param}) || ref($conf->{$param}) eq "HASH" );
287         $conf->{$param} = [ $conf->{$param} ]
288                                 if ( ref($conf->{$param}) ne "ARRAY" );
289         $conf->{$param} = { "*" => $conf->{$param} };
290     }
291
292     #
293     # Handle backward compatibility with defunct BlackoutHourBegin,
294     # BlackoutHourEnd, and BlackoutWeekDays parameters.
295     #
296     if ( defined($conf->{BlackoutHourBegin}) ) {
297         push(@{$conf->{BlackoutPeriods}},
298              {
299                  hourBegin => $conf->{BlackoutHourBegin},
300                  hourEnd   => $conf->{BlackoutHourEnd},
301                  weekDays  => $conf->{BlackoutWeekDays},
302              }
303         );
304         delete($conf->{BlackoutHourBegin});
305         delete($conf->{BlackoutHourEnd});
306         delete($conf->{BlackoutWeekDays});
307     }
308
309     return (undef, $conf);
310 }
311
312 sub ConfigDataWrite
313 {
314     my($s, $host, $newConf) = @_;
315
316     my $configPath = $s->ConfigPath($host);
317
318     my($err, $contents) = $s->ConfigFileMerge("$configPath", $newConf);
319     if ( defined($err) ) {
320         return $err;
321     } else {
322         #
323         # Write the file
324         #
325         return $s->TextFileWrite($configPath, $contents);
326     }
327 }
328
329 sub ConfigFileMerge
330 {
331     my($s, $inFile, $newConf) = @_;
332     local(*C);
333     my($contents, $skipExpr, $fakeVar);
334     my $done = {};
335
336     if ( -f $inFile ) {
337         #
338         # Match existing settings in current config file
339         #
340         open(C, $inFile)
341             || return ("ConfigFileMerge: can't open/read $inFile", undef);
342         binmode(C);
343
344         while ( <C> ) {
345             if ( /^\s*\$Conf\{([^}]*)\}\s*=(.*)/ ) {
346                 my $var = $1;
347                 $skipExpr = "\$fakeVar = $2\n";
348                 if ( exists($newConf->{$var}) ) {
349                     my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
350                     $d->Indent(1);
351                     $d->Terse(1);
352                     my $value = $d->Dump;
353                     $value =~ s/(.*)\n/$1;\n/s;
354                     $contents .= "\$Conf{$var} = " . $value;
355                     $done->{$var} = 1;
356                 }
357             } elsif ( defined($skipExpr) ) {
358                 $skipExpr .= $_;
359             } else {
360                 $contents .= $_;
361             }
362             if ( defined($skipExpr)
363                     && ($skipExpr =~ /^\$fakeVar = *<</
364                         || $skipExpr =~ /;[\n\r]*$/) ) {
365                 #
366                 # if we have a complete expression, then we are done
367                 # skipping text from the original config file.
368                 #
369                 $skipExpr = $1 if ( $skipExpr =~ /(.*)/s );
370                 eval($skipExpr);
371                 $skipExpr = undef if ( $@ eq "" );
372             }
373         }
374         close(C);
375     }
376
377     #
378     # Add new entries not matched in current config file
379     #
380     foreach my $var ( sort(keys(%$newConf)) ) {
381         next if ( $done->{$var} );
382         my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
383         $d->Indent(1);
384         $d->Terse(1);
385         my $value = $d->Dump;
386         $value =~ s/(.*)\n/$1;\n/s;
387         $contents .= "\$Conf{$var} = " . $value;
388         $done->{$var} = 1;
389     }
390     return (undef, $contents);
391 }
392
393 #
394 # Return the mtime of the config file
395 #
396 sub ConfigMTime
397 {
398     my($s) = @_;
399     return (stat($s->ConfigPath()))[9];
400 }
401
402 #
403 # Returns information from the host file in $s->{ConfDir}/hosts.
404 # With no argument a ref to a hash of hosts is returned.  Each
405 # hash contains fields as specified in the hosts file.  With an
406 # argument a ref to a single hash is returned with information
407 # for just that host.
408 #
409 sub HostInfoRead
410 {
411     my($s, $host) = @_;
412     my(%hosts, @hdr, @fld);
413     local(*HOST_INFO, *LOCK);
414
415     flock(LOCK, LOCK_EX) if open(LOCK, "$s->{ConfDir}/LOCK");
416     if ( !open(HOST_INFO, "$s->{ConfDir}/hosts") ) {
417         print(STDERR "Can't open $s->{ConfDir}/hosts\n");
418         close(LOCK);
419         return {};
420     }
421     binmode(HOST_INFO);
422     while ( <HOST_INFO> ) {
423         s/[\n\r]+//;
424         s/#.*//;
425         s/\s+$//;
426         next if ( /^\s*$/ || !/^([\w\.\\-]+\s+.*)/ );
427         #
428         # Split on white space, except if preceded by \
429         # using zero-width negative look-behind assertion
430         # (always wanted to use one of those).
431         #
432         @fld = split(/(?<!\\)\s+/, $1);
433         #
434         # Remove any \
435         #
436         foreach ( @fld ) {
437             s{\\(\s)}{$1}g;
438         }
439         if ( @hdr ) {
440             if ( defined($host) ) {
441                 next if ( lc($fld[0]) ne lc($host) );
442                 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
443                 close(HOST_INFO);
444                 close(LOCK);
445                 return \%hosts;
446             } else {
447                 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
448             }
449         } else {
450             @hdr = @fld;
451         }
452     }
453     close(HOST_INFO);
454     close(LOCK);
455     return \%hosts;
456 }
457
458 #
459 # Writes new hosts information to the hosts file in $s->{ConfDir}/hosts.
460 # With no argument a ref to a hash of hosts is returned.  Each
461 # hash contains fields as specified in the hosts file.  With an
462 # argument a ref to a single hash is returned with information
463 # for just that host.
464 #
465 sub HostInfoWrite
466 {
467     my($s, $hosts) = @_;
468     my($gotHdr, @fld, $hostText, $contents);
469     local(*HOST_INFO);
470
471     if ( !open(HOST_INFO, "$s->{ConfDir}/hosts") ) {
472         return "Can't open $s->{ConfDir}/hosts";
473     }
474     foreach my $host ( keys(%$hosts) ) {
475         my $name = "$hosts->{$host}{host}";
476         my $rest = "\t$hosts->{$host}{dhcp}"
477                  . "\t$hosts->{$host}{user}"
478                  . "\t$hosts->{$host}{moreUsers}";
479         $name =~ s/ /\\ /g;
480         $rest =~ s/ //g;
481         $hostText->{$host} = $name . $rest;
482     }
483     binmode(HOST_INFO);
484     while ( <HOST_INFO> ) {
485         s/[\n\r]+//;
486         if ( /^\s*$/ || /^\s*#/ ) {
487             $contents .= $_ . "\n";
488             next;
489         }
490         if ( !$gotHdr ) {
491             $contents .= $_ . "\n";
492             $gotHdr = 1;
493             next;
494         }
495         @fld = split(/(?<!\\)\s+/, $1);
496         #
497         # Remove any \
498         #
499         foreach ( @fld ) {
500             s{\\(\s)}{$1}g;
501         }
502         if ( defined($hostText->{$fld[0]}) ) {
503             $contents .= $hostText->{$fld[0]} . "\n";
504             delete($hostText->{$fld[0]});
505         }
506     }
507     foreach my $host ( sort(keys(%$hostText)) ) {
508         $contents .= $hostText->{$host} . "\n";
509         delete($hostText->{$host});
510     }
511     close(HOST_INFO);
512
513     #
514     # Write and verify the new host file
515     #
516     return $s->TextFileWrite("$s->{ConfDir}/hosts", $contents);
517 }
518
519 #
520 # Return the mtime of the hosts file
521 #
522 sub HostsMTime
523 {
524     my($s) = @_;
525     return (stat("$s->{ConfDir}/hosts"))[9];
526 }
527
528 1;