* Major changes from Ryan Kucera to add style sheets to the CGI
[BackupPC.git] / bin / BackupPC_archive
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_archive: Archive files for an archive client.
5 #
6 # DESCRIPTION
7 #
8 #   Usage: BackupPC_archive <user> <archiveclient> <reqFileName>
9 #
10 # AUTHOR
11 #   Craig Barratt  <cbarratt@users.sourceforge.net>
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2001-2003  Craig Barratt
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 2.1.0_CVS, released 3 Jul 2003.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 use strict;
39 no  utf8;
40 use lib "/usr/local/BackupPC/lib";
41 use BackupPC::Lib;
42 use BackupPC::FileZIO;
43 use BackupPC::Xfer::Archive;
44
45 use vars qw( %ArchiveReq );
46
47 ###########################################################################
48 # Initialize
49 ###########################################################################
50
51 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
52 my $TopDir = $bpc->TopDir();
53 my $BinDir = $bpc->BinDir();
54 my %Conf   = $bpc->Conf();
55 my $NeedPostCmd;
56
57 my($user, $host, $client, $reqFileName, %stat);
58
59 $bpc->ChildInit();
60
61 if ( @ARGV != 3 ) {
62     print("usage: $0 <user> <archiveclient> <reqFileName>\n");
63     exit(1);
64 }
65 $user = $1 if ( $ARGV[0] =~ /(.+)/ );
66 $client = $1 if ( $ARGV[1] =~ /(.+)/ );
67 if ( $ARGV[2] !~ /^([\w.]+)$/ ) {
68     print("$0: bad reqFileName (arg #3): $ARGV[2]\n");
69     exit(1);
70 }
71 $reqFileName = $1;
72
73 my $startTime = time();
74
75 #my $Hosts = $bpc->HostInfoRead($client);
76
77 my $Dir     = "$TopDir/pc/$client";
78 my @xferPid = ();
79
80 #
81 # Catch various signals
82 #
83 $SIG{INT}  = \&catch_signal;
84 $SIG{ALRM} = \&catch_signal;
85 $SIG{TERM} = \&catch_signal;
86 $SIG{PIPE} = \&catch_signal;
87 $SIG{STOP} = \&catch_signal;
88 $SIG{TSTP} = \&catch_signal;
89 $SIG{TTIN} = \&catch_signal;
90 my $Pid = $$;
91
92 mkpath($Dir, 0, 0777) if ( !-d $Dir );
93 if ( !-f "$Dir/LOCK" ) {
94     open(LOCK, ">", "$Dir/LOCK") && close(LOCK);
95 }
96 open(LOG, ">>", "$Dir/LOG");
97 select(LOG); $| = 1; select(STDOUT);
98
99
100 #
101 # Read the request file
102 #
103
104 if ( !(my $ret = do "$Dir/$reqFileName") ) {
105     my $err;
106     if ( $@ ) {
107         $err = "couldn't parse $Dir/$reqFileName: $@";
108     } elsif ( !defined($ret) ) {
109         $err = "couldn't do $Dir/$reqFileName: $!";
110     } else {
111         $err = "couldn't run $Dir/$reqFileName";
112     }
113     $stat{hostError} = $err;
114     exit(ArchiveCleanup($client));
115 }
116 #
117 # Re-read config file, so we can include the PC-specific config
118 #
119 if ( defined(my $error = $bpc->ConfigRead($client)) ) {
120     $stat{hostError} = "Can't read PC's config file: $error";
121     exit(ArchiveCleanup($client));
122 }
123 %Conf = $bpc->Conf();
124
125 #
126 # Make sure we eventually timeout if there is no activity from
127 # the data transport program.
128 #
129 alarm($Conf{ClientTimeout});
130
131 #
132 # See if the host name is aliased
133 #
134 if ( $Conf{ClientNameAlias} ne "" ) {
135     $host = $Conf{ClientNameAlias};
136 } else {
137     $host = $client;
138 }
139
140 #
141 # Setup file extension for compression and open ArchiveLOG output file
142 #
143 $Conf{CompressLevel} = 0 if ( !BackupPC::FileZIO->compOk );
144 my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
145 my $ArchiveLOG = BackupPC::FileZIO->open("$Dir/ArchiveLOG$fileExt", 1,
146                                      $Conf{CompressLevel});
147 my($logMsg, $xfer);
148
149 $stat{xferOK} = 1;
150 $stat{hostAbort} = undef;
151 $stat{hostError} = $stat{lastOutputLine} = undef;
152 local(*RH, *WH);
153
154 #
155 # Run an optional pre-archive command
156 #
157 UserCommandRun("ArchivePreUserCmd");
158 $NeedPostCmd = 1;
159
160 $xfer = BackupPC::Xfer::Archive->new($bpc);
161
162 #
163 # Run the transport program
164 #
165
166 my $xferArgs = {
167     client       => $client,
168     host         => $host,
169     user         => $ArchiveReq{user},
170     type         => "archive",
171     XferLOG      => $ArchiveLOG,
172     XferMethod   => $Conf{XferMethod},
173     pathHdrSrc   => $ArchiveReq{pathHdrSrc},
174     pathHdrDest  => $ArchiveReq{pathHdrDest},
175     HostList     => \@{$ArchiveReq{HostList}},
176     BackupList   => \@{$ArchiveReq{BackupList}},
177     archiveloc   => $ArchiveReq{archiveloc},
178     parfile      => $ArchiveReq{parfile},
179     compression  => $ArchiveReq{compression},
180     compext      => $ArchiveReq{compext},
181     splitsize    => $ArchiveReq{splitsize},
182     pidHandler   => \&pidHandler,
183 };
184
185 $xfer->args($xferArgs);
186
187 if ( !defined($logMsg = $xfer->start()) ) {
188     UserCommandRun("ArchivePostUserCmd") if ( $NeedPostCmd );
189     $stat{hostError} = "xfer start failed: ", $xfer->errStr;
190     exit(ArchiveCleanup($client));
191 }
192
193 print(LOG $bpc->timeStamp, "Starting archive\n");
194 $xfer->run();
195 alarm(0);
196
197 exit(ArchiveCleanup($client));
198
199 ###########################################################################
200 # Subroutines
201 ###########################################################################
202
203 sub catch_signal
204 {
205     my $signame = shift;
206
207     #
208     # Children quit quietly on ALRM
209     #
210     exit(1) if ( $Pid != $$ && $signame eq "ALRM" );
211
212     #
213     # Ignore signals in children
214     #
215     return if ( $Pid != $$ );
216
217     #
218     # Note: needs to be tested for each kind of XferMethod
219     #
220     print(LOG $bpc->timeStamp, "cleaning up after signal $signame\n");
221     $SIG{$signame} = 'IGNORE';
222     $ArchiveLOG->write(\"exiting after signal $signame\n");
223     $stat{xferOK} = 0;
224     if ( $signame eq "INT" ) {
225         $stat{hostError} = "aborted by user (signal=$signame)";
226     } else {
227         $stat{hostError} = "aborted by signal=$signame";
228     }
229     exit(ArchiveCleanup($client));
230 }
231
232 #
233 # Cleanup and update the archive status
234 #
235 sub ArchiveCleanup
236 {
237     my($client) = @_;
238
239     $stat{xferOK} = 0 if ( $stat{hostError} || $stat{hostAbort} );
240
241     if ( !$stat{xferOK} ) {
242         #
243         # kill off the tranfer program, first nicely then forcefully
244         #
245         if ( @xferPid ) {
246             kill(2, @xferPid);
247             sleep(1);
248             kill(9, @xferPid);
249         }
250     }
251
252     my $lastNum  = -1;
253     my @Archives;
254
255     @Archives = $bpc->ArchiveInfoRead($client);
256     for ( my $i = 0 ; $i < @Archives ; $i++ ) {
257         $lastNum = $Archives[$i]{num} if ( $lastNum < $Archives[$i]{num} );
258     }
259     $lastNum++;
260
261     #
262     # Run an optional post-archive command
263     #
264     UserCommandRun("ArchivePostUserCmd") if ( $NeedPostCmd );
265
266     rename("$Dir/ArchiveLOG$fileExt", "$Dir/ArchiveLOG.$lastNum$fileExt");
267     rename("$Dir/$reqFileName", "$Dir/ArchiveInfo.$lastNum");
268     my $endTime = time();
269
270     #
271     # If the archive failed, clean up
272     #
273     if ( !$stat{xferOK} ) {
274         #
275         # wait a short while and see if the system is still alive
276         #
277         $stat{hostError} = $stat{lastOutputLine} if ( $stat{hostError} eq "" );
278         sleep(2);
279         $stat{hostAbort} = 1;
280         $ArchiveLOG->write(\"Archive failed: $stat{hostError}\n")
281                                             if ( defined($ArchiveLOG) );
282     }
283
284     $ArchiveLOG->close() if ( defined($ArchiveLOG) );
285
286     #
287     # Add the new archive information to the archive file
288     #
289     @Archives = $bpc->ArchiveInfoRead($client);
290     my $i = @Archives;
291     $Archives[$i]{num}           = $lastNum;
292     $Archives[$i]{startTime}     = $startTime;
293     $Archives[$i]{endTime}       = $endTime;
294     $Archives[$i]{result}        = $stat{xferOK} ? "ok" : "failed";
295     $Archives[$i]{errorMsg}      = $stat{hostError};
296
297     while ( @Archives > $Conf{ArchiveInfoKeepCnt} ) {
298         my $num = $Archives[0]{num};
299         unlink("$Dir/ArchiveLOG.$num.z");
300         unlink("$Dir/ArchiveLOG.$num");
301         unlink("$Dir/ArchiveInfo.$num");
302         shift(@Archives);
303     }
304     $bpc->ArchiveInfoWrite($client, @Archives);
305
306     if ( !$stat{xferOK} ) {
307         print(LOG $bpc->timeStamp, "Archive failed ($stat{hostError})\n");
308         print("archive failed: $stat{hostError}\n");
309         return 1;
310     } else {
311         print(LOG $bpc->timeStamp, "Archive Complete\n");
312         print("Archive Complete\n");
313         return;
314     }
315 }
316
317 #
318 # The Xfer method might tell us from time to time about processes
319 # it forks.  We tell BackupPC about this (for status displays) and
320 # keep track of the pids in case we cancel the backup
321 #
322 sub pidHandler
323 {
324     @xferPid = @_;
325     @xferPid = grep(/./, @xferPid);
326     return if ( !@xferPid );
327     my @pids = @xferPid;
328     my $str = join(",", @pids);
329     $ArchiveLOG->write(\"Xfer PIDs are now $str\n") if ( defined($ArchiveLOG) );
330     print("xferPids $str\n");
331 }
332
333 #
334 # Run an optional pre- or post-dump command
335 #
336 sub UserCommandRun
337 {
338     my($type) = @_;
339
340     return if ( !defined($Conf{$type}) );
341     my $vars = {
342         xfer         => $xfer,
343         client       => $client,
344         host         => $host,
345         user         => $user,
346         share        => $ArchiveReq{shareDest},
347         XferMethod   => $Conf{XferMethod},
348         HostList     => \@{$ArchiveReq{HostList}},
349         BackupList   => \@{$ArchiveReq{BackupList}},
350         archiveloc   => $ArchiveReq{archiveloc},
351         parfile      => $ArchiveReq{parfile},
352         compression  => $ArchiveReq{compression},
353         compext      => $ArchiveReq{compext},
354         splitsize    => $ArchiveReq{splitsize},
355         sshPath      => $Conf{SshPath},
356         LOG          => *LOG,
357         XferLOG      => $ArchiveLOG,
358         stat         => \%stat,
359         xferOK       => $stat{xferOK} || 0,
360         type         => $type,
361     };
362     my $cmd = $bpc->cmdVarSubstitute($Conf{$type}, $vars);
363     $ArchiveLOG->write(\"Executing $type: @$cmd\n");
364     #
365     # Run the user's command, dumping the stdout/stderr into the
366     # Xfer log file.  Also supply the optional $vars and %Conf in
367     # case the command is really perl code instead of a shell
368     # command.
369     #
370     $bpc->cmdSystemOrEval($cmd,
371             sub {
372                 $ArchiveLOG->write(\$_[0]);
373             },
374             $vars, \%Conf);
375 }