2 #============================================================= -*-perl-*-w
4 # BackupPC_Admin: Apache/CGI interface for BackupPC.
7 # BackupPC_Admin provides a flexible web interface for BackupPC.
8 # It is a CGI script that runs under Apache.
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
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.
20 # Craig Barratt <cbarratt@users.sourceforge.net>
23 # Copyright (C) 2001 Craig Barratt
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.
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.
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
39 #========================================================================
41 # Version 1.5.0, released 2 Aug 2002.
43 # See http://backuppc.sourceforge.net.
45 #========================================================================
49 use lib "__INSTALLDIR__/lib";
51 use BackupPC::FileZIO;
52 use BackupPC::Attrib qw(:all);
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);
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.
70 $MyURL = $ENV{SCRIPT_NAME};
71 $User = $ENV{REMOTE_USER};
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();
79 $ConfigMTime = $bpc->ConfigMTime();
80 } elsif ( $bpc->ConfigMTime() != $ConfigMTime ) {
83 $ConfigMTime = $bpc->ConfigMTime();
87 # Clean up %ENV for taint checking
89 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
90 $ENV{PATH} = $Conf{MyPath};
93 # Verify we are running as the correct user
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");
101 if ( !defined($Hosts) || $bpc->HostsMTime() != $HostsMTime ) {
102 $HostsMTime = $bpc->HostsMTime();
103 $Hosts = $bpc->HostInfoRead();
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,
124 # Set default actions, then call sub handler
126 $In{action} ||= "hostInfo" if ( defined($In{host}) );
127 $In{action} = "generalInfo" if ( !defined($ActionDispatch{$In{action}}) );
128 $ActionDispatch{$In{action}}();
131 ###########################################################################
132 # Action handling subroutines
133 ###########################################################################
137 my($fullTot, $fullSizeTot, $incrTot, $incrSizeTot, $str,
138 $strNone, $strGood, $hostCntGood, $hostCntNone);
140 $hostCntGood = $hostCntNone = 0;
141 GetStatusInfo("hosts");
142 my $Privileged = CheckPermission();
144 if ( !$Privileged ) {
145 ErrorExit("Only privileged users can view PC summaries." );
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" ) {
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};
160 $fullSizeTot += $Backups[$i]{size} / (1024 * 1024);
163 if ( $incrAge < 0 || $Backups[$i]{startTime} > $incrAge ) {
164 $incrAge = $Backups[$i]{startTime};
166 $incrSizeTot += $Backups[$i]{size} / (1024 * 1024);
169 if ( $fullAge < 0 ) {
173 $fullAge = sprintf("%.1f", (time - $fullAge) / (24 * 3600));
174 $fullRate = sprintf("%.2f",
175 $fullSize / ($fullDur <= 0 ? 1 : $fullDur));
177 if ( $incrAge < 0 ) {
180 $incrAge = sprintf("%.1f", (time - $incrAge) / (24 * 3600));
182 $fullTot += $fullCnt;
183 $incrTot += $incrCnt;
184 $fullSize = sprintf("%.2f", $fullSize / 1000);
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>
197 if ( @Backups == 0 ) {
205 $fullSizeTot = sprintf("%.2f", $fullSizeTot / 1000);
206 $incrSizeTot = sprintf("%.2f", $incrSizeTot / 1000);
207 my $now = timeStamp2(time);
209 Header("BackupPC: Server Summary");
213 ${h1("BackupPC Summary")}
215 This status was generated at $now.
218 ${h2("Hosts with good Backups")}
220 There are $hostCntGood hosts that have been backed up, for a total of:
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).
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>
242 ${h2("Hosts with no Backups")}
244 There are $hostCntNone hosts with no backups.
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>
263 sub Action_StartStopBackup
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);
273 if ( !$Privileged ) {
274 ErrorExit("Only privileged users can stop or start backups on"
275 . " ${EscapeHTML($host)}.");
281 if ( $Hosts->{$host}{dhcp} ) {
282 $reply = $bpc->ServerMesg("backup $In{hostIP} $host"
284 $str = "Backup requested on DHCP $host ($In{hostIP}) by"
285 . " $User from $ENV{REMOTE_ADDR}";
287 $reply = $bpc->ServerMesg("backup $host $host $User $doFull");
288 $str = "Backup requested on $host by $User";
291 $reply = $bpc->ServerMesg("stop $host $User $In{backoff}");
292 $str = "Backup stopped/dequeued on $host by $User";
294 Header("BackupPC: Backup Requested on $host");
298 Reply from server was: $reply
300 Go back to <a href="$MyURL?host=$host">$host home page</a>.
305 my $ipAddr = ConfirmIPAddress($host);
307 Header("BackupPC: Start Backup Confirm on $host");
309 ${h1("Are you sure?")}
311 You are about to start a $type backup on $host.
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="">
324 GetStatusInfo("host($host)");
325 if ( $StatusHost{backoffTime} > time ) {
326 $backoff = sprintf("%.1f",
327 ($StatusHost{backoffTime} - time) / 3600);
329 Header("BackupPC: Stop Backup Confirm on $host");
331 ${h1("Are you sure?")}
333 You are about to stop/dequeue backups on $host;
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.
341 Do you really want to do this?
342 <input type="submit" value="$In{action}" name="action">
343 <input type="submit" value="No" name="">
353 my($strBg, $strUser, $strCmd);
355 GetStatusInfo("queues");
356 my $Privileged = CheckPermission();
358 if ( !$Privileged ) {
359 ErrorExit("Only privileged users can view queues." );
363 my $req = pop(@BgQueue);
364 my($reqTime) = timeStamp2($req->{reqTime});
366 <tr><td> ${HostLink($req->{host})} </td>
367 <td align="center"> $reqTime </td>
368 <td align="center"> $req->{user} </td></tr>
371 while ( @UserQueue ) {
372 my $req = pop(@UserQueue);
373 my $reqTime = timeStamp2($req->{reqTime});
375 <tr><td> ${HostLink($req->{host})} </td>
376 <td align="center"> $reqTime </td>
377 <td align="center"> $req->{user} </td></tr>
380 while ( @CmdQueue ) {
381 my $req = pop(@CmdQueue);
382 my $reqTime = timeStamp2($req->{reqTime});
383 (my $cmd = $req->{cmd}) =~ s/$BinDir\///;
385 <tr><td> ${HostLink($req->{host})} </td>
386 <td align="center"> $reqTime </td>
387 <td align="center"> $req->{user} </td>
391 Header("BackupPC: Queue Summary");
393 ${h1("Backup Queue Summary")}
395 ${h2("User Queue Summary")}
397 The following user requests are currently queued:
406 ${h2("Background Queue Summary")}
408 The following background requests are currently queued:
417 ${h2("Command Queue Summary")}
419 The following command requests are currently queued:
424 <td> Command </td></tr>
433 my $Privileged = CheckPermission($In{host});
436 my $host = $In{host};
438 my $type = $In{type};
441 my $ext = $num ne "" ? ".$num" : "";
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> );
473 ErrorExit("Unable to open $file: configuration problem?");
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";
483 $file = "$TopDir/log/LOG$ext";
486 if ( !$Privileged ) {
487 ErrorExit("Only privileged users can view log or config files." );
489 if ( !-f $file && -f "$file.z" ) {
493 Header("BackupPC: Log File $file");
495 ${h1("Log File $file $comment")}
498 if ( defined($fh = BackupPC::FileZIO->open($file, 0, $compress)) ) {
499 my $mtimeStr = $bpc->timeStamp((stat($file))[9], 1);
501 Contents of log file <tt>$file</tt>, modified $mtimeStr $comment
504 if ( $type eq "XferErr" || $type eq "XferErrbad"
505 || $type eq "RestoreErr" ) {
508 $_ = $fh->readLine();
510 print("[ skipped $skipped lines ]\n") if ( $skipped );
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
522 || /^Getting files newer than/i
523 || /^Output is \/dev\/null/
524 || /^\([\d\.]* kb\/s\) \(average [\d\.]* kb\/s\)$/
525 || /^\s+directory \\/
532 print("[ skipped $skipped lines ]\n") if ( $skipped );
534 print ${EscapeHTML($_)};
536 } elsif ( $linkHosts ) {
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;
545 } elsif ( $type eq "config" ) {
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($_)};
557 $_ = $fh->readLine();
558 last if ( $_ eq "" );
559 print ${EscapeHTML($_)};
564 printf("<pre>\nCan't open log file $file\n");
574 my $Privileged = CheckPermission($In{host});
576 if ( !$Privileged ) {
577 ErrorExit("Only privileged users can view log files.");
579 my $host = $In{host};
580 my($url0, $hdr, $root, $str);
582 $root = "$TopDir/pc/$host/LOG";
583 $url0 = "&host=$host";
584 $hdr = "for host $host";
586 $root = "$TopDir/log/LOG";
590 for ( my $i = -1 ; ; $i++ ) {
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];
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>
607 Header("BackupPC: Log File History");
610 ${h1("Log File History $hdr")}
613 <tr><td align="center"> File </td>
614 <td align="center"> Size </td>
615 <td align="center"> Modification time </td></tr>
622 sub Action_EmailSummary
624 my $Privileged = CheckPermission();
626 if ( !$Privileged ) {
627 ErrorExit("Only privileged users can view email summaries." );
629 GetStatusInfo("hosts");
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>
642 foreach my $t ( sort({$b <=> $a} keys(%EmailStr)) ) {
643 $str .= $EmailStr{$t};
645 Header("BackupPC: Email Summary");
647 ${h1("Recent Email Summary (Reverse time order)")}
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>
662 my $Privileged = CheckPermission($In{host});
663 my($i, $dirStr, $fileStr, $mangle);
664 my($numF, $compressF, $mangleF, $fullDirF);
665 my $checkBoxCnt = 0; # checkbox counter
667 if ( !$Privileged ) {
668 ErrorExit("Only privileged users can browse backup files"
669 . " for host ${EscapeHTML($In{host})}." );
671 my $host = $In{host};
675 ErrorExit("Empty host name.");
678 # Find the requested backup and the previous filled backup
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};
687 last if ( $Backups[$i]{num} == $num );
689 if ( $i >= @Backups ) {
690 ErrorExit("Backup number $num for host ${EscapeHTML($host)} does"
693 if ( !$Backups[$i]{noFill} ) {
694 # no need to back-fill a filled backup
695 $numF = $mangleF = $compressF = undef;
697 my $backupTime = timeStamp2($Backups[$i]{startTime});
698 my $backupAge = sprintf("%.1f", (time - $Backups[$i]{startTime})
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\")}");
707 # Read this directory and find the first directory
709 foreach my $f ( readdir(DIR) ) {
710 next if ( $f eq "." || $f eq ".." );
711 if ( -d "$TopDir/pc/$host/$num/$f" ) {
717 if ( $dir eq "" || $dir eq "." || $dir eq ".." ) {
718 ErrorExit("Directory ${EscapeHTML(\"$TopDir/pc/$host/$num\")}"
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);
730 $fullDirF = "$TopDir/pc/$host/$numF/$relDir";
735 # Read attributes for the directory and optionally for the filled backup
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);
746 # Loop up the directory tree until we hit the top.
750 my($fLast, $fum, $fLastum, @DirStr);
752 if ( $fullDir =~ m{(^|/)\.\.(/|$)} || !opendir(DIR, $fullDir) ) {
753 ErrorExit("Can't browse bad directory name"
754 . " ${EscapeHTML($fullDir)}");
757 # Read this directory and optionally the corresponding filled directory
759 my @Dir = readdir(DIR);
761 if ( defined($numF) && opendir(DIR, $fullDirF) ) {
762 if ( $mangle == $mangleF ) {
763 @Dir = (@Dir, readdir(DIR));
765 foreach my $f ( readdir(DIR) ) {
766 next if ( $f eq "." || $f eq ".." );
767 push(@Dir, $bpc->fileNameMangle($f));
772 my $fileCnt = 0; # file counter
773 $fLast = $dirStr = "";
775 # Loop over each of the files in this directory
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") );
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
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" ) {
795 # Display directory if it exists in current backup.
796 # First find out if there are subdirs
798 my @s = (defined($numF) && -d "$fullDirF/$f")
799 ? stat("$fullDirF/$f")
800 : stat("$fullDir/$f");
801 my($bold, $unbold, $BGcolor);
803 $img |= 1 << 5 if ( $s[3] > 2 );
808 $img |= 1 << 3 if ( $s[3] > 2 );
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}\"";
818 $dirName =~ s/ / /g;
819 push(@DirStr, {needTick => 1,
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"> $bold$dirName$unbold</a></td></tr>
827 my($lastTick, $doneLastTick);
828 foreach my $d ( @DirStrPrev ) {
829 $lastTick = $d if ( $d->{needTick} );
831 $doneLastTick = 1 if ( !defined($lastTick) );
832 foreach my $d ( @DirStrPrev ) {
834 if ( $d->{needTick} ) {
837 if ( $d == $lastTick ) {
840 } elsif ( !$doneLastTick ) {
841 $img |= 1 << 3 | 1 << 4;
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}
852 if ( $relDir eq $dir ) {
854 # This is the selected directory, so display all the files
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);
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>
869 $attrStr .= "<td colspan=\"4\" align=\"center\"> </td>\n";
873 <tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path"> <a href="$MyURL?action=browse&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
879 <tr bgcolor="#ffffcc"><td><input type="checkbox" name="fcb$checkBoxCnt" value="$path"> <a href="$MyURL?action=RestoreFile&host=$host&num=$num&dir=$path">${EscapeHTML($fum)}</a></td>
887 @DirStrPrev = @DirStr;
888 last if ( $relDir eq "" );
890 # Prune the last directory off $relDir
892 $relDir =~ s/(.*)\/(.*)/$1/;
894 $fullDir = "$TopDir/pc/$host/$num/$relDir";
895 $fullDirF = "$TopDir/pc/$host/$numF/$relDir" if ( defined($numF) );
897 my $dirDisplay = $mangle ? $bpc->fileNameUnmangle($dir) : $dir;
898 $dirDisplay =~ s{//}{/}g;
900 if ( defined($numF) ) {
901 $filledBackup = <<EOF;
902 <li> This display is merged with backup #$numF, the most recent prior
906 Header("BackupPC: Browse backup $num for $host");
908 foreach my $d ( @DirStrPrev ) {
909 $dirStr .= "<tr><td$d->{tdArgs}>$d->{link}\n";
912 ### hide checkall button if there are no files
913 my ($topCheckAll, $checkAll, $fileHeader);
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>
924 <tr bgcolor="#ffffcc"><td>
925 <input type="checkbox" name="allFiles" onClick="return checkAll('allFiles');"> Select all
926 </td><td colspan="4" align="center">
927 <input type="submit" name="Submit" value="Restore selected files">
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;
937 <tr><td bgcolor="#ffffff">The directory ${EscapeHTML($dirDisplay)} is empty
943 ${h1("Backup browse for $host")}
945 <script language="javascript" type="text/javascript">
948 function checkAll(location)
950 for (var i=0;i<document.form1.elements.length;i++)
952 var e = document.form1.elements[i];
953 if ((e.checked || !e.checked) && e.name != 'all') {
954 if (eval("document.form1."+location+".checked")) {
963 function toggleThis(checkbox)
965 var cb = eval("document.form1."+checkbox);
966 cb.checked = !cb.checked;
973 <li> You are browsing backup #$num, which started around $backupTime
974 ($backupAge days ago),
976 <li> Click on a directory below to navigate into that directory,
977 <li> Click on a file below to restore that file.
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">
988 <tr><td valign="top">
989 <!--Navigate here:-->
990 <br><table align="center" border="0" cellpadding="0" cellspacing="0" bgcolor="#ffffff">
994 </td><td valign="top">
995 <!--Restore files here:-->
997 <table cellpadding="0" cellspacing="0" bgcolor="#333333"><tr><td>
998 <table border="0" width="100%" align="left" cellpadding="2" cellspacing="1">
1007 This is now in the checkAll row
1008 <input type="submit" name="Submit" value="Restore selected files">
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})}." );
1024 my $host = $In{host};
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 );
1031 my $mangle = $Backups[$i]{mangle};
1034 if ( !defined($Hosts->{$host}) ) {
1035 ErrorExit("Bad host name ${EscapeHTML($host)}");
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{^/+(.*?)(/.*)} ) {
1043 $name = $mangle ? $bpc->fileNameUnmangle($2) : $2;
1044 if ( @fileList == 0 ) {
1047 while ( substr($name, 0, length($pathHdr)) ne $pathHdr ) {
1048 $pathHdr = substr($pathHdr, 0, rindex($pathHdr, "/"));
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}">
1057 $fileListStr .= <<EOF;
1058 <li> ${EscapeHTML($name)}
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.");
1068 if ( $badFileCnt ) {
1069 ErrorExit("Nice try, but you can't put '..' in any of the file names");
1071 if ( @fileList == 1 ) {
1072 $pathHdr =~ s/(.*)\/.*/$1/;
1074 $pathHdr = "/" if ( $pathHdr eq "" );
1075 if ( $In{type} != 0 && @fileList == $In{fcbMax} ) {
1077 # All the files in the list were selected, so just restore the
1078 # entire parent directory
1080 @fileList = ( $pathHdr );
1082 if ( $In{type} == 0 ) {
1084 # Tell the user what options they have
1086 Header("BackupPC: Restore Options for $host");
1088 ${h1("Restore Options for $host")}
1090 You have selected the following files/directories from
1091 share $share, backup number #$num:
1096 You have three choices for restoring these files/directories.
1097 Please select one of the following options.
1099 ${h2("Option 1: Direct Restore")}
1101 You can start a restore that will restore these files directly onto
1104 <b>Warning:</b> any existing files that match the ones you have
1105 selected will be overwritten!
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">
1112 <input type="hidden" value="$In{action}" name="action">
1115 <td>Restore the files to host</td>
1116 <td><input type="text" size="40" value="${EscapeHTML($host)}"
1117 name="hostDest"></td>
1119 <td>Restore the files to share</td>
1120 <td><input type="text" size="40" value="${EscapeHTML($share)}"
1121 name="shareDest"></td>
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>
1127 <td><input type="submit" value="Start Restore" name=""></td>
1133 # Verify that Archive::Zip is available before showing the
1134 # zip restore option
1136 if ( eval { require Archive::Zip } ) {
1139 ${h2("Option 2: Download Zip archive")}
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.
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
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">
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).
1160 Compression (0=off, 1=fast,...,9=best)
1161 <input type="text" size="6" value="5" name="compressLevel">
1163 <input type="submit" value="Download Zip File" name="">
1169 ${h2("Option 2: Download Zip archive")}
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>.
1178 ${h2("Option 3: Download Tar archive")}
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.
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
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">
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).
1199 <input type="submit" value="Download Tar File" name="">
1203 } elsif ( $In{type} == 1 ) {
1205 # Provide the selected files via a tar archive.
1207 $SIG{CHLD} = 'IGNORE';
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");
1215 # This is the parent.
1217 my @fileListTrim = @fileList;
1218 if ( @fileListTrim > 10 ) {
1219 @fileListTrim = (@fileListTrim[0..9], '...');
1221 $bpc->ServerMesg("log User $User downloaded tar archive for $host,"
1222 . " backup $num; files were: "
1223 . join(", ", @fileListTrim));
1227 # This is the child. Print the headers and run BackupPC_tarCreate.
1230 if ( $In{relative} ) {
1231 @pathOpts = ("-r", $pathHdr, "-p", "");
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",
1244 } elsif ( $In{type} == 2 ) {
1246 # Provide the selected files via a zip archive.
1248 $SIG{CHLD} = 'IGNORE';
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");
1256 # This is the parent.
1258 my @fileListTrim = @fileList;
1259 if ( @fileListTrim > 10 ) {
1260 @fileListTrim = (@fileListTrim[0..9], '...');
1262 $bpc->ServerMesg("log User $User downloaded zip archive for $host,"
1263 . " backup $num; files were: "
1264 . join(", ", @fileListTrim));
1268 # This is the child. Print the headers and run BackupPC_tarCreate.
1271 if ( $In{relative} ) {
1272 @pathOpts = ("-r", $pathHdr, "-p", "");
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",
1282 "-c", $In{compressLevel},
1287 } elsif ( $In{type} == 3 ) {
1289 # Do restore directly onto host
1291 if ( !defined($Hosts->{$In{hostDest}}) ) {
1292 ErrorExit("Host ${EscapeHTML($In{hostDest})} doesn't exist");
1294 if ( !CheckPermission($In{hostDest}) ) {
1295 ErrorExit("You don't have permission to restore onto host"
1296 . " ${EscapeHTML($In{hostDest})}");
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>
1308 Header("BackupPC: Restore Confirm on $host");
1310 ${h1("Are you sure?")}
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
1317 <tr><td>Original file/dir</td><td>Will be restored to</td></tr>
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">
1329 Do you really want to do this?
1330 <input type="submit" value="$In{action}" name="action">
1331 <input type="submit" value="No" name="">
1335 } elsif ( $In{type} == 4 ) {
1336 if ( !defined($Hosts->{$In{hostDest}}) ) {
1337 ErrorExit("Host ${EscapeHTML($In{hostDest})} doesn't exist");
1339 if ( !CheckPermission($In{hostDest}) ) {
1340 ErrorExit("You don't have permission to restore onto host"
1341 . " ${EscapeHTML($In{hostDest})}");
1343 my $hostDest = $1 if ( $In{hostDest} =~ /(.+)/ );
1344 my $ipAddr = ConfirmIPAddress($hostDest);
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.
1352 for ( my $i = 0 ; ; $i++ ) {
1353 $reqFileName = "restoreReq.$$.$i";
1354 last if ( !-f "$TopDir/pc/$hostDest/$reqFileName" );
1357 # source of restore is hostSrc, #num, path shareSrc/pathHdrSrc
1361 pathHdrSrc => $pathHdr,
1363 # destination of restore is hostDest:shareDest/pathHdrDest
1364 hostDest => $hostDest,
1365 shareDest => $In{shareDest},
1366 pathHdrDest => $In{pathHdr},
1368 # list of files to restore
1369 fileList => \@fileList,
1375 my($dump) = Data::Dumper->new(
1379 if ( open(REQ, ">$TopDir/pc/$hostDest/$reqFileName") ) {
1380 print(REQ $dump->Dump);
1383 ErrorExit("Can't open/create "
1384 . ${EscapeHTML("$TopDir/pc/$hostDest/$reqFileName")});
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");
1394 Reply from server was: $reply
1396 Go back to <a href="$MyURL?host=$hostDest">$hostDest home page</a>.
1402 sub Action_RestoreFile
1404 restoreFile($In{host}, $In{num}, $In{dir});
1409 my($host, $num, $dir, $skipHardLink, $origName) = @_;
1410 my($Privileged) = CheckPermission($host);
1411 my($i, $numF, $mangleF, $compressF, $mangle, $compress, $dirUM);
1413 # Some common content (media) types from www.iana.org (via MIME::Types).
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',
1499 if ( !$Privileged ) {
1500 ErrorExit("Only privileged users can restore backup files"
1501 . " for host ${EscapeHTML($host)}." );
1504 my @Backups = $bpc->BackupInfoRead($host);
1505 if ( $host eq "" ) {
1506 ErrorExit("Empty host name");
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};
1515 last if ( $Backups[$i]{num} == $num );
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;
1523 my $fullPath = "$TopDir/pc/$host/$num/$dir";
1524 $fullPath =~ s{/+}{/}g;
1525 if ( !-f $fullPath && defined($numF) ) {
1528 if ( $mangle && !$mangleF ) {
1529 $fullPathF = "$TopDir/pc/$host/$numF/"
1530 . $bpc->fileNameUnmangle($dir);
1532 $fullPathF = "$TopDir/pc/$host/$numF/$dir";
1534 if ( -f $fullPathF ) {
1535 $fullPath = $fullPathF;
1536 $compress = $compressF;
1539 if ( $fullPath =~ m{(^|/)\.\.(/|$)} || !-f $fullPath ) {
1540 ErrorExit("Can't restore bad file ${EscapeHTML($fullPath)}");
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);
1550 my $f = BackupPC::FileZIO->open($fullPath, 0, $compress);
1552 if ( !$skipHardLink && $a->{type} == BPC_FTYPE_HARDLINK ) {
1554 # hardlinks should look like the file they point to
1557 while ( $f->read(\$data, 65536) > 0 ) {
1561 $linkName =~ s/^\.\///;
1562 my $share = $1 if ( $dir =~ /^\/?(.*?)\// );
1563 restoreFile($host, $num,
1564 "$share/" . ($mangle ? $bpc->fileNameMangle($linkName)
1565 : $linkName), 1, $dir);
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 ) {
1589 my $host = $1 if ( $In{host} =~ /(.*)/ );
1590 my($statusStr, $startIncrStr);
1594 return Action_GeneralInfo() if ( $host eq "" );
1596 if ( !-d "$TopDir/pc/$host" && -d "$TopDir/pc/" . lc($host) );
1597 if ( $host =~ /\.\./ || !-d "$TopDir/pc/$host" ) {
1599 # try to lookup by user name
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) ) {
1610 ErrorExit("Unknown host or user ${EscapeHTML($host)}")
1611 if ( !defined($Hosts->{$host}) );
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)}." );
1623 ReadUserEmailInfo();
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}
1640 $ExistComp = sprintf("%.1f%%", 100 *
1641 (1 - $Backups[$i]{sizeExistComp} / $Backups[$i]{sizeExist}));
1643 if ( $Backups[$i]{sizeNew} && $Backups[$i]{sizeNewComp} ) {
1644 $MBNewComp = sprintf("%.1f", $Backups[$i]{sizeNewComp}
1646 $NewComp = sprintf("%.1f%%", 100 *
1647 (1 - $Backups[$i]{sizeNewComp} / $Backups[$i]{sizeNew}));
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 "" );
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>
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>
1675 $Backups[$i]{compress} ||= "off";
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>
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>
1700 my @Restores = $bpc->RestoreInfoRead($host);
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>
1722 $restoreStr = <<EOF if ( $restoreStr ne "" );
1723 ${h2("Restore Summary")}
1725 Click on the restore number for more details.
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>
1741 if ( @Backups == 0 ) {
1742 $warnStr = "<h2> This PC has never been backed up!! </h2>\n";
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)}.
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".
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.
1767 if ( $StatusHost{BgQueueOn} ) {
1768 $statusStr .= <<EOF;
1769 <li>Host $host is queued on the background queue (will be backed up soon).
1772 if ( $StatusHost{UserQueueOn} ) {
1773 $statusStr .= <<EOF;
1774 <li>Host $host is queued on the user queue (will be backed up soon).
1777 if ( $StatusHost{CmdQueueOn} ) {
1778 $statusStr .= <<EOF;
1779 <li>A command for $host is on the command queue (will run soon).
1782 my $startTime = timeStamp2($StatusHost{endTime} == 0 ?
1783 $StatusHost{startTime} : $StatusHost{endTime});
1785 if ( $StatusHost{reason} ne "" ) {
1786 $reason = " ($StatusHost{reason})";
1788 $statusStr .= <<EOF;
1789 <li>Last status is state "$StatusHost{state}"$reason
1792 if ( $StatusHost{error} ne "" ) {
1793 $statusStr .= <<EOF;
1794 <li>Last error is "${EscapeHTML($StatusHost{error})}"
1797 my $priorStr = "Pings";
1798 if ( $StatusHost{deadCnt} > 0 ) {
1799 $statusStr .= <<EOF;
1800 <li>Pings to $host have failed $StatusHost{deadCnt} consecutive times.
1802 $priorStr = "Prior to that, pings";
1804 if ( $StatusHost{aliveCnt} > 0 ) {
1805 $statusStr .= <<EOF;
1806 <li>$priorStr to $host have succeeded $StatusHost{aliveCnt}
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.
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
1835 # only allow incremental if there are already some backups
1836 $startIncrStr = <<EOF;
1837 <input type="submit" value="Start Incr Backup" name="action">
1841 Header("BackupPC: Host $host Backup Summary");
1843 ${h1("Host $host Backup Summary")}
1850 ${h2("User Actions")}
1852 <form action="$MyURL" method="get">
1853 <input type="hidden" name="host" value="$host">
1855 <input type="submit" value="Start Full Backup" name="action">
1856 <input type="submit" value="Stop/Dequeue Backup" name="action">
1859 ${h2("Backup Summary")}
1861 Click on the backup number to browse and restore backup files.
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>
1877 ${h2("Xfer Error Summary")}
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>
1892 ${h2("File Size/Count Reuse Summary")}
1894 Existing files are those already in the pool; new files are those added
1896 Empty files and SMB errors aren't counted in the reuse and new counts.
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>
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>
1918 ${h2("Compression Summary")}
1920 Compression performance for files already in the pool and newly
1923 <tr><td colspan="3"></td>
1924 <td align="center" colspan="3"> Existing Files </td>
1925 <td align="center" colspan="3"> New Files </td>
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>
1944 sub Action_GeneralInfo
1946 GetStatusInfo("info jobs hosts queueLen");
1947 my $Privileged = CheckPermission();
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;
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>
1963 <td align="center"> $Jobs{$host}{pid} </td>
1964 <td align="center"> $Jobs{$host}{xferPid} </td>
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";
1970 $jobStr .= "</tr>\n";
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});
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"
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>
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>
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 ) {
2014 <li>Uncompressed pool:
2018 <li>Compressed pool:
2023 } elsif ( $Info{cpoolFileCnt} > 0 ) {
2024 $poolInfo = $cpoolInfo;
2026 Header("BackupPC: Server Status");
2029 ${h1("BackupPC Server Status")}
2032 ${h2("General Server Information")}
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.
2041 <li>$numBgQueue pending backup requests from last scheduled wakeup,
2042 <li>$numUserQueue pending user backup requests,
2043 <li>$numCmdQueue pending command requests,
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}%.
2051 ${h2("Currently Running Jobs")}
2057 <td> Start Time </td>
2059 <td align="center"> PID </td>
2066 ${h2("Failures that need attention")}
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>
2082 sub Action_RestoreInfo
2084 my $Privileged = CheckPermission($In{host});
2085 my $host = $1 if ( $In{host} =~ /(.*)/ );
2089 if ( !$Privileged ) {
2090 ErrorExit("Only privileged users can view restore information." );
2093 # Find the requested restore
2095 my @Restores = $bpc->RestoreInfoRead($host);
2096 for ( $i = 0 ; $i < @Restores ; $i++ ) {
2097 last if ( $Restores[$i]{num} == $num );
2099 if ( $i >= @Restores ) {
2100 ErrorExit("Restore number $num for host ${EscapeHTML($host)} does"
2105 do "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}"
2106 if ( -f "$TopDir/pc/$host/RestoreInfo.$Restores[$i]{num}" );
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));
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>
2128 Header("BackupPC: Restore #$num details for $host");
2130 ${h1("Restore #$num Details for $host")}
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>
2156 ${h1("File/Directory list")}
2159 <tr><td>Original file/dir</td><td>Restored to</td></tr>
2166 ###########################################################################
2167 # Miscellaneous subroutines
2168 ###########################################################################
2172 my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
2173 = localtime($_[0] == 0 ? time : $_[0] );
2176 if ( $Conf{CgiDateFormatMMDD} ) {
2177 return sprintf("$mon/$mday %02d:%02d", $hour, $min);
2179 return sprintf("$mday/$mon %02d:%02d", $hour, $min);
2187 if ( defined($Hosts->{$host}) || defined($Status{$host}) ) {
2188 $s = "<a href=\"$MyURL?host=$host\">$host</a>";
2200 return \$user if ( $user eq ""
2201 || $Conf{CgiUserUrlCreate} eq "" );
2202 if ( $Conf{CgiUserHomePageCheck} eq ""
2203 || -f sprintf($Conf{CgiUserHomePageCheck}, $user, $user, $user) ) {
2205 . sprintf($Conf{CgiUserUrlCreate}, $user, $user, $user)
2217 $s =~ s/\"/"/g;
2220 $s =~ s{([^[:print:]])}{sprintf("&#x%02X;", ord($1));}eg;
2227 ## $s =~ s{(['"&%[:^print:]])}{sprintf("%%%02X", ord($1));}eg;
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";
2241 $bpc->ServerMesg("log User $User (host=$In{host}) got CGI error: $head")
2242 if ( defined($bpc) );
2243 Header("BackupPC: Error");
2245 ${h1("Error: $head")}
2255 # Verify that the server connection is ok
2257 return if ( $bpc->ServerOK() );
2258 $bpc->ServerDisconnect();
2259 if ( my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort}) ) {
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"
2265 "Perhaps the BackupPC server is not running or there is a "
2266 . " configuration error. Please report this to your Sys Admin."
2275 my $reply = $bpc->ServerMesg("status $status");
2276 $reply = $1 if ( $reply =~ /(.*)/s );
2278 # ignore status related to admin and trashClean jobs
2279 if ( $status =~ /\bhosts\b/ ) {
2280 delete($Status{$bpc->adminJob});
2281 delete($Status{$bpc->trashJob});
2285 sub ReadUserEmailInfo
2287 if ( (stat("$TopDir/log/UserEmailInfo.pl"))[9] != $UserEmailInfoMTime ) {
2288 do "$TopDir/log/UserEmailInfo.pl";
2289 $UserEmailInfoMTime = (stat("$TopDir/log/UserEmailInfo.pl"))[9];
2294 # Check if the user is privileged. A privileged user can access
2295 # any information (backup files, logs, status pages etc).
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.
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/);
2311 if ( $Conf{CgiAdminUsers} ne "" ) {
2312 $Privileged ||= ($Conf{CgiAdminUsers} =~ /\b$User\b/);
2313 $Privileged ||= $Conf{CgiAdminUsers} eq "*";
2315 $PrivAdmin = $Privileged;
2316 $Privileged ||= $User eq $Hosts->{$host}{user};
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
2327 sub ConfirmIPAddress
2332 if ( $Hosts->{$host}{dhcp}
2333 && $ENV{REMOTE_ADDR} =~ /^(\d+[\.\d]*)$/ ) {
2335 my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($ipAddr);
2336 if ( $netBiosHost ne $host ) {
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});
2345 if ( $netBiosHost ne $host ) {
2346 ErrorExit("Can't find IP address for ${EscapeHTML($host)}",
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
2352 Until I see $host at a particular DHCP address, you can only
2353 start this request from the client machine itself.
2356 $ipAddr = $StatusHost{dhcpHostIP};
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;
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),
2379 ###########################################################################
2380 # HTML layout subroutines
2381 ###########################################################################
2387 { link => "", name => "Status",
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",
2398 { link => "http://backuppc.sourceforge.net", name => "SourceForge",
2401 print $Cgi->header();
2403 <!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN">
2405 <title>$title</title>
2408 <table cellpadding="0" cellspacing="0" border="0">
2409 <tr valign="top"><td valign="top" bgcolor="$Conf{CgiNavBarBgColor}" width="10%">
2411 NavSectionTitle("BackupPC");
2413 if ( defined($In{host}) && defined($Hosts->{$In{host}}) ) {
2414 my $host = $In{host};
2415 NavSectionTitle("Host $In{host}");
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 only)");
2429 if ( -f "$TopDir/pc/$host/config.pl" ) {
2430 NavLink("?action=view&type=config&host=$host", "Config file");
2434 NavSectionTitle("Hosts");
2435 if ( %$Hosts > 0 ) {
2437 foreach my $host ( sort(keys(%$Hosts)) ) {
2438 next if ( $Hosts->{$host}{user} ne $User );
2439 NavLink("?host=$host", $host);
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>
2452 NavSectionTitle("Server");
2454 foreach my $l ( @adminLinks ) {
2455 if ( $PrivAdmin || $l->{priv} ) {
2456 NavLink($l->{link}, $l->{name});
2458 NavLink(undef, $l->{name});
2463 </td><td valign="top" width="5"> </td>
2464 <td valign="top" width="90%">
2480 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2482 <td bgcolor="$Conf{CgiHeaderBgColor}"> <font face="$Conf{CgiHeaderFontType}"
2483 size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
2493 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2495 <td bgcolor="$Conf{CgiHeaderBgColor}"> <font face="$Conf{CgiHeaderFontType}"
2496 size="$Conf{CgiHeaderFontSize}"><b>$str</b></font>
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>
2517 <table cellpadding="2" cellspacing="0" border="0" width="100%">
2528 my($link, $text) = @_;
2529 print "<tr><td width=\"2%\" valign=\"top\"><b>·</b></td>";
2530 if ( defined($link) ) {
2531 $link = "$MyURL$link" if ( $link eq "" || $link =~ /^\?/ );
2533 <td width="98%"><a href="$link"><small>$text</small></a></td></tr>
2537 <td width="98%"><small>$text</small></td></tr>