v1.5.0
[BackupPC.git] / cgi-bin / BackupPC_Admin
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-w
3 #
4 # BackupPC_Admin: Apache/CGI interface for BackupPC.
5 #
6 # DESCRIPTION
7 #   BackupPC_Admin provides a flexible web interface for BackupPC.
8 #   It is a CGI script that runs under Apache.
9 #
10 #   It requires that Apache pass in $ENV{SCRIPT_NAME} and
11 #   $ENV{REMOTE_USER}. The latter requires .ht_access style
12 #   authentication. Replace the code below if you are using some other
13 #   type of authentication, and have a different way of getting the
14 #   user name.
15 #
16 #   Also, this script needs to run as the BackupPC user.  To accomplish
17 #   this the script is typically installed as setuid to the BackupPC user.
18 #
19 # AUTHOR
20 #   Craig Barratt  <cbarratt@users.sourceforge.net>
21 #
22 # COPYRIGHT
23 #   Copyright (C) 2001  Craig Barratt
24 #
25 #   This program is free software; you can redistribute it and/or modify
26 #   it under the terms of the GNU General Public License as published by
27 #   the Free Software Foundation; either version 2 of the License, or
28 #   (at your option) any later version.
29 #
30 #   This program is distributed in the hope that it will be useful,
31 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
32 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33 #   GNU General Public License for more details.
34 #
35 #   You should have received a copy of the GNU General Public License
36 #   along with this program; if not, write to the Free Software
37 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
38 #
39 #========================================================================
40 #
41 # Version 1.5.0, released 2 Aug 2002.
42 #
43 # See http://backuppc.sourceforge.net.
44 #
45 #========================================================================
46
47 use strict;
48 use CGI;
49 use lib "__INSTALLDIR__/lib";
50 use BackupPC::Lib;
51 use BackupPC::FileZIO;
52 use BackupPC::Attrib qw(:all);
53 use Data::Dumper;
54
55 use vars qw($Cgi %In $MyURL $User %Conf $TopDir $BinDir $bpc);
56 use vars qw(%Status %Info %Jobs @BgQueue @UserQueue @CmdQueue
57             %QueueLen %StatusHost);
58 use vars qw($Hosts $HostsMTime $ConfigMTime $PrivAdmin);
59 use vars qw(%UserEmailInfo $UserEmailInfoMTime %RestoreReq);
60
61 $Cgi = new CGI;
62 %In = $Cgi->Vars;
63
64 #
65 # We require that Apache pass in $ENV{SCRIPT_NAME} and $ENV{REMOTE_USER}.
66 # The latter requires .ht_access style authentication.  Replace this
67 # code if you are using some other type of authentication, and have
68 # a different way of getting the user name.
69 #
70 $MyURL  = $ENV{SCRIPT_NAME};
71 $User   = $ENV{REMOTE_USER};
72
73 if ( !defined($bpc) ) {
74     ErrorExit("BackupPC::Lib->new failed: check apache error_log\n")
75                                     if ( !($bpc = BackupPC::Lib->new) );
76     $TopDir = $bpc->TopDir();
77     $BinDir = $bpc->BinDir();
78     %Conf   = $bpc->Conf();
79     $ConfigMTime = $bpc->ConfigMTime();
80 } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
81     $bpc->ConfigRead();
82     %Conf   = $bpc->Conf();
83     $ConfigMTime = $bpc->ConfigMTime();
84 }
85
86 #
87 # Clean up %ENV for taint checking
88 #
89 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
90 $ENV{PATH} = $Conf{MyPath};
91
92 #
93 # Verify we are running as the correct user
94 #
95 if ( $Conf{BackupPCUserVerify}
96         && $> != (my $uid = (getpwnam($Conf{BackupPCUser}))[2]) ) {
97     ErrorExit("Wrong user: my userid is $>, instead of $uid"
98             . " ($Conf{BackupPCUser})\n");
99 }
100
101 if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
102     $HostsMTime = $bpc->HostsMTime();
103     $Hosts = $bpc->HostInfoRead();
104 }
105
106 my %ActionDispatch = (
107     "summary"             => \&Action_Summary,
108     "Start Incr Backup"   => \&Action_StartStopBackup,
109     "Start Full Backup"   => \&Action_StartStopBackup,
110     "Stop/Dequeue Backup" => \&Action_StartStopBackup,
111     "queue"               => \&Action_Queue,
112     "view"                => \&Action_View,
113     "LOGlist"             => \&Action_LOGlist,
114     "emailSummary"        => \&Action_EmailSummary,
115     "browse"              => \&Action_Browse,
116     "Restore"             => \&Action_Restore,
117     "RestoreFile"         => \&Action_RestoreFile,
118     "hostInfo"            => \&Action_HostInfo,
119     "generalInfo"         => \&Action_GeneralInfo,
120     "restoreInfo"         => \&Action_RestoreInfo,
121 );
122
123 #
124 # Set default actions, then call sub handler
125 #
126 $In{action} ||= "hostInfo"    if ( defined($In{host}) );
127 $In{action}   = "generalInfo" if ( !defined($ActionDispatch{$In{action}}) );
128 $ActionDispatch{$In{action}}();
129 exit(0);
130
131 ###########################################################################
132 # Action handling subroutines
133 ###########################################################################
134
135 sub Action_Summary
136 {
137     my($fullTot, $fullSizeTot, $incrTot, $incrSizeTot, $str,
138        $strNone, $strGood, $hostCntGood, $hostCntNone);
139
140     $hostCntGood = $hostCntNone = 0;
141     GetStatusInfo("hosts");
142     my $Privileged = CheckPermission();
143
144     if ( !$Privileged ) {
145         ErrorExit("Only privileged users can view PC summaries." );
146     }
147     foreach my $host ( sort(keys(%Status)) ) {
148         my($fullDur, $incrCnt, $incrAge, $fullSize, $fullRate);
149         my @Backups = $bpc->BackupInfoRead($host);
150         my $fullCnt = $incrCnt = 0;
151         my $fullAge = $incrAge = -1;
152         for ( my $i = 0 ; $i < @Backups ; $i++ ) {
153             if ( $Backups[$i]{type} eq "full" ) {
154                 $fullCnt++;
155                 if ( $fullAge < 0 || $Backups[$i]{startTime} > $fullAge ) {
156                     $fullAge  = $Backups[$i]{startTime};
157                     $fullSize = $Backups[$i]{size} / (1024 * 1024);
158                     $fullDur  = $Backups[$i]{endTime} - $Backups[$i]{startTime};
159                 }
160                 $fullSizeTot += $Backups[$i]{size} / (1024 * 1024);
161             } else {
162                 $incrCnt++;
163                 if ( $incrAge < 0 || $Backups[$i]{startTime} > $incrAge ) {
164                     $incrAge = $Backups[$i]{startTime};
165                 }
166                 $incrSizeTot += $Backups[$i]{size} / (1024 * 1024);
167             }
168         }
169         if ( $fullAge < 0 ) {
170             $fullAge = "";
171             $fullRate = "";
172         } else {
173             $fullAge = sprintf("%.1f", (time - $fullAge) / (24 * 3600));
174             $fullRate = sprintf("%.2f",
175                                 $fullSize / ($fullDur <= 0 ? 1 : $fullDur));
176         }
177         if ( $incrAge < 0 ) {
178             $incrAge = "";
179         } else {
180             $incrAge = sprintf("%.1f", (time - $incrAge) / (24 * 3600));
181         }
182         $fullTot += $fullCnt;
183         $incrTot += $incrCnt;
184         $fullSize = sprintf("%.2f", $fullSize / 1000);
185         $str = <<EOF;
186 <tr><td> ${HostLink($host)} </td>
187     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
188     <td align="center"> $fullCnt </td>
189     <td align="center"> $fullAge </td>
190     <td align="center"> $fullSize </td>
191     <td align="center"> $fullRate </td>
192     <td align="center"> $incrCnt </td>
193     <td align="center"> $incrAge </td>
194     <td align="center"> $Status{$host}{state} </td>
195     <td> $Status{$host}{reason} </td></tr>
196 EOF
197         if ( @Backups == 0 ) {
198             $hostCntNone++;
199             $strNone .= $str;
200         } else {
201             $hostCntGood++;
202             $strGood .= $str;
203         }
204     }
205     $fullSizeTot = sprintf("%.2f", $fullSizeTot / 1000);
206     $incrSizeTot = sprintf("%.2f", $incrSizeTot / 1000);
207     my $now      = timeStamp2(time);
208
209     Header("BackupPC: Server Summary");
210
211     print <<EOF;
212
213 ${h1("BackupPC Summary")}
214 <p>
215 This status was generated at $now.
216 <p>
217
218 ${h2("Hosts with good Backups")}
219 <p>
220 There are $hostCntGood hosts that have been backed up, for a total of:
221 <ul>
222 <li> $fullTot full backups of total size ${fullSizeTot}GB
223      (prior to pooling and compression),
224 <li> $incrTot incr backups of total size ${incrSizeTot}GB
225      (prior to pooling and compression).
226 </ul>
227 <table border>
228 <tr><td> Host </td>
229     <td align="center"> User </td>
230     <td align="center"> #Full </td>
231     <td align="center"> Full Age/days </td>
232     <td align="center"> Full Size/GB </td>
233     <td align="center"> Speed MB/sec </td>
234     <td align="center"> #Incr </td>
235     <td align="center"> Incr Age/days </td>
236     <td align="center"> State </td>
237     <td align="center"> Last attempt </td></tr>
238 $strGood
239 </table>
240 <p>
241
242 ${h2("Hosts with no Backups")}
243 <p>
244 There are $hostCntNone hosts with no backups.
245 <p>
246 <table border>
247 <tr><td> Host </td>
248     <td align="center"> User </td>
249     <td align="center"> #Full </td>
250     <td align="center"> Full Age/days </td>
251     <td align="center"> Full Size/GB </td>
252     <td align="center"> Speed MB/sec </td>
253     <td align="center"> #Incr </td>
254     <td align="center"> Incr Age/days </td>
255     <td align="center"> Current State </td>
256     <td align="center"> Last backup attempt </td></tr>
257 $strNone
258 </table>
259 EOF
260     Trailer();
261 }
262
263 sub Action_StartStopBackup
264 {
265     my($str, $reply);
266     my $start = 1 if ( $In{action} eq "Start Incr Backup"
267                        || $In{action} eq "Start Full Backup" );
268     my $doFull = $In{action} eq "Start Full Backup" ? 1 : 0;
269     my $type = $doFull ? "full" : "incremental";
270     my $host = $In{host};
271     my $Privileged = CheckPermission($host);
272
273     if ( !$Privileged ) {
274         ErrorExit("Only privileged users can stop or start backups on"
275                 . " ${EscapeHTML($host)}.");
276     }
277     ServerConnect();
278
279     if ( $In{doit} ) {
280         if ( $start ) {
281             if ( $Hosts->{$host}{dhcp} ) {
282                 $reply = $bpc->ServerMesg("backup $In{hostIP} $host"
283                                         . " $User $doFull");
284                 $str = "Backup requested on DHCP $host ($In{hostIP}) by"
285                      . " $User from $ENV{REMOTE_ADDR}";
286             } else {
287                 $reply = $bpc->ServerMesg("backup $host $host $User $doFull");
288                 $str = "Backup requested on $host by $User";
289             }
290         } else {
291             $reply = $bpc->ServerMesg("stop $host $User $In{backoff}");
292             $str = "Backup stopped/dequeued on $host by $User";
293         }
294         Header("BackupPC: Backup Requested on $host");
295         print <<EOF;
296 ${h1($str)}
297 <p>
298 Reply from server was: $reply
299 <p>
300 Go back to <a href="$MyURL?host=$host">$host home page</a>.
301 EOF
302         Trailer();
303     } else {
304         if ( $start ) {
305             my $ipAddr = ConfirmIPAddress($host);
306
307             Header("BackupPC: Start Backup Confirm on $host");
308             print <<EOF;
309 ${h1("Are you sure?")}
310 <p>
311 You are about to start a $type backup on $host.
312
313 <form action="$MyURL" method="get">
314 <input type="hidden" name="host" value="$host">
315 <input type="hidden" name="hostIP" value="$ipAddr">
316 <input type="hidden" name="doit" value="1">
317 Do you really want to do this?
318 <input type="submit" value="$In{action}" name="action">
319 <input type="submit" value="No" name="">
320 </form>
321 EOF
322         } else {
323             my $backoff = "";
324             GetStatusInfo("host($host)");
325             if ( $StatusHost{backoffTime} > time ) {
326                 $backoff = sprintf("%.1f",
327                                   ($StatusHost{backoffTime} - time) / 3600);
328             }
329             Header("BackupPC: Stop Backup Confirm on $host");
330             print <<EOF;
331 ${h1("Are you sure?")}
332 <p>
333 You are about to stop/dequeue backups on $host;
334
335 <form action="$MyURL" method="get">
336 <input type="hidden" name="host" value="$host">
337 <input type="hidden" name="doit" value="1">
338 Also, please don't start another backup for
339 <input type="text" name="backoff" size="10" value="$backoff"> hours.
340 <p>
341 Do you really want to do this?
342 <input type="submit" value="$In{action}" name="action">
343 <input type="submit" value="No" name="">
344 </form>
345 EOF
346         }
347         Trailer();
348     }
349 }
350
351 sub Action_Queue
352 {
353     my($strBg, $strUser, $strCmd);
354
355     GetStatusInfo("queues");
356     my $Privileged = CheckPermission();
357
358     if ( !$Privileged ) {
359         ErrorExit("Only privileged users can view queues." );
360     }
361
362     while ( @BgQueue ) {
363         my $req = pop(@BgQueue);
364         my($reqTime) = timeStamp2($req->{reqTime});
365         $strBg .= <<EOF;
366 <tr><td> ${HostLink($req->{host})} </td>
367     <td align="center"> $reqTime </td>
368     <td align="center"> $req->{user} </td></tr>
369 EOF
370     }
371     while ( @UserQueue ) {
372         my $req = pop(@UserQueue);
373         my $reqTime = timeStamp2($req->{reqTime});
374         $strUser .= <<EOF;
375 <tr><td> ${HostLink($req->{host})} </td>
376     <td align="center"> $reqTime </td>
377     <td align="center"> $req->{user} </td></tr>
378 EOF
379     }
380     while ( @CmdQueue ) {
381         my $req = pop(@CmdQueue);
382         my $reqTime = timeStamp2($req->{reqTime});
383         (my $cmd = $req->{cmd}) =~ s/$BinDir\///;
384         $strCmd .= <<EOF;
385 <tr><td> ${HostLink($req->{host})} </td>
386     <td align="center"> $reqTime </td>
387     <td align="center"> $req->{user} </td>
388     <td> $cmd </td></tr>
389 EOF
390     }
391     Header("BackupPC: Queue Summary");
392     print <<EOF;
393 ${h1("Backup Queue Summary")}
394 <p>
395 ${h2("User Queue Summary")}
396 <p>
397 The following user requests are currently queued:
398 <table border>
399 <tr><td> Host </td>
400     <td> Req Time </td>
401     <td> User </td></tr>
402 $strUser
403 </table>
404 <p>
405
406 ${h2("Background Queue Summary")}
407 <p>
408 The following background requests are currently queued:
409 <table border>
410 <tr><td> Host </td>
411     <td> Req Time </td>
412     <td> User </td></tr>
413 $strBg
414 </table>
415 <p>
416
417 ${h2("Command Queue Summary")}
418 <p>
419 The following command requests are currently queued:
420 <table border>
421 <tr><td> Host </td>
422     <td> Req Time </td>
423     <td> User </td>
424     <td> Command </td></tr>
425 $strCmd
426 </table>
427 EOF
428     Trailer();
429 }
430
431 sub Action_View
432 {
433     my $Privileged = CheckPermission($In{host});
434     my $compress = 0;
435     my $fh;
436     my $host = $In{host};
437     my $num  = $In{num};
438     my $type = $In{type};
439     my $linkHosts = 0;
440     my($file, $comment);
441     my $ext = $num ne "" ? ".$num" : "";
442
443     ErrorExit("Invalid number $num") if ( $num ne "" && $num !~ /^\d+$/ );
444     if ( $type eq "XferLOG" ) {
445         $file = "$TopDir/pc/$host/SmbLOG$ext";
446         $file = "$TopDir/pc/$host/XferLOG$ext" if ( !-f $file && !-f "$file.z");
447     } elsif ( $type eq "XferLOGbad" ) {
448         $file = "$TopDir/pc/$host/SmbLOG.bad";
449         $file = "$TopDir/pc/$host/XferLOG.bad" if ( !-f $file && !-f "$file.z");
450     } elsif ( $type eq "XferErrbad" ) {
451         $file = "$TopDir/pc/$host/SmbLOG.bad";
452         $file = "$TopDir/pc/$host/XferLOG.bad" if ( !-f $file && !-f "$file.z");
453         $comment = "(Extracting only Errors)";
454     } elsif ( $type eq "XferErr" ) {
455         $file = "$TopDir/pc/$host/SmbLOG$ext";
456         $file = "$TopDir/pc/$host/XferLOG$ext" if ( !-f $file && !-f "$file.z");
457         $comment = "(Extracting only Errors)";
458     } elsif ( $type eq "RestoreLOG" ) {
459         $file = "$TopDir/pc/$host/RestoreLOG$ext";
460     } elsif ( $type eq "RestoreErr" ) {
461         $file = "$TopDir/pc/$host/RestoreLOG$ext";
462         $comment = "(Extracting only Errors)";
463     } elsif ( $host ne "" && $type eq "config" ) {
464         $file = "$TopDir/pc/$host/config.pl";
465     } elsif ( $type eq "docs" ) {
466         $file = "$BinDir/../doc/BackupPC.html";
467         if ( open(LOG, $file) ) {
468             Header("BackupPC: Documentation");
469             print while ( <LOG> );
470             close(LOG);
471             Trailer();
472         } else {
473             ErrorExit("Unable to open $file: configuration problem?");
474         }
475         return;
476     } elsif ( $type eq "config" ) {
477         $file = "$TopDir/conf/config.pl";
478     } elsif ( $type eq "hosts" ) {
479         $file = "$TopDir/conf/hosts";
480     } elsif ( $host ne "" ) {
481         $file = "$TopDir/pc/$host/LOG$ext";
482     } else {
483         $file = "$TopDir/log/LOG$ext";
484         $linkHosts = 1;
485     }
486     if ( !$Privileged ) {
487         ErrorExit("Only privileged users can view log or config files." );
488     }
489     if ( !-f $file && -f "$file.z" ) {
490         $file .= ".z";
491         $compress = 1;
492     }
493     Header("BackupPC: Log File $file");
494     print <<EOF;
495 ${h1("Log File $file $comment")}
496 <p>
497 EOF
498     if ( defined($fh = BackupPC::FileZIO->open($file, 0, $compress)) ) {
499         my $mtimeStr = $bpc->timeStamp((stat($file))[9], 1);
500         print <<EOF;
501 Contents of log file <tt>$file</tt>, modified $mtimeStr $comment
502 EOF
503         print "<pre>";
504         if ( $type eq "XferErr" || $type eq "XferErrbad"
505                                 || $type eq "RestoreErr" ) {
506             my $skipped;
507             while ( 1 ) {
508                 $_ = $fh->readLine();
509                 if ( $_ eq "" ) {
510                     print("[ skipped $skipped lines ]\n") if ( $skipped );
511                     last;
512                 }
513                 if ( /smb: \\>/
514                         || /^\s*(\d+) \(\s*\d+\.\d kb\/s\) (.*)$/
515                         || /^tar: dumped \d+ files/
516                         || /^added interface/i
517                         || /^restore tar file /i
518                         || /^restore directory /i
519                         || /^tarmode is now/i
520                         || /^Total bytes written/i
521                         || /^Domain=/i
522                         || /^Getting files newer than/i
523                         || /^Output is \/dev\/null/
524                         || /^\([\d\.]* kb\/s\) \(average [\d\.]* kb\/s\)$/
525                         || /^\s+directory \\/
526                         || /^Timezone is/
527                         || /^\.\//
528                             ) {
529                     $skipped++;
530                     next;
531                 }
532                 print("[ skipped $skipped lines ]\n") if ( $skipped );
533                 $skipped = 0;
534                 print ${EscapeHTML($_)};
535             }
536         } elsif ( $linkHosts ) {
537             while ( 1 ) {
538                 $_ = $fh->readLine();
539                 last if ( $_ eq "" );
540                 my $s = ${EscapeHTML($_)};
541                 $s =~ s/\b([\w-]+)\b/defined($Hosts->{$1})
542                                         ? ${HostLink($1)} : $1/eg;
543                 print $s;
544             }
545         } elsif ( $type eq "config" ) {
546             while ( 1 ) {
547                 $_ = $fh->readLine();
548                 last if ( $_ eq "" );
549                 # remove any passwords and user names
550                 s/(SmbSharePasswd.*=.*['"]).*(['"])/$1$2/ig;
551                 s/(SmbShareUserName.*=.*['"]).*(['"])/$1$2/ig;
552                 s/(ServerMesgSecret.*=.*['"]).*(['"])/$1$2/ig;
553                 print ${EscapeHTML($_)};
554             }
555         } else {
556             while ( 1 ) {
557                 $_ = $fh->readLine();
558                 last if ( $_ eq "" );
559                 print ${EscapeHTML($_)};
560             }
561         }
562         $fh->close();
563     } else {
564         printf("<pre>\nCan't open log file $file\n");
565     }
566     print <<EOF;
567 </pre>
568 EOF
569     Trailer();
570 }
571
572 sub Action_LOGlist
573 {
574     my $Privileged = CheckPermission($In{host});
575
576     if ( !$Privileged ) {
577         ErrorExit("Only privileged users can view log files.");
578     }
579     my $host = $In{host};
580     my($url0, $hdr, $root, $str);
581     if ( $host ne "" ) {
582         $root = "$TopDir/pc/$host/LOG";
583         $url0 = "&host=$host";
584         $hdr = "for host $host";
585     } else {
586         $root = "$TopDir/log/LOG";
587         $url0 = "";
588         $hdr = "";
589     }
590     for ( my $i = -1 ; ; $i++ ) {
591         my $url1 = "";
592         my $file = $root;
593         if ( $i >= 0 ) {
594             $file .= ".$i";
595             $url1 = "&num=$i";
596         }
597         $file .= ".z" if ( !-f $file && -f "$file.z" );
598         last if ( !-f $file );
599         my $mtimeStr = $bpc->timeStamp((stat($file))[9], 1);
600         my $size     = (stat($file))[7];
601         $str .= <<EOF;
602 <tr><td> <a href="$MyURL?action=view&type=LOG$url0$url1"><tt>$file</tt></a> </td>
603     <td align="right"> $size </td>
604     <td> $mtimeStr </td></tr>
605 EOF
606     }
607     Header("BackupPC: Log File History");
608     print <<EOF;
609
610 ${h1("Log File History $hdr")}
611 <p>
612 <table border>
613 <tr><td align="center"> File </td>
614     <td align="center"> Size </td>
615     <td align="center"> Modification time </td></tr>
616 $str
617 </table>
618 EOF
619     Trailer();
620 }
621
622 sub Action_EmailSummary
623 {
624     my $Privileged = CheckPermission();
625
626     if ( !$Privileged ) {
627         ErrorExit("Only privileged users can view email summaries." );
628     }
629     GetStatusInfo("hosts");
630     ReadUserEmailInfo();
631     my(%EmailStr, $str);
632     foreach my $u ( keys(%UserEmailInfo) ) {
633         next if ( !defined($UserEmailInfo{$u}{lastTime}) );
634         my $emailTimeStr = timeStamp2($UserEmailInfo{$u}{lastTime});
635         $EmailStr{$UserEmailInfo{$u}{lastTime}} .= <<EOF;
636 <tr><td>${UserLink($u)} </td>
637     <td>${HostLink($UserEmailInfo{$u}{lastHost})} </td>
638     <td>$emailTimeStr </td>
639     <td>$UserEmailInfo{$u}{lastSubj} </td></tr>
640 EOF
641     }
642     foreach my $t ( sort({$b <=> $a} keys(%EmailStr)) ) {
643         $str .= $EmailStr{$t};
644     }
645     Header("BackupPC: Email Summary");
646     print <<EOF;
647 ${h1("Recent Email Summary (Reverse time order)")}
648 <p>
649 <table border>
650 <tr><td align="center"> Recipient </td>
651     <td align="center"> Host </td>
652     <td align="center"> Time </td>
653     <td align="center"> Subject </td></tr>
654 $str
655 </table>
656 EOF
657     Trailer();
658 }
659
660 sub Action_Browse
661 {
662     my $Privileged = CheckPermission($In{host});
663     my($i, $dirStr, $fileStr, $mangle);
664     my($numF, $compressF, $mangleF, $fullDirF);
665     my $checkBoxCnt = 0;      # checkbox counter
666
667     if ( !$Privileged ) {
668         ErrorExit("Only privileged users can browse backup files"
669                 . " for host ${EscapeHTML($In{host})}." );
670     }
671     my $host = $In{host};
672     my $num  = $In{num};
673     my $dir  = $In{dir};
674     if ( $host eq "" ) {
675         ErrorExit("Empty host name.");
676     }
677     #
678     # Find the requested backup and the previous filled backup
679     #
680     my @Backups = $bpc->BackupInfoRead($host);
681     for ( $i = 0 ; $i < @Backups ; $i++ ) {
682         if ( !$Backups[$i]{noFill} ) {
683             $numF      = $Backups[$i]{num};
684             $mangleF   = $Backups[$i]{mangle};
685             $compressF = $Backups[$i]{compress};
686         }
687         last if ( $Backups[$i]{num} == $num );
688     }
689     if ( $i >= @Backups ) {
690         ErrorExit("Backup number $num for host ${EscapeHTML($host)} does"
691                 . " not exist.");
692     }
693     if ( !$Backups[$i]{noFill} ) {
694         # no need to back-fill a filled backup
695         $numF = $mangleF = $compressF = undef;
696     }
697     my $backupTime = timeStamp2($Backups[$i]{startTime});
698     my $backupAge = sprintf("%.1f", (time - $Backups[$i]{startTime})
699                                     / (24 * 3600));
700     $mangle = $Backups[$i]{mangle};
701     if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
702         if ( !opendir(DIR, "$TopDir/pc/$host/$num") ) {
703             ErrorExit("Can't browse bad directory name"
704                     . " ${EscapeHTML(\"$TopDir/pc/$host/$num\")}");
705         }
706         #
707         # Read this directory and find the first directory
708         #
709         foreach my $f ( readdir(DIR) ) {
710             next if ( $f eq "." || $f eq ".." );
711             if ( -d "$TopDir/pc/$host/$num/$f" ) {
712                 $dir = "/$f";
713                 last;
714             }
715         }
716         closedir(DIR);
717         if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
718             ErrorExit("Directory ${EscapeHTML(\"$TopDir/pc/$host/$num\")}"
719                     . " is empty");
720         }
721     }
722     my $relDir   = $dir;
723     my $fullDir  = "$TopDir/pc/$host/$num/$relDir";
724     if ( defined($numF) ) {
725         # get full path to filled backup
726         if ( $mangle && !$mangleF ) {
727             $fullDirF = "$TopDir/pc/$host/$numF/"
728                                 . $bpc->fileNameUnmangle($relDir);
729         } else {
730             $fullDirF = "$TopDir/pc/$host/$numF/$relDir";
731         }
732     }
733     my $currDir    = undef;
734     #
735     # Read attributes for the directory and optionally for the filled backup
736     #
737     my $attr  = BackupPC::Attrib->new({ compress => $Backups[$i]{compress}});
738     my $attrF = BackupPC::Attrib->new({ compress => $compressF})
739                                                     if ( defined($numF) );
740     $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
741     if ( defined($numF) && -f $attrF->fileName($fullDirF)
742                         && $attrF->read($fullDirF) ) {
743         $attr->merge($attrF);
744     }
745     #
746     # Loop up the directory tree until we hit the top.
747     #
748     my(@DirStrPrev);
749     while ( 1 ) {
750         my($fLast, $fum, $fLastum, @DirStr);
751
752         if ( $fullDir =~ m{(^|/)\.\.(/|$)} || !opendir(DIR, $fullDir) ) {
753             ErrorExit("Can't browse bad directory name"
754                     . " ${EscapeHTML($fullDir)}");
755         }
756         #
757         # Read this directory and optionally the corresponding filled directory
758         #
759         my @Dir = readdir(DIR);
760         closedir(DIR);
761         if ( defined($numF) && opendir(DIR, $fullDirF) ) {
762             if ( $mangle == $mangleF ) {
763                 @Dir = (@Dir, readdir(DIR));
764             } else {
765                 foreach my $f ( readdir(DIR) ) {
766                     next if ( $f eq "." || $f eq ".." );
767                     push(@Dir, $bpc->fileNameMangle($f));
768                 }
769             }
770             closedir(DIR);
771         }
772         my $fileCnt = 0;          # file counter
773         $fLast = $dirStr = "";
774         #
775         # Loop over each of the files in this directory
776         #
777         my(@DirUniq);
778         foreach my $f ( sort({uc($a) cmp uc($b)} @Dir) ) {
779             next if ( $f eq "." || $f eq ".."
780                    || $f eq $fLast || ($mangle && $f eq "attrib") );
781             $fLast = $f;
782             push(@DirUniq, $f);
783         }
784         while ( defined(my $f = shift(@DirUniq)) ) {
785             my $path = "$relDir/$f";
786             my($dirOpen, $gotDir, $imgStr, $img);
787             $fum = $mangle ? $bpc->fileNameUnmangle($f) : $f;  # unmangled $f
788             my $fumURI = $fum;                                 # URI escaped $f
789             $path =~ s{^/+}{/};
790             $path =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
791             $fumURI =~ s/([^\w.\/-])/uc sprintf("%%%02x", ord($1))/eg;
792             $dirOpen = 1 if ( defined($currDir) && $f eq $currDir );
793             if ( -d "$fullDir/$f" ) {
794                 #
795                 # Display directory if it exists in current backup.
796                 # First find out if there are subdirs
797                 #
798                 my @s = (defined($numF) && -d "$fullDirF/$f")
799                             ? stat("$fullDirF/$f")
800                             : stat("$fullDir/$f");
801                 my($bold, $unbold, $BGcolor);
802                 $img |= 1 << 6;
803                 $img |= 1 << 5 if ( $s[3] > 2 );
804                 if ( $dirOpen ) {
805                     $bold = "<b>";
806                     $unbold = "</b>";
807                     $img |= 1 << 2;
808                     $img |= 1 << 3 if ( $s[3] > 2 );
809                 }
810                 my $imgFileName = sprintf("%07b.gif", $img);
811                 $imgStr = "<img src=\"$Conf{CgiImageDirURL}/$imgFileName\" align=\"absmiddle\" width=\"9\" height=\"19\" border=\"0\">";
812                 if ( "$relDir/$f" eq $dir ) {
813                     $BGcolor = " bgcolor=\"$Conf{CgiHeaderBgColor}\"";
814                 } else {
815                     $BGcolor = "";
816                 }
817                 my $dirName = $fum;
818                 $dirName =~ s/ /&nbsp;/g;
819                 push(@DirStr, {needTick => 1,
820                                tdArgs   => $BGcolor,
821                                link     => <<EOF});
822 <a href="$MyURL?action=browse&host=$host&num=$num&dir=$path">$imgStr</a><a href="$MyURL?action=browse&host=$host&num=$num&dir=$path" style="font-size:13px;font-family:arial;text-decoration:none;line-height:15px">&nbsp;$bold$dirName$unbold</a></td></tr>
823 EOF
824                 $fileCnt++;
825                 $gotDir = 1;
826                 if ( $dirOpen ) {
827                     my($lastTick, $doneLastTick);
828                     foreach my $d ( @DirStrPrev ) {
829                         $lastTick = $d if ( $d->{needTick} );
830                     }
831                     $doneLastTick = 1 if ( !defined($lastTick) );
832                     foreach my $d ( @DirStrPrev ) {
833                         $img = 0;
834                         if  ( $d->{needTick} ) {
835                             $img |= 1 << 0;
836                         }
837                         if ( $d == $lastTick ) {
838                             $img |= 1 << 4;
839                             $doneLastTick = 1;
840                         } elsif ( !$doneLastTick ) {
841                             $img |= 1 << 3 | 1 << 4;
842                         }
843                         my $imgFileName = sprintf("%07b.gif", $img);
844                         $imgStr = "<img src=\"$Conf{CgiImageDirURL}/$imgFileName\" align=\"absmiddle\" width=\"9\" height=\"19\" border=\"0\">";
845                         push(@DirStr, {needTick => 0,
846                                        tdArgs   => $d->{tdArgs},
847                                        link     => $imgStr . $d->{link}
848                         });
849                     }
850                 }
851             }
852             if ( $relDir eq $dir ) {
853                 #
854                 # This is the selected directory, so display all the files
855                 #
856                 my $attrStr;
857                 if ( defined($a = $attr->get($fum)) ) {
858                     my $mtimeStr = $bpc->timeStamp($a->{mtime});
859                     my $typeStr  = $attr->fileType2Text($a->{type});
860                     my $modeStr  = sprintf("0%o", $a->{mode} & 07777);
861                     $attrStr .= <<EOF;
862     <td align="center">$typeStr</td>
863     <td align="right">$modeStr</td>
864     <td align="right">$a->{size}</td>
865     <td align="right">$mtimeStr</td>
866 </tr>
867 EOF
868                 } else {
869                     $attrStr .= "<td colspan=\"4\" align=\"center\"> </td>\n";
870                 }
871                 if ( $gotDir ) {
872                     $fileStr .= <<EOF;
873 <tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=browse&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
874 $attrStr
875 </tr>
876 EOF
877                 } else {
878                     $fileStr .= <<EOF;
879 <tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path">&nbsp;<a href="$MyURL?action=RestoreFile&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
880 $attrStr
881 </tr>
882 EOF
883                 }
884                 $checkBoxCnt++;
885             }
886         }
887         @DirStrPrev = @DirStr;
888         last if ( $relDir eq "" );
889         # 
890         # Prune the last directory off $relDir
891         #
892         $relDir =~ s/(.*)\/(.*)/$1/;
893         $currDir = $2;
894         $fullDir = "$TopDir/pc/$host/$num/$relDir";
895         $fullDirF = "$TopDir/pc/$host/$numF/$relDir" if ( defined($numF) );
896     }
897     my $dirDisplay = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
898     $dirDisplay =~ s{//}{/}g;
899     my $filledBackup;
900     if ( defined($numF) ) {
901         $filledBackup = <<EOF;
902 <li> This display is merged with backup #$numF, the most recent prior
903      filled (full) dump.
904 EOF
905     }
906     Header("BackupPC: Browse backup $num for $host");
907
908     foreach my $d ( @DirStrPrev ) {
909         $dirStr .= "<tr><td$d->{tdArgs}>$d->{link}\n";
910     }
911
912     ### hide checkall button if there are no files
913     my ($topCheckAll, $checkAll, $fileHeader);
914     if ( $fileStr ) {
915         $fileHeader = <<EOF;
916             <tr bgcolor="$Conf{CgiHeaderBgColor}"><td align=center> Name</td>
917                 <td align="center"> Type</td>
918                 <td align="center"> Mode</td>
919                 <td align="center"> Size</td>
920                 <td align="center"> Mod time</td>
921             </tr>
922 EOF
923         $checkAll = <<EOF;
924 <tr bgcolor="#ffffcc"><td>
925 <input type="checkbox" name="allFiles" onClick="return checkAll('allFiles');">&nbsp;Select all
926 </td><td colspan="4" align="center">
927 <input type="submit" name="Submit" value="Restore selected files">
928 </td></tr>
929 EOF
930         # and put a checkall box on top if there are at least 20 files
931         if ( $checkBoxCnt >= 20 ) {
932             $topCheckAll = $checkAll;
933             $topCheckAll =~ s{allFiles}{allFilestop}g;
934         }
935     } else {
936         $fileStr = <<EOF;
937 <tr><td bgcolor="#ffffff">The directory ${EscapeHTML($dirDisplay)} is empty
938 </td></tr>
939 EOF
940     }
941  
942     print <<EOF;
943 ${h1("Backup browse for $host")}
944
945 <script language="javascript" type="text/javascript">
946 <!--
947
948     function checkAll(location)
949     {
950       for (var i=0;i<document.form1.elements.length;i++)
951       {
952         var e = document.form1.elements[i];
953         if ((e.checked || !e.checked) && e.name != 'all') {
954             if (eval("document.form1."+location+".checked")) {
955                 e.checked = true;
956             } else {
957                 e.checked = false;
958             }
959         }
960       }
961     }
962     
963     function toggleThis(checkbox)
964     {
965        var cb = eval("document.form1."+checkbox);
966        cb.checked = !cb.checked;        
967     }
968
969 //-->
970 </script>
971
972 <ul>
973 <li> You are browsing backup #$num, which started around $backupTime
974         ($backupAge days ago),
975 $filledBackup
976 <li> Click on a directory below to navigate into that directory,
977 <li> Click on a file below to restore that file.
978 </ul>
979
980 ${h2("Contents of ${EscapeHTML($dirDisplay)}")}
981 <form name="form1" method="post" action="$MyURL">
982 <input type="hidden" name="num" value="$num">
983 <input type="hidden" name="host" value="$host">
984 <input type="hidden" name="fcbMax" value="$checkBoxCnt">
985 <input type="hidden" name="action" value="Restore">
986 <br>
987 <table>
988 <tr><td valign="top">
989     <!--Navigate here:-->
990     <br><table align="center" border="0" cellpadding="0" cellspacing="0" bgcolor="#ffffff">
991     $dirStr
992     </table>
993 </td><td width="3%">
994 </td><td valign="top">
995     <!--Restore files here:-->
996     <br>
997     <table cellpadding="0" cellspacing="0" bgcolor="#333333"><tr><td>
998         <table border="0" width="100%" align="left" cellpadding="2" cellspacing="1">
999         $fileHeader
1000         $topCheckAll
1001         $fileStr
1002         $checkAll
1003         </table>
1004     </td></tr></table>
1005 <br>
1006 <!--
1007 This is now in the checkAll row
1008 <input type="submit" name="Submit" value="Restore selected files">
1009 -->
1010 </td></tr></table>
1011 </form>
1012 EOF
1013     Trailer();
1014 }
1015
1016 sub Action_Restore
1017 {
1018     my($str, $reply, $i);
1019     my $Privileged = CheckPermission($In{host});
1020     if ( !$Privileged ) {
1021         ErrorExit("Only privileged users can restore backup files"
1022                 . " for host ${EscapeHTML($In{host})}." );
1023     }
1024     my $host = $In{host};
1025     my $num  = $In{num};
1026     my(@fileList, $fileListStr, $hiddenStr, $share, $pathHdr, $badFileCnt);
1027     my @Backups = $bpc->BackupInfoRead($host);
1028     for ( $i = 0 ; $i < @Backups ; $i++ ) {
1029         last if ( $Backups[$i]{num} == $num );
1030     }
1031     my $mangle = $Backups[$i]{mangle};
1032     ServerConnect();
1033
1034     if ( !defined($Hosts->{$host}) ) {
1035         ErrorExit("Bad host name ${EscapeHTML($host)}");
1036     }
1037     for ( my $i = 0 ; $i < $In{fcbMax} ; $i++ ) {
1038         next if ( !defined($In{"fcb$i"}) );
1039         (my $name = $In{"fcb$i"}) =~ s/%([0-9A-F]{2})/chr(hex($1))/eg;
1040         $badFileCnt++ if ( $name =~ m{(^|/)\.\.(/|$)} );
1041         if ( $name =~ m{^/+(.*?)(/.*)} ) {
1042             $share = $1;
1043             $name  = $mangle ? $bpc->fileNameUnmangle($2) : $2;
1044             if ( @fileList == 0 ) {
1045                 $pathHdr = $name;
1046             } else {
1047                 while ( substr($name, 0, length($pathHdr)) ne $pathHdr ) {
1048                     $pathHdr = substr($pathHdr, 0, rindex($pathHdr, "/"));
1049                 }
1050             }
1051         }
1052         push(@fileList, $name);
1053         $share = $mangle ? $bpc->fileNameUnmangle($share) : $share;
1054         $hiddenStr .= <<EOF;
1055 <input type="hidden" name="fcb$i" value="$In{'fcb' . $i}">
1056 EOF
1057         $fileListStr .= <<EOF;
1058 <li> ${EscapeHTML($name)}
1059 EOF
1060     }
1061     $hiddenStr .= "<input type=\"hidden\" name=\"fcbMax\" value=\"$In{fcbMax}\">\n";
1062     $badFileCnt++ if ( $In{pathHdr} =~ m{(^|/)\.\.(/|$)} );
1063     $badFileCnt++ if ( $In{num} =~ m{(^|/)\.\.(/|$)} );
1064     if ( @fileList == 0 ) {
1065         ErrorExit("You haven't selected any files; please go Back to"
1066                 . " select some files.");
1067     }
1068     if ( $badFileCnt ) {
1069         ErrorExit("Nice try, but you can't put '..' in any of the file names");
1070     }
1071     if ( @fileList == 1 ) {
1072         $pathHdr =~ s/(.*)\/.*/$1/;
1073     }
1074     $pathHdr = "/" if ( $pathHdr eq "" );
1075     if ( $In{type} != 0 && @fileList == $In{fcbMax} ) {
1076         #
1077         # All the files in the list were selected, so just restore the
1078         # entire parent directory
1079         #
1080         @fileList = ( $pathHdr );
1081     }
1082     if ( $In{type} == 0 ) {
1083         #
1084         # Tell the user what options they have
1085         #
1086         Header("BackupPC: Restore Options for $host");
1087         print <<EOF;
1088 ${h1("Restore Options for $host")}
1089 <p>
1090 You have selected the following files/directories from
1091 share $share, backup number #$num:
1092 <ul>
1093 $fileListStr
1094 </ul>
1095 <p>
1096 You have three choices for restoring these files/directories.
1097 Please select one of the following options.
1098 <p>
1099 ${h2("Option 1: Direct Restore")}
1100 <p>
1101 You can start a restore that will restore these files directly onto
1102 $host.
1103 <p>
1104 <b>Warning:</b> any existing files that match the ones you have
1105 selected will be overwritten!
1106
1107 <form action="$MyURL" method="post">
1108 <input type="hidden" name="host" value="${EscapeHTML($host)}">
1109 <input type="hidden" name="num" value="$num">
1110 <input type="hidden" name="type" value="3">
1111 $hiddenStr
1112 <input type="hidden" value="$In{action}" name="action">
1113 <table border="0">
1114 <tr>
1115     <td>Restore the files to host</td>
1116     <td><input type="text" size="40" value="${EscapeHTML($host)}"
1117          name="hostDest"></td>
1118 </tr><tr>
1119     <td>Restore the files to share</td>
1120     <td><input type="text" size="40" value="${EscapeHTML($share)}"
1121          name="shareDest"></td>
1122 </tr><tr>
1123     <td>Restore the files below dir<br>(relative to share)</td>
1124     <td valign="top"><input type="text" size="40" maxlength="256"
1125         value="${EscapeHTML($pathHdr)}" name="pathHdr"></td>
1126 </tr><tr>
1127     <td><input type="submit" value="Start Restore" name=""></td>
1128 </table>
1129 </form>
1130 EOF
1131
1132         #
1133         # Verify that Archive::Zip is available before showing the
1134         # zip restore option
1135         #
1136         if ( eval { require Archive::Zip } ) {
1137             print <<EOF;
1138
1139 ${h2("Option 2: Download Zip archive")}
1140 <p>
1141 You can download a Zip archive containing all the files/directories you have
1142 selected.  You can then use a local application, such as WinZip,
1143 to view or extract any of the files.
1144 <p>
1145 <b>Warning:</b> depending upon which files/directories you have selected,
1146 this archive might be very very large.  It might take many minutes to
1147 create and transfer the archive, and you will need enough local disk
1148 space to store it.
1149 <p>
1150 <form action="$MyURL" method="post">
1151 <input type="hidden" name="host" value="${EscapeHTML($host)}">
1152 <input type="hidden" name="num" value="$num">
1153 <input type="hidden" name="type" value="2">
1154 $hiddenStr
1155 <input type="hidden" value="$In{action}" name="action">
1156 <input type="checkbox" value="1" name="relative" checked> Make archive relative
1157 to ${EscapeHTML($pathHdr eq "" ? "/" : $pathHdr)}
1158 (otherwise archive will contain full paths).
1159 <br>
1160 Compression (0=off, 1=fast,...,9=best)
1161 <input type="text" size="6" value="5" name="compressLevel">
1162 <br>
1163 <input type="submit" value="Download Zip File" name="">
1164 </form>
1165 EOF
1166         } else {
1167             print <<EOF;
1168
1169 ${h2("Option 2: Download Zip archive")}
1170 <p>
1171 You could download a zip archive, but Archive::Zip is not installed.
1172 Please ask your system adminstrator to install Archive::Zip from
1173 <a href="http://www.cpan.org">www.cpan.org</a>.
1174 <p>
1175 EOF
1176         }
1177         print <<EOF;
1178 ${h2("Option 3: Download Tar archive")}
1179 <p>
1180 You can download a Tar archive containing all the files/directories you
1181 have selected.  You can then use a local application, such as tar or
1182 WinZip to view or extract any of the files.
1183 <p>
1184 <b>Warning:</b> depending upon which files/directories you have selected,
1185 this archive might be very very large.  It might take many minutes to
1186 create and transfer the archive, and you will need enough local disk
1187 space to store it.
1188 <p>
1189 <form action="$MyURL" method="post">
1190 <input type="hidden" name="host" value="${EscapeHTML($host)}">
1191 <input type="hidden" name="num" value="$num">
1192 <input type="hidden" name="type" value="1">
1193 $hiddenStr
1194 <input type="hidden" value="$In{action}" name="action">
1195 <input type="checkbox" value="1" name="relative" checked> Make archive relative
1196 to ${EscapeHTML($pathHdr eq "" ? "/" : $pathHdr)}
1197 (otherwise archive will contain full paths).
1198 <br>
1199 <input type="submit" value="Download Tar File" name="">
1200 </form>
1201 EOF
1202         Trailer();
1203     } elsif ( $In{type} == 1 ) {
1204         #
1205         # Provide the selected files via a tar archive.
1206         #
1207         $SIG{CHLD} = 'IGNORE';
1208         my $pid = fork();
1209         if ( !defined($pid) ) {
1210             $bpc->ServerMesg("log Can't fork for tar restore request by $User");
1211             ErrorExit("Can't fork for tar restore");
1212         }
1213         if ( $pid ) {
1214             #
1215             # This is the parent.
1216             #
1217             my @fileListTrim = @fileList;
1218             if ( @fileListTrim > 10 ) {
1219                 @fileListTrim = (@fileListTrim[0..9], '...');
1220             }
1221             $bpc->ServerMesg("log User $User downloaded tar archive for $host,"
1222                            . " backup $num; files were: "
1223                            . join(", ", @fileListTrim));
1224             return;
1225         }
1226         #
1227         # This is the child.  Print the headers and run BackupPC_tarCreate.
1228         #
1229         my @pathOpts;
1230         if ( $In{relative} ) {
1231             @pathOpts = ("-r", $pathHdr, "-p", "");
1232         }
1233         $bpc->ServerDisconnect();
1234         print "Content-Type: application/x-gtar\n";
1235         print "Content-Transfer-Encoding: binary\n";
1236         print "Content-Disposition: attachment; filename=\"restore.tar\"\n\n";
1237         exec("$BinDir/BackupPC_tarCreate",
1238              "-h", $host,
1239              "-n", $num,
1240              "-s", $share,
1241              @pathOpts,
1242              @fileList
1243         );
1244     } elsif ( $In{type} == 2 ) {
1245         #
1246         # Provide the selected files via a zip archive.
1247         #
1248         $SIG{CHLD} = 'IGNORE';
1249         my $pid = fork();
1250         if ( !defined($pid) ) {
1251             $bpc->ServerMesg("log Can't fork for zip restore request by $User");
1252             ErrorExit("Can't fork for zip restore");
1253         }
1254         if ( $pid ) {
1255             #
1256             # This is the parent.
1257             #
1258             my @fileListTrim = @fileList;
1259             if ( @fileListTrim > 10 ) {
1260                 @fileListTrim = (@fileListTrim[0..9], '...');
1261             }
1262             $bpc->ServerMesg("log User $User downloaded zip archive for $host,"
1263                            . " backup $num; files were: "
1264                            . join(", ", @fileListTrim));
1265             return;
1266         }
1267         #
1268         # This is the child.  Print the headers and run BackupPC_tarCreate.
1269         #
1270         my @pathOpts;
1271         if ( $In{relative} ) {
1272             @pathOpts = ("-r", $pathHdr, "-p", "");
1273         }
1274         $bpc->ServerDisconnect();
1275         print "Content-Type: application/zip\n";
1276         print "Content-Transfer-Encoding: binary\n";
1277         print "Content-Disposition: attachment; filename=\"restore.zip\"\n\n";
1278         $In{compressLevel} = 5 if ( $In{compressLevel} !~ /^\d+$/ );
1279         exec("$BinDir/BackupPC_zipCreate",
1280              "-h", $host,
1281              "-n", $num,
1282              "-c", $In{compressLevel},
1283              "-s", $share,
1284              @pathOpts,
1285              @fileList
1286         );
1287     } elsif ( $In{type} == 3 ) {
1288         #
1289         # Do restore directly onto host
1290         #
1291         if ( !defined($Hosts->{$In{hostDest}}) ) {
1292             ErrorExit("Host ${EscapeHTML($In{hostDest})} doesn't exist");
1293         }
1294         if ( !CheckPermission($In{hostDest}) ) {
1295             ErrorExit("You don't have permission to restore onto host"
1296                     . " ${EscapeHTML($In{hostDest})}");
1297         }
1298         $fileListStr = "";
1299         foreach my $f ( @fileList ) {
1300             my $targetFile = $f;
1301             (my $strippedShare = $share) =~ s/^\///;
1302             (my $strippedShareDest = $In{shareDest}) =~ s/^\///;
1303             substr($targetFile, 0, length($pathHdr)) = $In{pathHdr};
1304             $fileListStr .= <<EOF;
1305 <tr><td>$host:/$strippedShare$f</td><td>$In{hostDest}:/$strippedShareDest$targetFile</td></tr>
1306 EOF
1307         }
1308         Header("BackupPC: Restore Confirm on $host");
1309         print <<EOF;
1310 ${h1("Are you sure?")}
1311 <p>
1312 You are about to start a restore directly to the machine $In{hostDest}.
1313 The following files will be restored to share $In{shareDest}, from
1314 backup number $num:
1315 <p>
1316 <table border>
1317 <tr><td>Original file/dir</td><td>Will be restored to</td></tr>
1318 $fileListStr
1319 </table>
1320
1321 <form action="$MyURL" method="post">
1322 <input type="hidden" name="host" value="${EscapeHTML($host)}">
1323 <input type="hidden" name="hostDest" value="${EscapeHTML($In{hostDest})}">
1324 <input type="hidden" name="shareDest" value="${EscapeHTML($In{shareDest})}">
1325 <input type="hidden" name="pathHdr" value="${EscapeHTML($In{pathHdr})}">
1326 <input type="hidden" name="num" value="$num">
1327 <input type="hidden" name="type" value="4">
1328 $hiddenStr
1329 Do you really want to do this?
1330 <input type="submit" value="$In{action}" name="action">
1331 <input type="submit" value="No" name="">
1332 </form>
1333 EOF
1334         Trailer();
1335     } elsif ( $In{type} == 4 ) {
1336         if ( !defined($Hosts->{$In{hostDest}}) ) {
1337             ErrorExit("Host ${EscapeHTML($In{hostDest})} doesn't exist");
1338         }
1339         if ( !CheckPermission($In{hostDest}) ) {
1340             ErrorExit("You don't have permission to restore onto host"
1341                     . " ${EscapeHTML($In{hostDest})}");
1342         }
1343         my $hostDest = $1 if ( $In{hostDest} =~ /(.+)/ );
1344         my $ipAddr = ConfirmIPAddress($hostDest);
1345         #
1346         # Prepare and send the restore request.  We write the request
1347         # information using Data::Dumper to a unique file,
1348         # $TopDir/pc/$hostDest/restoreReq.$$.n.  We use a file
1349         # in case the list of files to restore is very long.
1350         #
1351         my $reqFileName;
1352         for ( my $i = 0 ; ; $i++ ) {
1353             $reqFileName = "restoreReq.$$.$i";
1354             last if ( !-f "$TopDir/pc/$hostDest/$reqFileName" );
1355         }
1356         my %restoreReq = (
1357             # source of restore is hostSrc, #num, path shareSrc/pathHdrSrc
1358             num         => $In{num},
1359             hostSrc     => $host,
1360             shareSrc    => $share,
1361             pathHdrSrc  => $pathHdr,
1362
1363             # destination of restore is hostDest:shareDest/pathHdrDest
1364             hostDest    => $hostDest,
1365             shareDest   => $In{shareDest},
1366             pathHdrDest => $In{pathHdr},
1367
1368             # list of files to restore
1369             fileList    => \@fileList,
1370
1371             # other info
1372             user        => $User,
1373             reqTime     => time,
1374         );
1375         my($dump) = Data::Dumper->new(
1376                          [  \%restoreReq],
1377                          [qw(*RestoreReq)]);
1378         $dump->Indent(1);
1379         if ( open(REQ, ">$TopDir/pc/$hostDest/$reqFileName") ) {
1380             print(REQ $dump->Dump);
1381             close(REQ);
1382         } else {
1383             ErrorExit("Can't open/create "
1384                     . ${EscapeHTML("$TopDir/pc/$hostDest/$reqFileName")});
1385         }
1386         $reply = $bpc->ServerMesg("restore $ipAddr"
1387                         . " $hostDest $User $reqFileName");
1388         $str = "Restore requested to host $hostDest, backup #$num,"
1389              . " by $User from $ENV{REMOTE_ADDR}";
1390         Header("BackupPC: Restore Requested on $hostDest");
1391         print <<EOF;
1392 ${h1($str)}
1393 <p>
1394 Reply from server was: $reply
1395 <p>
1396 Go back to <a href="$MyURL?host=$hostDest">$hostDest home page</a>.
1397 EOF
1398         Trailer();
1399     }
1400 }
1401
1402 sub Action_RestoreFile
1403 {
1404     restoreFile($In{host}, $In{num}, $In{dir});
1405 }
1406
1407 sub restoreFile
1408 {
1409     my($host, $num, $dir, $skipHardLink, $origName) = @_;
1410     my($Privileged) = CheckPermission($host);
1411     my($i, $numF, $mangleF, $compressF, $mangle, $compress, $dirUM);
1412     #
1413     # Some common content (media) types from www.iana.org (via MIME::Types).
1414     #
1415     my $Ext2ContentType = {
1416         'asc'  => 'text/plain',
1417         'avi'  => 'video/x-msvideo',
1418         'bmp'  => 'image/bmp',
1419         'book' => 'application/x-maker',
1420         'cc'   => 'text/plain',
1421         'cpp'  => 'text/plain',
1422         'csh'  => 'application/x-csh',
1423         'csv'  => 'text/comma-separated-values',
1424         'c'    => 'text/plain',
1425         'deb'  => 'application/x-debian-package',
1426         'doc'  => 'application/msword',
1427         'dot'  => 'application/msword',
1428         'dtd'  => 'text/xml',
1429         'dvi'  => 'application/x-dvi',
1430         'eps'  => 'application/postscript',
1431         'fb'   => 'application/x-maker',
1432         'fbdoc'=> 'application/x-maker',
1433         'fm'   => 'application/x-maker',
1434         'frame'=> 'application/x-maker',
1435         'frm'  => 'application/x-maker',
1436         'gif'  => 'image/gif',
1437         'gtar' => 'application/x-gtar',
1438         'gz'   => 'application/x-gzip',
1439         'hh'   => 'text/plain',
1440         'hpp'  => 'text/plain',
1441         'h'    => 'text/plain',
1442         'html' => 'text/html',
1443         'htmlx'=> 'text/html',
1444         'htm'  => 'text/html',
1445         'iges' => 'model/iges',
1446         'igs'  => 'model/iges',
1447         'jpeg' => 'image/jpeg',
1448         'jpe'  => 'image/jpeg',
1449         'jpg'  => 'image/jpeg',
1450         'js'   => 'application/x-javascript',
1451         'latex'=> 'application/x-latex',
1452         'maker'=> 'application/x-maker',
1453         'mid'  => 'audio/midi',
1454         'midi' => 'audio/midi',
1455         'movie'=> 'video/x-sgi-movie',
1456         'mov'  => 'video/quicktime',
1457         'mp2'  => 'audio/mpeg',
1458         'mp3'  => 'audio/mpeg',
1459         'mpeg' => 'video/mpeg',
1460         'mpg'  => 'video/mpeg',
1461         'mpp'  => 'application/vnd.ms-project',
1462         'pdf'  => 'application/pdf',
1463         'pgp'  => 'application/pgp-signature',
1464         'php'  => 'application/x-httpd-php',
1465         'pht'  => 'application/x-httpd-php',
1466         'phtml'=> 'application/x-httpd-php',
1467         'png'  => 'image/png',
1468         'ppm'  => 'image/x-portable-pixmap',
1469         'ppt'  => 'application/powerpoint',
1470         'ppt'  => 'application/vnd.ms-powerpoint',
1471         'ps'   => 'application/postscript',
1472         'qt'   => 'video/quicktime',
1473         'rgb'  => 'image/x-rgb',
1474         'rtf'  => 'application/rtf',
1475         'rtf'  => 'text/rtf',
1476         'shar' => 'application/x-shar',
1477         'shtml'=> 'text/html',
1478         'swf'  => 'application/x-shockwave-flash',
1479         'tex'  => 'application/x-tex',
1480         'texi' => 'application/x-texinfo',
1481         'texinfo'=> 'application/x-texinfo',
1482         'tgz'  => 'application/x-gtar',
1483         'tiff' => 'image/tiff',
1484         'tif'  => 'image/tiff',
1485         'txt'  => 'text/plain',
1486         'vcf'  => 'text/x-vCard',
1487         'vrml' => 'model/vrml',
1488         'wav'  => 'audio/x-wav',
1489         'wmls' => 'text/vnd.wap.wmlscript',
1490         'wml'  => 'text/vnd.wap.wml',
1491         'wrl'  => 'model/vrml',
1492         'xls'  => 'application/vnd.ms-excel',
1493         'xml'  => 'text/xml',
1494         'xwd'  => 'image/x-xwindowdump',
1495         'z'    => 'application/x-compress',
1496         'zip'  => 'application/zip',
1497     };
1498
1499     if ( !$Privileged ) {
1500         ErrorExit("Only privileged users can restore backup files"
1501                 . " for host ${EscapeHTML($host)}." );
1502     }
1503     ServerConnect();
1504     my @Backups = $bpc->BackupInfoRead($host);
1505     if ( $host eq "" ) {
1506         ErrorExit("Empty host name");
1507     }
1508     $dir = "/" if ( $dir eq "" );
1509     for ( $i = 0 ; $i < @Backups ; $i++ ) {
1510         if ( !$Backups[$i]{noFill} ) {
1511             $numF      = $Backups[$i]{num};
1512             $mangleF   = $Backups[$i]{mangle};
1513             $compressF = $Backups[$i]{compress};
1514         }
1515         last if ( $Backups[$i]{num} == $num );
1516     }
1517     $mangle = $Backups[$i]{mangle};
1518     $compress = $Backups[$i]{compress};
1519     if ( !$Backups[$i]{noFill} ) {
1520         # no need to back-fill a filled backup
1521         $numF = $mangleF = $compressF = undef;
1522     }
1523     my $fullPath = "$TopDir/pc/$host/$num/$dir";
1524     $fullPath =~ s{/+}{/}g;
1525     if ( !-f $fullPath && defined($numF) ) {
1526         my $dirF = $dir;
1527         my $fullPathF;
1528         if ( $mangle && !$mangleF ) {
1529             $fullPathF = "$TopDir/pc/$host/$numF/"
1530                                 . $bpc->fileNameUnmangle($dir);
1531         } else {
1532             $fullPathF = "$TopDir/pc/$host/$numF/$dir";
1533         }
1534         if ( -f $fullPathF ) {
1535             $fullPath = $fullPathF;
1536             $compress = $compressF;
1537         }
1538     }
1539     if ( $fullPath =~ m{(^|/)\.\.(/|$)} || !-f $fullPath ) {
1540         ErrorExit("Can't restore bad file ${EscapeHTML($fullPath)}");
1541     }
1542     my $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
1543     my $attr = BackupPC::Attrib->new({compress => $compress});
1544     my $fullDir = $fullPath;
1545     $fullDir =~ s{(.*)/.*}{$1};
1546     my $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
1547     $attr->read($fullDir) if ( -f $attr->fileName($fullDir) );
1548     my $a = $attr->get($fileName);
1549
1550     my $f = BackupPC::FileZIO->open($fullPath, 0, $compress);
1551     my $data;
1552     if ( !$skipHardLink && $a->{type} == BPC_FTYPE_HARDLINK ) {
1553         #
1554         # hardlinks should look like the file they point to
1555         #
1556         my $linkName;
1557         while ( $f->read(\$data, 65536) > 0 ) {
1558             $linkName .= $data;
1559         }
1560         $f->close;
1561         $linkName =~ s/^\.\///;
1562         my $share = $1 if ( $dir =~ /^\/?(.*?)\// );
1563         restoreFile($host, $num,
1564                 "$share/" . ($mangle ? $bpc->fileNameMangle($linkName)
1565                                      : $linkName), 1, $dir);
1566         return;
1567     }
1568     $dirUM =~ s{//}{/}g;
1569     $fullPath =~ s{//}{/}g;
1570     $bpc->ServerMesg("log User $User recovered file $dirUM ($fullPath)");
1571     $dir = $origName if ( defined($origName) );
1572     $dirUM = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
1573     my $ext = $1 if ( $dirUM =~ /\.([^\/\.]+)$/ );
1574     my $contentType = $Ext2ContentType->{lc($ext)}
1575                                     || "application/octet-stream";
1576     $fileName = $1 if ( $dirUM =~ /.*\/(.*)/ );
1577     $fileName =~ s/"/\\"/g;
1578     print "Content-Type: $contentType\n";
1579     print "Content-Transfer-Encoding: binary\n";
1580     print "Content-Disposition: attachment; filename=\"$fileName\"\n\n";
1581     while ( $f->read(\$data, 1024 * 1024) > 0 ) {
1582         print STDOUT $data;
1583     }
1584     $f->close;
1585 }
1586
1587 sub Action_HostInfo
1588 {
1589     my $host = $1 if ( $In{host} =~ /(.*)/ );
1590     my($statusStr, $startIncrStr);
1591
1592     $host =~ s/^\s+//;
1593     $host =~ s/\s+$//;
1594     return Action_GeneralInfo() if ( $host eq "" );
1595     $host = lc($host)
1596                 if ( !-d "$TopDir/pc/$host" && -d "$TopDir/pc/" . lc($host) );
1597     if ( $host =~ /\.\./ || !-d "$TopDir/pc/$host" ) {
1598         #
1599         # try to lookup by user name
1600         #
1601         if ( !defined($Hosts->{$host}) ) {
1602             foreach my $h ( keys(%$Hosts) ) {
1603                 if ( $Hosts->{$h}{user} eq $host
1604                         || lc($Hosts->{$h}{user}) eq lc($host) ) {
1605                     $host = $h;
1606                     last;
1607                 }
1608             }
1609             CheckPermission();
1610             ErrorExit("Unknown host or user ${EscapeHTML($host)}")
1611                                 if ( !defined($Hosts->{$host}) );
1612         }
1613         $In{host} = $host;
1614     }
1615     GetStatusInfo("host($host)");
1616     $bpc->ConfigRead($host);
1617     %Conf = $bpc->Conf();
1618     my $Privileged = CheckPermission($host);
1619     if ( !$Privileged ) {
1620         ErrorExit("Only privileged users can view information about"
1621                 . " host ${EscapeHTML($host)}." );
1622     }
1623     ReadUserEmailInfo();
1624
1625     my @Backups = $bpc->BackupInfoRead($host);
1626     my($str, $sizeStr, $compStr, $errStr, $warnStr);
1627     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
1628         my $startTime = timeStamp2($Backups[$i]{startTime});
1629         my $dur       = $Backups[$i]{endTime} - $Backups[$i]{startTime};
1630         $dur          = 1 if ( $dur <= 0 );
1631         my $duration  = sprintf("%.1f", $dur / 60);
1632         my $MB        = sprintf("%.1f", $Backups[$i]{size} / (1024*1024));
1633         my $MBperSec  = sprintf("%.2f", $Backups[$i]{size} / (1024*1024*$dur));
1634         my $MBExist   = sprintf("%.1f", $Backups[$i]{sizeExist} / (1024*1024));
1635         my $MBNew     = sprintf("%.1f", $Backups[$i]{sizeNew} / (1024*1024));
1636         my($MBExistComp, $ExistComp, $MBNewComp, $NewComp);
1637         if ( $Backups[$i]{sizeExist} && $Backups[$i]{sizeExistComp} ) {
1638             $MBExistComp = sprintf("%.1f", $Backups[$i]{sizeExistComp}
1639                                                 / (1024 * 1024));
1640             $ExistComp = sprintf("%.1f%%", 100 *
1641                   (1 - $Backups[$i]{sizeExistComp} / $Backups[$i]{sizeExist}));
1642         }
1643         if ( $Backups[$i]{sizeNew} && $Backups[$i]{sizeNewComp} ) {
1644             $MBNewComp = sprintf("%.1f", $Backups[$i]{sizeNewComp}
1645                                                 / (1024 * 1024));
1646             $NewComp = sprintf("%.1f%%", 100 *
1647                   (1 - $Backups[$i]{sizeNewComp} / $Backups[$i]{sizeNew}));
1648         }
1649         my $age = sprintf("%.1f", (time - $Backups[$i]{startTime}) / (24*3600));
1650         my $browseURL = "$MyURL?action=browse&host=$host&num=$Backups[$i]{num}";
1651         my $filled = $Backups[$i]{noFill} ? "no" : "yes";
1652         $filled .= " ($Backups[$i]{fillFromNum}) "
1653                             if ( $Backups[$i]{fillFromNum} ne "" );
1654         $str .= <<EOF;
1655 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1656     <td align="center"> $Backups[$i]{type} </td>
1657     <td align="center"> $filled </td>
1658     <td align="right">  $startTime </td>
1659     <td align="right">  $duration </td>
1660     <td align="right">  $age </td>
1661     <td align="left">   <tt>$TopDir/pc/$host/$Backups[$i]{num}</tt> </td></tr>
1662 EOF
1663         $sizeStr .= <<EOF;
1664 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1665     <td align="center"> $Backups[$i]{type} </td>
1666     <td align="right">  $Backups[$i]{nFiles} </td>
1667     <td align="right">  $MB </td>
1668     <td align="right">  $MBperSec </td>
1669     <td align="right">  $Backups[$i]{nFilesExist} </td>
1670     <td align="right">  $MBExist </td>
1671     <td align="right">  $Backups[$i]{nFilesNew} </td>
1672     <td align="right">  $MBNew </td>
1673 </tr>
1674 EOF
1675         $Backups[$i]{compress} ||= "off";
1676         $compStr .= <<EOF;
1677 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1678     <td align="center"> $Backups[$i]{type} </td>
1679     <td align="center"> $Backups[$i]{compress} </td>
1680     <td align="right">  $MBExist </td>
1681     <td align="right">  $MBExistComp </td>
1682     <td align="right">  $ExistComp </td>
1683     <td align="right">  $MBNew </td>
1684     <td align="right">  $MBNewComp </td>
1685     <td align="right">  $NewComp </td>
1686 </tr>
1687 EOF
1688         $errStr .= <<EOF;
1689 <tr><td align="center"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
1690     <td align="center"> $Backups[$i]{type} </td>
1691     <td align="center"> <a href="$MyURL?action=view&type=XferLOG&num=$Backups[$i]{num}&host=$host">XferLOG</a>,
1692                       <a href="$MyURL?action=view&type=XferErr&num=$Backups[$i]{num}&host=$host">Errors</a> </td>
1693     <td align="right">  $Backups[$i]{xferErrs} </td>
1694     <td align="right">  $Backups[$i]{xferBadFile} </td>
1695     <td align="right">  $Backups[$i]{xferBadShare} </td>
1696     <td align="right">  $Backups[$i]{tarErrs} </td></tr>
1697 EOF
1698     }
1699
1700     my @Restores = $bpc->RestoreInfoRead($host);
1701     my $restoreStr;
1702
1703     for ( my $i = 0 ; $i < @Restores ; $i++ ) {
1704         my $startTime = timeStamp2($Restores[$i]{startTime});
1705         my $dur       = $Restores[$i]{endTime} - $Restores[$i]{startTime};
1706         $dur          = 1 if ( $dur <= 0 );
1707         my $duration  = sprintf("%.1f", $dur / 60);
1708         my $MB        = sprintf("%.1f", $Restores[$i]{size} / (1024*1024));
1709         my $MBperSec  = sprintf("%.2f", $Restores[$i]{size} / (1024*1024*$dur));
1710         $restoreStr  .= <<EOF;
1711 <tr><td align="center"><a href="$MyURL?action=restoreInfo&num=$Restores[$i]{num}&host=$host">$Restores[$i]{num}</a> </td>
1712     <td align="center"> $Restores[$i]{result} </td>
1713     <td align="right"> $startTime </td>
1714     <td align="right"> $duration </td>
1715     <td align="right"> $Restores[$i]{nFiles} </td>
1716     <td align="right"> $MB </td>
1717     <td align="right"> $Restores[$i]{tarCreateErrs} </td>
1718     <td align="right"> $Restores[$i]{xferErrs} </td>
1719 </tr>
1720 EOF
1721     }
1722     $restoreStr   = <<EOF if ( $restoreStr ne "" );
1723 ${h2("Restore Summary")}
1724 <p>
1725 Click on the restore number for more details.
1726 <table border>
1727 <tr><td align="center"> Restore# </td>
1728     <td align="center"> Result </td>
1729     <td align="right"> Start Date</td>
1730     <td align="right"> Dur/mins</td>
1731     <td align="right"> #files </td>
1732     <td align="right"> MB </td>
1733     <td align="right"> #tar errs </td>
1734     <td align="right"> #xferErrs </td>
1735 </tr>
1736 $restoreStr
1737 </table>
1738 <p>
1739 EOF
1740
1741     if ( @Backups == 0 ) {
1742         $warnStr = "<h2> This PC has never been backed up!! </h2>\n";
1743     }
1744     if ( defined($Hosts->{$host}) ) {
1745         my $user = $Hosts->{$host}{user};
1746         if ( $user ne "" ) {
1747             $statusStr .= <<EOF;
1748 <li>This PC is used by ${UserLink($user)}.
1749 EOF
1750         }
1751         if ( defined($UserEmailInfo{$user})
1752                 && $UserEmailInfo{$user}{lastHost} eq $host ) {
1753             my $mailTime = timeStamp2($UserEmailInfo{$user}{lastTime});
1754             my $subj     = $UserEmailInfo{$user}{lastSubj};
1755             $statusStr  .= <<EOF;
1756 <li>Last email sent to ${UserLink($user)} was at $mailTime, subject "$subj".
1757 EOF
1758         }
1759     }
1760     if ( defined($Jobs{$host}) ) {
1761         my $startTime = timeStamp2($Jobs{$host}{startTime});
1762         (my $cmd = $Jobs{$host}{cmd}) =~ s/$BinDir\///g;
1763         $statusStr .= <<EOF;
1764 <li>The command $cmd is currently running for $host, started $startTime.
1765 EOF
1766     }
1767     if ( $StatusHost{BgQueueOn} ) {
1768         $statusStr .= <<EOF;
1769 <li>Host $host is queued on the background queue (will be backed up soon).
1770 EOF
1771     }
1772     if ( $StatusHost{UserQueueOn} ) {
1773         $statusStr .= <<EOF;
1774 <li>Host $host is queued on the user queue (will be backed up soon).
1775 EOF
1776     }
1777     if ( $StatusHost{CmdQueueOn} ) {
1778         $statusStr .= <<EOF;
1779 <li>A command for $host is on the command queue (will run soon).
1780 EOF
1781     }
1782     my $startTime = timeStamp2($StatusHost{endTime} == 0 ?
1783                 $StatusHost{startTime} : $StatusHost{endTime});
1784     my $reason = "";
1785     if ( $StatusHost{reason} ne "" ) {
1786         $reason = " ($StatusHost{reason})";
1787     }
1788     $statusStr .= <<EOF;
1789 <li>Last status is state "$StatusHost{state}"$reason
1790     as of $startTime.
1791 EOF
1792     if ( $StatusHost{error} ne "" ) {
1793         $statusStr .= <<EOF;
1794 <li>Last error is "${EscapeHTML($StatusHost{error})}"
1795 EOF
1796     }
1797     my $priorStr = "Pings";
1798     if ( $StatusHost{deadCnt} > 0 ) {
1799         $statusStr .= <<EOF;
1800 <li>Pings to $host have failed $StatusHost{deadCnt} consecutive times.
1801 EOF
1802         $priorStr = "Prior to that, pings";
1803     }
1804     if ( $StatusHost{aliveCnt} > 0 ) {
1805         $statusStr .= <<EOF;
1806 <li>$priorStr to $host have succeeded $StatusHost{aliveCnt}
1807         consecutive times.
1808 EOF
1809         if ( $StatusHost{aliveCnt} >= $Conf{BlackoutGoodCnt}
1810                 && $Conf{BlackoutGoodCnt} >= 0 && $Conf{BlackoutHourBegin} >= 0
1811                 && $Conf{BlackoutHourEnd} >= 0 ) {
1812             my(@days) = qw(Sun Mon Tue Wed Thu Fri Sat);
1813             my($days) = join(", ", @days[@{$Conf{BlackoutWeekDays}}]);
1814             my($t0) = sprintf("%d:%02d", $Conf{BlackoutHourBegin},
1815                             60 * ($Conf{BlackoutHourBegin}
1816                                      - int($Conf{BlackoutHourBegin})));
1817             my($t1) = sprintf("%d:%02d", $Conf{BlackoutHourEnd},
1818                             60 * ($Conf{BlackoutHourEnd}
1819                                      - int($Conf{BlackoutHourEnd})));
1820             $statusStr .= <<EOF;
1821 <li>Because $host has been on the network at least $Conf{BlackoutGoodCnt}
1822 consecutive times, it will not be backed up from $t0 to $t1 on $days.
1823 EOF
1824         }
1825     }
1826     if ( $StatusHost{backoffTime} > time ) {
1827         my $hours = sprintf("%.1f", ($StatusHost{backoffTime} - time) / 3600);
1828         $statusStr .= <<EOF;
1829 <li>Backups are deferred for $hours hours
1830     (<a href="$MyURL?action=Stop/Dequeue%20Backup&host=$host">change this
1831     number</a>).
1832 EOF
1833     }
1834     if ( @Backups ) {
1835         # only allow incremental if there are already some backups
1836         $startIncrStr = <<EOF;
1837 <input type="submit" value="Start Incr Backup" name="action">
1838 EOF
1839     }
1840
1841     Header("BackupPC: Host $host Backup Summary");
1842     print <<EOF;
1843 ${h1("Host $host Backup Summary")}
1844 <p>
1845 $warnStr
1846 <ul>
1847 $statusStr
1848 </ul>
1849
1850 ${h2("User Actions")}
1851 <p>
1852 <form action="$MyURL" method="get">
1853 <input type="hidden" name="host" value="$host">
1854 $startIncrStr
1855 <input type="submit" value="Start Full Backup" name="action">
1856 <input type="submit" value="Stop/Dequeue Backup" name="action">
1857 </form>
1858
1859 ${h2("Backup Summary")}
1860 <p>
1861 Click on the backup number to browse and restore backup files.
1862 <table border>
1863 <tr><td align="center"> Backup# </td>
1864     <td align="center"> Type </td>
1865     <td align="center"> Filled </td>
1866     <td align="center"> Start Date </td>
1867     <td align="center"> Duration/mins </td>
1868     <td align="center"> Age/days </td>
1869     <td align="center"> Server Backup Path </td>
1870 </tr>
1871 $str
1872 </table>
1873 <p>
1874
1875 $restoreStr
1876
1877 ${h2("Xfer Error Summary")}
1878 <p>
1879 <table border>
1880 <tr><td align="center"> Backup# </td>
1881     <td align="center"> Type </td>
1882     <td align="center"> View </td>
1883     <td align="center"> #Xfer errs </td>
1884     <td align="center"> #bad files </td>
1885     <td align="center"> #bad share </td>
1886     <td align="center"> #tar errs </td>
1887 </tr>
1888 $errStr
1889 </table>
1890 <p>
1891
1892 ${h2("File Size/Count Reuse Summary")}
1893 <p>
1894 Existing files are those already in the pool; new files are those added
1895 to the pool.
1896 Empty files and SMB errors aren't counted in the reuse and new counts.
1897 <table border>
1898 <tr><td colspan="2"></td>
1899     <td align="center" colspan="3"> Totals </td>
1900     <td align="center" colspan="2"> Existing Files </td>
1901     <td align="center" colspan="2"> New Files </td>
1902 </tr>
1903 <tr>
1904     <td align="center"> Backup# </td>
1905     <td align="center"> Type </td>
1906     <td align="center"> #Files </td>
1907     <td align="center"> Size/MB </td>
1908     <td align="center"> MB/sec </td>
1909     <td align="center"> #Files </td>
1910     <td align="center"> Size/MB </td>
1911     <td align="center"> #Files </td>
1912     <td align="center"> Size/MB </td>
1913 </tr>
1914 $sizeStr
1915 </table>
1916 <p>
1917
1918 ${h2("Compression Summary")}
1919 <p>
1920 Compression performance for files already in the pool and newly
1921 compressed files.
1922 <table border>
1923 <tr><td colspan="3"></td>
1924     <td align="center" colspan="3"> Existing Files </td>
1925     <td align="center" colspan="3"> New Files </td>
1926 </tr>
1927 <tr><td align="center"> Backup# </td>
1928     <td align="center"> Type </td>
1929     <td align="center"> Comp Level </td>
1930     <td align="center"> Size/MB </td>
1931     <td align="center"> Comp/MB </td>
1932     <td align="center"> Comp </td>
1933     <td align="center"> Size/MB </td>
1934     <td align="center"> Comp/MB </td>
1935     <td align="center"> Comp </td>
1936 </tr>
1937 $compStr
1938 </table>
1939 <p>
1940 EOF
1941     Trailer();
1942 }
1943
1944 sub Action_GeneralInfo
1945 {
1946     GetStatusInfo("info jobs hosts queueLen");
1947     my $Privileged = CheckPermission();
1948
1949     my($jobStr, $statusStr, $tarPidHdr, $ rivLinks);
1950     foreach my $host ( sort(keys(%Jobs)) ) {
1951         my $startTime = timeStamp2($Jobs{$host}{startTime});
1952         next if ( $host eq $bpc->trashJob
1953                     && $Jobs{$host}{processState} ne "running" );
1954         $Jobs{$host}{type} = $Status{$host}{type}
1955                     if ( $Jobs{$host}{type} eq "" && defined($Status{$host}));
1956         (my $cmd = $Jobs{$host}{cmd}) =~ s/$BinDir\///g;
1957         $jobStr .= <<EOF;
1958 <tr><td> ${HostLink($host)} </td>
1959     <td align="center"> $Jobs{$host}{type} </td>
1960     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
1961     <td> $startTime </td>
1962     <td> $cmd </td>
1963     <td align="center"> $Jobs{$host}{pid} </td>
1964     <td align="center"> $Jobs{$host}{xferPid} </td>
1965 EOF
1966         if ( $Jobs{$host}{tarPid} > 0 ) {
1967             $jobStr .= "    <td align=\"center\"> $Jobs{$host}{tarPid} </td>\n";
1968             $tarPidHdr ||= "<td align=\"center\"> tar PID </td>\n";
1969         }
1970         $jobStr .= "</tr>\n";
1971     }
1972     foreach my $host ( sort(keys(%Status)) ) {
1973         next if ( $Status{$host}{reason} ne "backup failed" );
1974         my $startTime = timeStamp2($Status{$host}{startTime});
1975         my($errorTime, $XferViewStr);
1976         if ( $Status{$host}{errorTime} > 0 ) {
1977             $errorTime = timeStamp2($Status{$host}{errorTime});
1978         }
1979         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
1980                 || -f "$TopDir/pc/$host/SmbLOG.bad.z"
1981                 || -f "$TopDir/pc/$host/XferLOG.bad"
1982                 || -f "$TopDir/pc/$host/XferLOG.bad.z"
1983                 ) {
1984             $XferViewStr = <<EOF;
1985 <a href="$MyURL?action=view&type=XferLOGbad&host=$host">XferLOG</a>,
1986 <a href="$MyURL?action=view&type=XferErrbad&host=$host">XferErr</a>
1987 EOF
1988         } else {
1989             $XferViewStr = "";
1990         }
1991         (my $shortErr = $Status{$host}{error}) =~ s/(.{48}).*/$1.../;   
1992         $statusStr .= <<EOF;
1993 <tr><td> ${HostLink($host)} </td>
1994     <td align="center"> $Status{$host}{type} </td>
1995     <td align="center"> ${UserLink($Hosts->{$host}{user})} </td>
1996     <td align="right"> $startTime </td>
1997     <td> $XferViewStr </td>
1998     <td align="right"> $errorTime </td>
1999     <td> ${EscapeHTML($shortErr)} </td></tr>
2000 EOF
2001     }
2002     my $now          = timeStamp2(time);
2003     my $nextWakeupTime = timeStamp2($Info{nextWakeup});
2004     my $DUlastTime   = timeStamp2($Info{DUlastValueTime});
2005     my $DUmaxTime    = timeStamp2($Info{DUDailyMaxTime});
2006     my $numBgQueue   = $QueueLen{BgQueue};
2007     my $numUserQueue = $QueueLen{UserQueue};
2008     my $numCmdQueue  = $QueueLen{CmdQueue};
2009     my $serverStartTime = timeStamp2($Info{startTime});
2010     my $poolInfo     = genPoolInfo("pool", \%Info);
2011     my $cpoolInfo    = genPoolInfo("cpool", \%Info);
2012     if ( $Info{poolFileCnt} > 0 && $Info{cpoolFileCnt} > 0 ) {
2013         $poolInfo = <<EOF;
2014 <li>Uncompressed pool:
2015 <ul>
2016 $poolInfo
2017 </ul>
2018 <li>Compressed pool:
2019 <ul>
2020 $cpoolInfo
2021 </ul>
2022 EOF
2023     } elsif ( $Info{cpoolFileCnt} > 0 ) {
2024         $poolInfo = $cpoolInfo;
2025     }
2026     Header("BackupPC: Server Status");
2027     print <<EOF;
2028
2029 ${h1("BackupPC Server Status")}
2030 <p>
2031
2032 ${h2("General Server Information")}
2033
2034 <ul>
2035 <li> The server's PID is $Info{pid} on host $Conf{ServerHost},
2036      version $Info{Version}, started at $serverStartTime.
2037 <li> This status was generated at $now.
2038 <li> PCs will be next queued at $nextWakeupTime.
2039 <li> Other info:
2040     <ul>
2041         <li>$numBgQueue pending backup requests from last scheduled wakeup,
2042         <li>$numUserQueue pending user backup requests,
2043         <li>$numCmdQueue pending command requests,
2044         $poolInfo
2045         <li>Pool file system was recently at $Info{DUlastValue}%
2046             ($DUlastTime), today's max is $Info{DUDailyMax}% ($DUmaxTime)
2047             and yesterday's max was $Info{DUDailyMaxPrev}%.
2048     </ul>
2049 </ul>
2050
2051 ${h2("Currently Running Jobs")}
2052 <p>
2053 <table border>
2054 <tr><td> Host </td>
2055     <td> Type </td>
2056     <td> User </td>
2057     <td> Start Time </td>
2058     <td> Command </td>
2059     <td align="center"> PID </td>
2060     <td> Xfer PID </td>
2061     $tarPidHdr</tr>
2062 $jobStr
2063 </table>
2064 <p>
2065
2066 ${h2("Failures that need attention")}
2067 <p>
2068 <table border>
2069 <tr><td align="center"> Host </td>
2070     <td align="center"> Type </td>
2071     <td align="center"> User </td>
2072     <td align="center"> Last Try </td>
2073     <td align="center"> Details </td>
2074     <td align="center"> Error Time </td>
2075     <td> Last error (other than no ping) </td></tr>
2076 $statusStr
2077 </table>
2078 EOF
2079     Trailer();
2080 }
2081
2082 sub Action_RestoreInfo
2083 {
2084     my $Privileged = CheckPermission($In{host});
2085     my $host = $1 if ( $In{host} =~ /(.*)/ );
2086     my $num  = $In{num};
2087     my $i;
2088
2089     if ( !$Privileged ) {
2090         ErrorExit("Only privileged users can view restore information." );
2091     }
2092     #
2093     # Find the requested restore
2094     #
2095     my @Restores = $bpc->RestoreInfoRead($host);
2096     for ( $i = 0 ; $i < @Restores ; $i++ ) {
2097         last if ( $Restores[$i]{num} == $num );
2098     }
2099     if ( $i >= @Restores ) {
2100         ErrorExit("Restore number $num for host ${EscapeHTML($host)} does"
2101                 . " not exist.");
2102     }
2103
2104     %RestoreReq = ();
2105     do "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}"
2106             if ( -f "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}" );
2107
2108     my $startTime = timeStamp2($Restores[$i]{startTime});
2109     my $reqTime   = timeStamp2($RestoreReq{reqTime});
2110     my $dur       = $Restores[$i]{endTime} - $Restores[$i]{startTime};
2111     $dur          = 1 if ( $dur <= 0 );
2112     my $duration  = sprintf("%.1f", $dur / 60);
2113     my $MB        = sprintf("%.1f", $Restores[$i]{size} / (1024*1024));
2114     my $MBperSec  = sprintf("%.2f", $Restores[$i]{size} / (1024*1024*$dur));
2115
2116     my $fileListStr = "";
2117     foreach my $f ( @{$RestoreReq{fileList}} ) {
2118         my $targetFile = $f;
2119         (my $strippedShareSrc  = $RestoreReq{shareSrc}) =~ s/^\///;
2120         (my $strippedShareDest = $RestoreReq{shareDest}) =~ s/^\///;
2121         substr($targetFile, 0, length($RestoreReq{pathHdrSrc}))
2122                                         = $RestoreReq{pathHdrDest};
2123         $fileListStr .= <<EOF;
2124 <tr><td>$RestoreReq{hostSrc}:/$strippedShareSrc$f</td><td>$RestoreReq{hostDest}:/$strippedShareDest$targetFile</td></tr>
2125 EOF
2126     }
2127
2128     Header("BackupPC: Restore #$num details for $host");
2129     print <<EOF;
2130 ${h1("Restore #$num Details for $host")}
2131 <p>
2132 <table border>
2133 <tr><td> Number </td><td> $Restores[$i]{num} </td></tr>
2134 <tr><td> Requested by </td><td> $RestoreReq{user} </td></tr>
2135 <tr><td> Request time </td><td> $reqTime </td></tr>
2136 <tr><td> Result </td><td> $Restores[$i]{result} </td></tr>
2137 <tr><td> Error Message </td><td> $Restores[$i]{errorMsg} </td></tr>
2138 <tr><td> Source host </td><td> $RestoreReq{hostSrc} </td></tr>
2139 <tr><td> Source backup num </td><td> $RestoreReq{num} </td></tr>
2140 <tr><td> Source share </td><td> $RestoreReq{shareSrc} </td></tr>
2141 <tr><td> Destination host </td><td> $RestoreReq{hostDest} </td></tr>
2142 <tr><td> Destination share </td><td> $RestoreReq{shareDest} </td></tr>
2143 <tr><td> Start time </td><td> $startTime </td></tr>
2144 <tr><td> Duration </td><td> $duration min </td></tr>
2145 <tr><td> Number of files </td><td> $Restores[$i]{nFiles} </td></tr>
2146 <tr><td> Total size </td><td> ${MB} MB </td></tr>
2147 <tr><td> Transfer rate </td><td> $MBperSec MB/sec </td></tr>
2148 <tr><td> TarCreate errors </td><td> $Restores[$i]{tarCreateErrs} </td></tr>
2149 <tr><td> Xfer errors </td><td> $Restores[$i]{xferErrs} </td></tr>
2150 <tr><td> Xfer log file </td><td>
2151 <a href="$MyURL?action=view&type=RestoreLOG&num=$Restores[$i]{num}&host=$host">View</a>,
2152 <a href="$MyURL?action=view&type=RestoreErr&num=$Restores[$i]{num}&host=$host">Errors</a>
2153 </tr></tr>
2154 </table>
2155 <p>
2156 ${h1("File/Directory list")}
2157 <p>
2158 <table border>
2159 <tr><td>Original file/dir</td><td>Restored to</td></tr>
2160 $fileListStr
2161 </table>
2162 EOF
2163     Trailer();
2164 }
2165     
2166 ###########################################################################
2167 # Miscellaneous subroutines
2168 ###########################################################################
2169
2170 sub timeStamp2
2171 {
2172     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
2173               = localtime($_[0] == 0 ? time : $_[0] );
2174     $year += 1900;
2175     $mon++;
2176     if ( $Conf{CgiDateFormatMMDD} ) {
2177         return sprintf("$mon/$mday %02d:%02d", $hour, $min);
2178     } else {
2179         return sprintf("$mday/$mon %02d:%02d", $hour, $min);
2180     }
2181 }
2182
2183 sub HostLink
2184 {
2185     my($host) = @_;
2186     my($s);
2187     if ( defined($Hosts->{$host}) || defined($Status{$host}) ) {
2188         $s = "<a href=\"$MyURL?host=$host\">$host</a>";
2189     } else {
2190         $s = $host;
2191     }
2192     return \$s;
2193 }
2194
2195 sub UserLink
2196 {
2197     my($user) = @_;
2198     my($s);
2199
2200     return \$user if ( $user eq ""
2201                     || $Conf{CgiUserUrlCreate} eq "" );
2202     if ( $Conf{CgiUserHomePageCheck} eq ""
2203             || -f sprintf($Conf{CgiUserHomePageCheck}, $user, $user, $user) ) {
2204         $s = "<a href=\""
2205              . sprintf($Conf{CgiUserUrlCreate}, $user, $user, $user)
2206              . "\">$user</a>";
2207     } else {
2208         $s = $user;
2209     }
2210     return \$s;
2211 }
2212
2213 sub EscapeHTML
2214 {
2215     my($s) = @_;
2216     $s =~ s/&/&amp;/g;
2217     $s =~ s/\"/&quot;/g;
2218     $s =~ s/>/&gt;/g;
2219     $s =~ s/</&lt;/g;
2220     $s =~ s{([^[:print:]])}{sprintf("&#x%02X;", ord($1));}eg;
2221     return \$s;
2222 }
2223
2224 ##sub URIEncode
2225 ##{
2226 ##    my($s) = @_;
2227 ##    $s =~ s{(['"&%[:^print:]])}{sprintf("%%%02X", ord($1));}eg;
2228 ##    return \$s;
2229 ##}
2230
2231 sub ErrorExit
2232 {
2233     my(@mesg) = @_;
2234     my($head) = shift(@mesg);
2235     my($mesg) = join("</p>\n<p>", @mesg);
2236     $Conf{CgiHeaderFontType} ||= "arial"; 
2237     $Conf{CgiHeaderFontSize} ||= "3";  
2238     $Conf{CgiNavBarBgColor}  ||= "#ddeeee";
2239     $Conf{CgiHeaderBgColor}  ||= "#99cc33";
2240
2241     $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
2242                             if ( defined($bpc) );
2243     Header("BackupPC: Error");
2244     print <<EOF;
2245 ${h1("Error: $head")}
2246 <p>$mesg</p>
2247 EOF
2248     Trailer();
2249     exit(1);
2250 }
2251
2252 sub ServerConnect
2253 {
2254     #
2255     # Verify that the server connection is ok
2256     #
2257     return if ( $bpc->ServerOK() );
2258     $bpc->ServerDisconnect();
2259     if ( my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}) ) {
2260         ErrorExit(
2261             "Unable to connect to BackupPC server",
2262             "This CGI script ($MyURL) is unable to connect to the BackupPC"
2263           . " server on $Conf{ServerHost} port $Conf{ServerPort}.  The error"
2264           . " was: $err.",
2265             "Perhaps the BackupPC server is not running or there is a "
2266           . " configuration error.  Please report this to your Sys Admin."
2267         );
2268     }
2269 }
2270
2271 sub GetStatusInfo
2272 {
2273     my($status) = @_;
2274     ServerConnect();
2275     my $reply = $bpc->ServerMesg("status $status");
2276     $reply = $1 if ( $reply =~ /(.*)/s );
2277     eval($reply);
2278     # ignore status related to admin and trashClean jobs
2279     if ( $status =~ /\bhosts\b/ ) {
2280         delete($Status{$bpc->adminJob});
2281         delete($Status{$bpc->trashJob});
2282     }
2283 }
2284
2285 sub ReadUserEmailInfo
2286 {
2287     if ( (stat("$TopDir/log/UserEmailInfo.pl"))[9] != $UserEmailInfoMTime ) {
2288         do "$TopDir/log/UserEmailInfo.pl";
2289         $UserEmailInfoMTime = (stat("$TopDir/log/UserEmailInfo.pl"))[9];
2290     }
2291 }
2292
2293 #
2294 # Check if the user is privileged.  A privileged user can access
2295 # any information (backup files, logs, status pages etc).
2296 #
2297 # A user is privileged if they belong to the group
2298 # $Conf{CgiAdminUserGroup}, or they are in $Conf{CgiAdminUsers}
2299 # or they are the user assigned to a host in the host file.
2300 #
2301 sub CheckPermission
2302 {
2303     my($host) = @_;
2304     my $Privileged = 0;
2305
2306     return 0 if ( $User eq "" || ($host ne "" && !defined($Hosts->{$host})) );
2307     if ( $Conf{CgiAdminUserGroup} ne "" ) {
2308         my($n,$p,$gid,$mem) = getgrnam($Conf{CgiAdminUserGroup});
2309         $Privileged ||= ($mem =~ /\b$User\b/);
2310     }
2311     if ( $Conf{CgiAdminUsers} ne "" ) {
2312         $Privileged ||= ($Conf{CgiAdminUsers} =~ /\b$User\b/);
2313         $Privileged ||= $Conf{CgiAdminUsers} eq "*";
2314     }
2315     $PrivAdmin = $Privileged;
2316     $Privileged ||= $User eq $Hosts->{$host}{user};
2317     return $Privileged;
2318 }
2319
2320 #
2321 # Given a host name tries to find the IP address.  For non-dhcp hosts
2322 # we just return the host name.  For dhcp hosts we check the address
2323 # the user is using ($ENV{REMOTE_ADDR}) and also the last-known IP
2324 # address for $host.  (Later we should replace this with a broadcast
2325 # nmblookup.)
2326 #
2327 sub ConfirmIPAddress
2328 {
2329     my($host) = @_;
2330     my $ipAddr = $host;
2331
2332     if ( $Hosts->{$host}{dhcp}
2333                && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
2334         $ipAddr = $1;
2335         my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
2336         if ( $netBiosHost ne $host ) {
2337             my($tryIP);
2338             GetStatusInfo("host($host)");
2339             if ( defined($StatusHost{dhcpHostIP})
2340                         && $StatusHost{dhcpHostIP} ne $ipAddr ) {
2341                 $tryIP = " and $StatusHost{dhcpHostIP}";
2342                 ($netBiosHost, $netBiosUser)
2343                         = $bpc->NetBiosInfoGet($StatusHost{dhcpHostIP});
2344             }
2345             if ( $netBiosHost ne $host ) {
2346                 ErrorExit("Can't find IP address for ${EscapeHTML($host)}",
2347                           <<EOF);
2348 $host is a DHCP host, and I don't know its IP address.  I checked the
2349 netbios name of $ENV{REMOTE_ADDR}$tryIP, and found that that machine
2350 is not $host.
2351 <p>
2352 Until I see $host at a particular DHCP address, you can only
2353 start this request from the client machine itself.
2354 EOF
2355             }
2356             $ipAddr = $StatusHost{dhcpHostIP};
2357         }
2358     }
2359     return $ipAddr;
2360 }
2361
2362 sub genPoolInfo
2363 {
2364     my($name, $info) = @_;
2365     my $poolSize   = sprintf("%.2f", $info->{"${name}Kb"} / (1000 * 1024));
2366     my $poolRmSize = sprintf("%.2f", $info->{"${name}KbRm"} / (1000 * 1024));
2367     my $poolTime   = timeStamp2($info->{"${name}Time"});
2368     $info->{"${name}FileCntRm"} = $info->{"${name}FileCntRm"} + 0;
2369     return <<EOF;
2370         <li>Pool is ${poolSize}GB comprising $info->{"${name}FileCnt"} files
2371             and $info->{"${name}DirCnt"} directories (as of $poolTime),
2372         <li>Pool hashing gives $info->{"${name}FileCntRep"} repeated
2373             files with longest chain $info->{"${name}FileRepMax"},
2374         <li>Nightly cleanup removed $info->{"${name}FileCntRm"} files of
2375             size ${poolRmSize}GB (around $poolTime),
2376 EOF
2377 }
2378
2379 ###########################################################################
2380 # HTML layout subroutines
2381 ###########################################################################
2382
2383 sub Header
2384 {
2385     my($title) = @_;
2386     my @adminLinks = (
2387         { link => "",                          name => "Status",
2388                                                priv => 1},
2389         { link => "?action=summary",           name => "PC Summary" },
2390         { link => "?action=view&type=LOG",     name => "LOG file" },
2391         { link => "?action=LOGlist",           name => "Old LOGs" },
2392         { link => "?action=emailSummary",      name => "Email summary" },
2393         { link => "?action=view&type=config",  name => "Config file" },
2394         { link => "?action=view&type=hosts",   name => "Hosts file" },
2395         { link => "?action=queue",             name => "Current queues" },
2396         { link => "?action=view&type=docs",    name => "Documentation",
2397                                                priv => 1},
2398         { link => "http://backuppc.sourceforge.net", name => "SourceForge",
2399                                                      priv => 1},
2400     );
2401     print $Cgi->header();
2402     print <<EOF;
2403 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
2404 <html><head>
2405 <title>$title</title>
2406 $Conf{CgiHeaders}
2407 </head><body>
2408 <table cellpadding="0" cellspacing="0" border="0">
2409 <tr valign="top"><td valign="top" bgcolor="$Conf{CgiNavBarBgColor}" width="10%">
2410 EOF
2411     NavSectionTitle("BackupPC");
2412     print "&nbsp;\n";
2413     if ( defined($In{host}) && defined($Hosts->{$In{host}}) ) {
2414         my $host = $In{host};
2415         NavSectionTitle("Host $In{host}");
2416         NavSectionStart();
2417         NavLink("?host=$host", "Home");
2418         NavLink("?action=view&type=LOG&host=$host", "LOG file");
2419         NavLink("?action=LOGlist&host=$host", "Old LOGs");
2420         if ( -f "$TopDir/pc/$host/SmbLOG.bad"
2421                     || -f "$TopDir/pc/$host/SmbLOG.bad.z"
2422                     || -f "$TopDir/pc/$host/XferLOG.bad"
2423                     || -f "$TopDir/pc/$host/XferLOG.bad.z" ) {
2424             NavLink("?action=view&type=XferLOGbad&host=$host",
2425                                 "Last bad XferLOG");
2426             NavLink("?action=view&type=XferErrbad&host=$host",
2427                                 "Last bad XferLOG (errors&nbsp;only)");
2428         }
2429         if ( -f "$TopDir/pc/$host/config.pl" ) {
2430             NavLink("?action=view&type=config&host=$host", "Config file");
2431         }
2432         NavSectionEnd();
2433     }
2434     NavSectionTitle("Hosts");
2435     if ( %$Hosts > 0 ) {
2436         NavSectionStart();
2437         foreach my $host ( sort(keys(%$Hosts)) ) {
2438             next if ( $Hosts->{$host}{user} ne $User );
2439             NavLink("?host=$host", $host);
2440         }
2441         NavSectionEnd();
2442     }
2443     print <<EOF;
2444 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2445     <tr><td><small>Host or User name:</small></td>
2446     <tr><td><form action="$MyURL" method="get"><small>
2447     <input type="text" name="host" size="10" maxlength="64">
2448     <input type="hidden" name="action" value="hostInfo"><input type="submit" value="Go" name="ignore">
2449     </small></form></td></tr>
2450 </table>
2451 EOF
2452     NavSectionTitle("Server");
2453     NavSectionStart();
2454     foreach my $l ( @adminLinks ) {
2455         if ( $PrivAdmin || $l->{priv} ) {
2456             NavLink($l->{link}, $l->{name});
2457         } else {
2458             NavLink(undef, $l->{name});
2459         }
2460     }
2461     NavSectionEnd();
2462     print <<EOF;
2463 </td><td valign="top" width="5">&nbsp;&nbsp;</td>
2464 <td valign="top" width="90%">
2465 EOF
2466 }
2467
2468 sub Trailer
2469 {
2470     print <<EOF;
2471 </td></table>
2472 </body></html>
2473 EOF
2474 }
2475
2476 sub h1
2477 {
2478     my($str) = @_;
2479     return \<<EOF;
2480 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2481 <tr>
2482 <td bgcolor="$Conf{CgiHeaderBgColor}">&nbsp;<font face="$Conf{CgiHeaderFontType}"
2483     size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
2484 </td></tr>
2485 </table>
2486 EOF
2487 }
2488
2489 sub h2
2490 {
2491     my($str) = @_;
2492     return \<<EOF;
2493 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2494 <tr>
2495 <td bgcolor="$Conf{CgiHeaderBgColor}">&nbsp;<font face="$Conf{CgiHeaderFontType}"
2496     size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
2497 </td></tr>
2498 </table>
2499 EOF
2500 }
2501
2502 sub NavSectionTitle
2503 {
2504     my($head) = @_;
2505     print <<EOF;
2506 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2507 <tr><td bgcolor="$Conf{CgiHeaderBgColor}"><font face="$Conf{CgiHeaderFontType}"
2508 size="$Conf{CgiHeaderFontSize}"><b>$head</b>
2509 </font></td></tr>
2510 </table>
2511 EOF
2512 }
2513
2514 sub NavSectionStart
2515 {
2516     print <<EOF;
2517 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2518 EOF
2519 }
2520
2521 sub NavSectionEnd
2522 {
2523     print "</table>\n";
2524 }
2525
2526 sub NavLink
2527 {
2528     my($link, $text) = @_;
2529     print "<tr><td width=\"2%\" valign=\"top\"><b>&middot;</b></td>";
2530     if ( defined($link) ) {
2531         $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
2532         print <<EOF;
2533 <td width="98%"><a href="$link"><small>$text</small></a></td></tr>
2534 EOF
2535     } else {
2536         print <<EOF;
2537 <td width="98%"><small>$text</small></td></tr>
2538 EOF
2539     }
2540 }