- config and host editing pretty much done
[BackupPC.git] / lib / BackupPC / Storage / 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 2.1.0, released 20 Jun 2004.
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, $dir, $file, $contents) = @_;
200     local(*FD, *LOCK);
201     my($fileOk);
202
203     mkpath($dir, 0, 0775) if ( !-d $dir );
204     if ( open(FD, ">", "$dir/$file.new") ) {
205         binmode(FD);
206         print FD $contents;
207         close(FD);
208         #
209         # verify the file
210         #
211         if ( open(FD, "<", "$dir/$file.new") ) {
212             binmode(FD);
213             if ( join("", <FD>) ne $contents ) {
214                 return "TextFileWrite: Failed to verify $dir/$file.new";
215             } else {
216                 $fileOk = 1;
217             }
218             close(FD);
219         }
220     }
221     if ( $fileOk ) {
222         my $lock;
223         
224         if ( open(LOCK, "$dir/LOCK") || open(LOCK, ">", "$dir/LOCK") ) {
225             $lock = 1;
226             flock(LOCK, LOCK_EX);
227         }
228         if ( -s "$dir/$file" ) {
229             unlink("$dir/$file.old")               if ( -f "$dir/$file.old" );
230             rename("$dir/$file", "$dir/$file.old") if ( -f "$dir/$file" );
231         } else {
232             unlink("$dir/$file") if ( -f "$dir/$file" );
233         }
234         rename("$dir/$file.new", "$dir/$file") if ( -f "$dir/$file.new" );
235         close(LOCK) if ( $lock );
236     } else {
237         return "TextFileWrite: Failed to write $dir/$file.new";
238     }
239     return;
240 }
241
242 sub ConfigDataRead
243 {
244     my($s, $host) = @_;
245     my($ret, $mesg, $config, @configs);
246
247     #
248     # TODO: add lock
249     #
250     my $conf = {};
251
252     if ( defined($host) ) {
253         push(@configs, "$s->{TopDir}/conf/$host.pl")
254                 if ( $host ne "config" && -f "$s->{TopDir}/conf/$host.pl" );
255         push(@configs, "$s->{TopDir}/pc/$host/config.pl")
256                 if ( -f "$s->{TopDir}/pc/$host/config.pl" );
257     } else {
258         push(@configs, "$s->{TopDir}/conf/config.pl");
259     }
260     foreach $config ( @configs ) {
261         %Conf = ();
262         if ( !defined($ret = do $config) && ($! || $@) ) {
263             $mesg = "Couldn't open $config: $!" if ( $! );
264             $mesg = "Couldn't execute $config: $@" if ( $@ );
265             $mesg =~ s/[\n\r]+//;
266             return ($mesg, $conf);
267         }
268         %$conf = ( %$conf, %Conf );
269     }
270     #
271     # Promote BackupFilesOnly and BackupFilesExclude to hashes
272     #
273     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
274         next if ( !defined($conf->{$param}) || ref($conf->{$param}) eq "HASH" );
275         $conf->{$param} = [ $conf->{$param} ]
276                                 if ( ref($conf->{$param}) ne "ARRAY" );
277         $conf->{$param} = { "*" => $conf->{$param} };
278     }
279
280     return (undef, $conf);
281 }
282
283 sub ConfigDataWrite
284 {
285     my($s, $host, $newConf) = @_;
286
287     my($confDir) = $host eq "" ? "$s->{TopDir}/conf"
288                                : "$s->{TopDir}/pc/$host";
289
290     my($err, $contents) = $s->ConfigFileMerge("$confDir/config.pl", $newConf);
291     if ( defined($err) ) {
292         return $err;
293     } else {
294         #
295         # Write the file
296         #
297         return $s->TextFileWrite($confDir, "config.pl", $contents);
298     }
299 }
300
301 sub ConfigFileMerge
302 {
303     my($s, $inFile, $newConf) = @_;
304     local(*C);
305     my($contents, $out);
306     my $comment = 1;
307     my $skipVar = 0;
308     my $endLine = undef;
309     my $done = {};
310
311     if ( -f $inFile ) {
312         #
313         # Match existing settings in current config file
314         #
315         open(C, $inFile)
316             || return ("ConfigFileMerge: can't open/read $inFile", undef);
317         binmode(C);
318
319         while ( <C> ) {
320             if ( $comment && /^\s*#/ ) {
321                 $out .= $_;
322             } elsif ( /^\s*\$Conf\{([^}]*)\}\s*=/ ) {
323                 my $var = $1;
324                 if ( exists($newConf->{$var}) ) { 
325                     $contents .= $out;
326                     my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
327                     $d->Indent(1);
328                     $d->Terse(1);
329                     my $value = $d->Dump;
330                     $value =~ s/(.*)\n/$1;\n/s;
331                     $contents .= "\$Conf{$var} = " . $value;
332                     $done->{$var} = 1;
333                 }
334                 $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<(.*);/ );
335                 $endLine = $1 if ( /^\s*\$Conf\{[^}]*} *= *<<'(.*)';/ );
336                 $out = "";
337                 $skipVar = 1;
338             } elsif ( $skipVar ) {
339                 if ( !defined($endLine) && (/^\s*[\r\n]*$/ || /^\s*#/) ) {
340                     $skipVar = 0;
341                     $comment = 1;
342                     $out .= $_;
343                 }
344                 if ( defined($endLine) && /^\Q$endLine\E[\n\r]*$/ ) {
345                     $endLine = undef;
346                     $skipVar = 0;
347                     $comment = 1;
348                 }
349             } else {
350                 $out .= $_;
351             }
352         }
353         close(C);
354         $contents .= $out;
355     }
356
357     #
358     # Add new entries not matched in current config file
359     #
360     foreach my $var ( sort(keys(%$newConf)) ) {
361         next if ( $done->{$var} );
362         my $d = Data::Dumper->new([$newConf->{$var}], [*value]);
363         $d->Indent(1);
364         $d->Terse(1);
365         my $value = $d->Dump;
366         $value =~ s/(.*)\n/$1;\n/s;
367         $contents .= "\$Conf{$var} = " . $value;
368         $done->{$var} = 1;
369     }
370     return (undef, $contents);
371 }
372
373 #
374 # Return the mtime of the config file
375 #
376 sub ConfigMTime
377 {
378     my($s) = @_;
379     return (stat("$s->{TopDir}/conf/config.pl"))[9];
380 }
381
382 #
383 # Returns information from the host file in $s->{TopDir}/conf/hosts.
384 # With no argument a ref to a hash of hosts is returned.  Each
385 # hash contains fields as specified in the hosts file.  With an
386 # argument a ref to a single hash is returned with information
387 # for just that host.
388 #
389 sub HostInfoRead
390 {
391     my($s, $host) = @_;
392     my(%hosts, @hdr, @fld);
393     local(*HOST_INFO, *LOCK);
394
395     flock(LOCK, LOCK_EX) if open(LOCK, "$s->{TopDir}/pc/$host/LOCK");
396     if ( !open(HOST_INFO, "$s->{TopDir}/conf/hosts") ) {
397         print(STDERR "Can't open $s->{TopDir}/conf/hosts\n");
398         close(LOCK);
399         return {};
400     }
401     binmode(HOST_INFO);
402     while ( <HOST_INFO> ) {
403         s/[\n\r]+//;
404         s/#.*//;
405         s/\s+$//;
406         next if ( /^\s*$/ || !/^([\w\.\\-]+\s+.*)/ );
407         #
408         # Split on white space, except if preceded by \
409         # using zero-width negative look-behind assertion
410         # (always wanted to use one of those).
411         #
412         @fld = split(/(?<!\\)\s+/, $1);
413         #
414         # Remove any \
415         #
416         foreach ( @fld ) {
417             s{\\(\s)}{$1}g;
418         }
419         if ( @hdr ) {
420             if ( defined($host) ) {
421                 next if ( lc($fld[0]) ne lc($host) );
422                 @{$hosts{$fld[0]}}{@hdr} = @fld;
423                 close(HOST_INFO);
424                 close(LOCK);
425                 return \%hosts;
426             } else {
427                 @{$hosts{$fld[0]}}{@hdr} = @fld;
428             }
429         } else {
430             @hdr = @fld;
431         }
432     }
433     close(HOST_INFO);
434     close(LOCK);
435     return \%hosts;
436 }
437
438 #
439 # Writes new hosts information to the hosts file in $s->{TopDir}/conf/hosts.
440 # With no argument a ref to a hash of hosts is returned.  Each
441 # hash contains fields as specified in the hosts file.  With an
442 # argument a ref to a single hash is returned with information
443 # for just that host.
444 #
445 sub HostInfoWrite
446 {
447     my($s, $hosts) = @_;
448     my($gotHdr, @fld, $hostText, $contents);
449     local(*HOST_INFO);
450
451     if ( !open(HOST_INFO, "$s->{TopDir}/conf/hosts") ) {
452         return "Can't open $s->{TopDir}/conf/hosts";
453     }
454     foreach my $host ( keys(%$hosts) ) {
455         my $name = "$hosts->{$host}{host}";
456         my $rest = "\t$hosts->{$host}{dhcp}"
457                  . "\t$hosts->{$host}{user}"
458                  . "\t$hosts->{$host}{moreUsers}";
459         $name =~ s/ /\\ /g;
460         $rest =~ s/ //g;
461         $hostText->{$host} = $name . $rest;
462     }
463     binmode(HOST_INFO);
464     while ( <HOST_INFO> ) {
465         s/[\n\r]+//;
466         if ( /^\s*$/ || /^\s*#/ ) {
467             $contents .= $_ . "\n";
468             next;
469         }
470         if ( !$gotHdr ) {
471             $contents .= $_ . "\n";
472             $gotHdr = 1;
473             next;
474         }
475         @fld = split(/(?<!\\)\s+/, $1);
476         #
477         # Remove any \
478         #
479         foreach ( @fld ) {
480             s{\\(\s)}{$1}g;
481         }
482         if ( defined($hostText->{$fld[0]}) ) {
483             $contents .= $hostText->{$fld[0]} . "\n";
484             delete($hostText->{$fld[0]});
485         }
486     }
487     foreach my $host ( sort(keys(%$hostText)) ) {
488         $contents .= $hostText->{$host} . "\n";
489         delete($hostText->{$host});
490     }
491     close(HOST_INFO);
492
493     #
494     # Write and verify the new host file
495     #
496     return $s->TextFileWrite("$s->{TopDir}/conf", "hosts", $contents);
497 }
498
499 #
500 # Return the mtime of the hosts file
501 #
502 sub HostsMTime
503 {
504     my($s) = @_;
505     return (stat("$s->{TopDir}/conf/hosts"))[9];
506 }
507
508 1;