Added GFK's code for language specific email messages. The config.pl
[BackupPC.git] / bin / BackupPC_compressPool
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_compressPool: Compress existing pool
5 #
6 # DESCRIPTION
7 #
8 #   Usage: BackupPC_compressPool [-t] [-r] <host>
9 #
10 #   Flags:
11 #     -t     test mode: do everything except actually replace the pool files.
12 #            Useful for estimating total run time without making any real
13 #            changes.
14 #     -r     read check: re-read the compressed file and compare it against
15 #            the original uncompressed file.  Can only be used in test mode.
16 #     -c #   number of children to fork.  BackupPC_compressPool can take
17 #            a long time to run, so to speed things up it spawns four children,
18 #            each working on a different part of the pool.  You can change
19 #            the number of children with the -c option.
20 #
21 #   BackupPC_compressPool is used to convert an uncompressed pool to
22 #   a compressed pool.  If BackupPC compression is enabled after
23 #   uncompressed backups already exist, BackupPC_compressPool can
24 #   be used to compress all the old uncompressed backups.
25 #
26 #   It is important that BackupPC not run while BackupPC_compressPool
27 #   runs.  Also, BackupPC_compressPool must run to completion before
28 #   BackupPC is restarted.
29 #
30 # AUTHOR
31 #   Craig Barratt  <cbarratt@users.sourceforge.net>
32 #
33 # COPYRIGHT
34 #   Copyright (C) 2001  Craig Barratt
35 #
36 #   This program is free software; you can redistribute it and/or modify
37 #   it under the terms of the GNU General Public License as published by
38 #   the Free Software Foundation; either version 2 of the License, or
39 #   (at your option) any later version.
40 #
41 #   This program is distributed in the hope that it will be useful,
42 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
43 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
44 #   GNU General Public License for more details.
45 #
46 #   You should have received a copy of the GNU General Public License
47 #   along with this program; if not, write to the Free Software
48 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
49 #
50 #========================================================================
51 #
52 # Version 1.6.0_CVS, released 10 Dec 2002.
53 #
54 # See http://backuppc.sourceforge.net.
55 #
56 #========================================================================
57
58 use strict;
59
60 use File::Find;
61 use File::Path;
62 use Compress::Zlib;
63 use Getopt::Std;
64 use lib "/usr/local/BackupPC/lib";
65 use BackupPC::Lib;
66 use BackupPC::FileZIO;
67
68 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
69 $bpc->ChildInit();
70 my $TopDir   = $bpc->TopDir();
71 my $BinDir   = $bpc->BinDir();
72 my %Conf     = $bpc->Conf();
73 my $PoolDir  = "$TopDir/pool";
74 my $CPoolDir = "$TopDir/cpool";
75 my $Compress = $Conf{CompressLevel};
76 my %opts;
77 my $SigName = "";
78
79 #
80 # Catch various signals
81 #
82 foreach my $sig ( qw(INT BUS SEGV PIPE TERM ALRM HUP) ) {
83     $SIG{$sig} = \&catch_signal;
84 }
85
86 $| = 1;
87
88 my $CompMaxRead  = 131072;          # 128K
89 my $CompMaxWrite = 6291456;         # 6MB
90
91 if ( !getopts("trc:", \%opts) || @ARGV != 0 ) {
92     print("usage: $0 [-c nChild] [-r] [-t]\n");
93     exit(1);
94 }
95 my $TestMode  = $opts{t};
96 my $ReadCheck = $opts{r};
97 my $nChild    = $opts{c} || 4;
98 if ( $ReadCheck && !$TestMode ) {
99     print(STDERR "$0: -r (read check) option must have -t (test)\n");
100     exit(1);
101 }
102 if ( $nChild < 1 || $nChild >= 16 ) {
103     print(STDERR "$0: number of children (-c option) must be from 1 to 16\n");
104     exit(1);
105 }
106 if ( !BackupPC::FileZIO->compOk ) {
107     print STDERR <<EOF;
108 $0: Compress::Zlib is not installed.   You need to install it
109 before running this script.
110 EOF
111     exit(1);
112 }
113 if ( $Compress <= 0 ) {
114     print STDERR <<EOF;
115 $0: compression is not enabled. \%Conf{CompressLevel} needs
116 to be set to a value from 1 to 9.  Please edit the config.pl file and
117 re-start $0.
118 EOF
119     exit(1);
120 }
121
122 my $Errors     = 0;
123 my $SubDirDone = 0;
124 my $SubDirCnt  = 0;
125 my $SubDirCurr = 0;
126 my $FileCnt    = 0;
127 my $FileOrigSz = 0;
128 my $FileCompressSz = 0;
129
130 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
131 if ( $err eq "" ) {
132     print <<EOF;
133 BackupPC is running on $Conf{ServerHost}.  You need to stop BackupPC
134 before you can upgrade the code.  Depending upon your installation,
135 you could run "/etc/init.d/backuppc stop".
136 EOF
137     exit(1);
138 }
139
140 umask($Conf{UmaskMode});
141
142 sub cpoolFileName
143 {
144     my($new) = @_;
145     if ( $new !~ m{/(\w/\w/\w)/(\w{32})(_\d+)?$} ) {
146         print("Error: Can't parse filename from $new\n");
147         $Errors++;
148         return;
149     }
150     my $dir = "$CPoolDir/$1";
151     $new = "$dir/$2";
152     mkpath($dir, 0, 0777) if ( !-d $dir );
153     return $new if ( !-f $new );
154     for ( my $i = 0 ; ; $i++ ) {
155         return "${new}_$i" if ( !-f "${new}_$i" );
156     }
157 }
158
159 sub doCompress
160 {
161     my $file = ($File::Find::name =~ /(.*)/ && $1);
162     local(*FH, *OUT);
163     my(@s) = stat($file);
164     my($n, $dataIn, $dataOut, $flush, $copy);
165
166     if ( $SigName ) {
167         print("Child got signal $SigName; quitting\n");
168         reportStats();
169         exit(0);
170     }
171     return if ( !-f $file );
172     my $defl = deflateInit(
173                 -Bufsize => 65536,
174                 -Level   => $Compress,
175            );
176     if ( !open(FH, $TestMode ? "<$file" : "+<$file") ) {
177         print("Error: Can't open $file for read/write\n");
178         $Errors++;
179         return;
180     }
181     while ( sysread(FH, $dataIn, $CompMaxWrite) > 0 ) {
182         $flush = 0;
183         $FileOrigSz += length($dataIn);
184         my $fragOut = $defl->deflate($dataIn);
185         if ( length($fragOut) < $CompMaxRead ) {
186             #
187             # Compression is too high: to avoid huge memory requirements
188             # on read we need to flush().
189             #
190             $fragOut .= $defl->flush();
191             $flush = 1;
192             $defl = deflateInit(
193                         -Bufsize => 65536,
194                         -Level   => $Compress,
195                    );
196         }
197         $dataOut .= $fragOut;
198         if ( !$copy && length($dataOut) > $CompMaxWrite ) {
199             if ( !open(OUT, "+>$file.__z") ) {
200                 print("Error: Can't open $file.__z for write\n");
201                 $Errors++;
202                 close(FH);
203                 return;
204             }
205             $copy = 1;
206         }
207         if ( $copy && $dataOut ne "" ) {
208             if ( syswrite(OUT, $dataOut) != length($dataOut) ) {
209                 printf("Error: Can't write %d bytes to %s\n",
210                                     length($dataOut), "$file.__z");
211                 $Errors++;
212                 close(OUT);
213                 close(FH);
214                 unlink("$file.__z");
215                 return;
216             }
217             $FileCompressSz += length($dataOut);
218             $dataOut = undef;
219         }
220     }
221     if ( !$flush ) {
222         $dataOut .= $defl->flush();
223         if ( $copy && $dataOut ne "" ) {
224             if ( syswrite(OUT, $dataOut) != length($dataOut) ) {
225                 printf("Error: Can't write %d bytes to %s\n",
226                                     length($dataOut), "$file.__z");
227                 $Errors++;
228                 close(OUT);
229                 close(FH);
230                 unlink("$file.__z");
231                 return;
232             }
233             $FileCompressSz += length($dataOut);
234             $dataOut = undef;
235         }
236     }
237     my $newFile = cpoolFileName($file);
238     if ( $TestMode ) {
239         close(FH);
240         if ( !open(FH, ">$newFile") ) {
241             print("Error: Can't open $newFile for write\n");
242             $Errors++;
243             close(FH);
244             unlink("$file.__z");
245             return;
246         }
247     }
248     if ( $copy ) {
249         if ( !sysseek(OUT, 0, 0) ) {
250             print("Error: Can't seek $file.__z to 0\n");
251             $Errors++;
252         }
253         if ( !sysseek(FH, 0, 0) ) {
254             print("Error: Can't seek $newFile to 0\n");
255             $Errors++;
256         }
257         while ( sysread(OUT, $dataIn, $CompMaxWrite) > 0 ) {
258             if ( syswrite(FH, $dataIn) != length($dataIn) ) {
259                 printf("Error: Can't write %d bytes to %s\n",
260                                         length($dataIn), $file);
261                 $Errors++;
262             }
263         }
264         if ( !truncate(FH, sysseek(OUT, 0, 1)) ) {
265             printf("Error: Can't truncate %s to %d\n",
266                                         $file, sysseek(OUT, 0, 1));
267             $Errors++;
268         }
269         close(OUT);
270         close(FH);
271         unlink("$file.__z");
272     } else {
273         if ( !sysseek(FH, 0, 0) ) {
274             print("Error: Can't seek $file to 0\n");
275             $Errors++;
276         }
277         if ( syswrite(FH, $dataOut) != length($dataOut) ) {
278             printf("Error: Can't write %d bytes to %s\n",
279                                         length($dataOut), $file);
280             $Errors++;
281         }
282         $FileCompressSz += length($dataOut);
283         if ( !truncate(FH, length($dataOut)) ) {
284             printf("Error: Can't truncate %s to %d\n", $file, length($dataOut));
285             $Errors++;
286         }
287         close(FH);
288     }
289     if ( $TestMode ) {
290         if ( $ReadCheck ) {
291             checkRead($file, $newFile);
292         }
293         unlink($newFile);
294     } else {
295         rename($file, $newFile);
296         my $atime = $s[8] =~ /(.*)/ && $1;
297         my $mtime = $s[9] =~ /(.*)/ && $1;
298         utime($atime, $mtime, $newFile);
299     }
300     (my $dir = $file) =~ s{/[^/]*$}{};
301     $FileCnt++;
302     if ( $SubDirCurr ne "" && $SubDirCurr ne $dir ) {
303         $SubDirDone++;
304         $SubDirCurr = $dir;
305         reportStats();
306     } elsif ( $SubDirCurr eq "" ) {
307         $SubDirCurr = $dir;
308     }
309 }
310
311 sub reportStats
312 {
313     print("stats: $SubDirDone $SubDirCnt $FileCnt $FileOrigSz"
314                 . " $FileCompressSz $Errors\n");
315 }
316
317 sub checkRead
318 {
319     my($file, $cfile) = @_;
320     return if ( !-f $file || !-f $cfile );
321     my $f = BackupPC::FileZIO->open($cfile, 0, $Compress)
322                                 || die("can't open $cfile for read\n");
323     my($n, $nd, $r, $d, $d0);
324     local(*FH);
325
326     if ( !open(FH, $file) ) {
327         print("can't open $file for check\n");
328         $Errors++;
329         $f->close();
330         return;
331     }
332     #print("comparing $file to $cfile\n");
333     while ( 1 ) {
334         $n = 1 + int(rand($CompMaxRead) + rand(100));
335         $r = $f->read(\$d, $n);
336         sysread(FH, $d0, $n);
337         if ( $d ne $d0 ) {
338             print("Botch read data on $cfile\n");
339         }
340         last if ( length($d) == 0 );
341     }
342     if ( ($r = $f->read(\$d, 100)) != 0 || ($r = $f->read(\$d, 100)) != 0 ) {
343         printf("Botch at EOF on $cfile got $r (%d,%d)\n",
344                         sysseek(FH, 0, 1), $n);
345         $Errors++;
346     }
347     $f->close;
348     close(FH);
349 }
350
351 sub checkReadLine
352 {
353     my($file, $cfile) = @_;
354     return if ( !-f $file || !-f $cfile );
355     my $f = BackupPC::FileZIO->open($cfile, 0, $Compress)
356                                 || die("can't open $cfile for read\n");
357     my($n, $nd, $r, $d, $d0);
358     local(*FH);
359
360     if ( !open(FH, $file) ) {
361         print("can't open $file for check\n");
362         $Errors++;
363         $f->close();
364         return;
365     }
366     while ( 1 ) {
367         $d0 = <FH>;
368         $d  = $f->readLine();
369         if ( $d ne $d0 ) {
370             print("Botch read data on $cfile\n");
371         }
372         last if ( length($d) == 0 );
373     }
374     if ( ($r = $f->read(\$d, 100)) != 0 || ($r = $f->read(\$d, 100)) != 0 ) {
375         printf("Botch at EOF on $cfile got $r (%d,%d)\n",
376                         sysseek(FH, 0, 1), $n);
377         $Errors++;
378     }
379     $f->close;
380     close(FH);
381 }
382
383 sub catch_signal
384 {
385     $SigName = shift;
386 }
387
388 sub compressHostFiles
389 {
390     my($host) = @_;
391     my(@Files, @Backups, $fh, $data);
392     local(*FH);
393
394     if ( !defined($host) ) {
395         for ( my $i = 0 ; ; $i++ ) {
396             last if ( !-f "$TopDir/log/LOG.$i" );
397             push(@Files, "$TopDir/log/LOG.$i");
398         }
399     } else {
400         @Backups = $bpc->BackupInfoRead($host);
401         for ( my $i = 0 ; $i < @Backups ; $i++ ) {
402             next if ( $Backups[$i]{compress} );
403             push(@Files, "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}");
404             push(@Files, "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}");
405         }
406         push(@Files, "$TopDir/pc/$host/SmbLOG.bad");
407         push(@Files, "$TopDir/pc/$host/XferLOG.bad");
408         for ( my $i = 0 ; ; $i++ ) {
409             last if ( !-f "$TopDir/pc/$host/LOG.$i" );
410             push(@Files, "$TopDir/pc/$host/LOG.$i");
411         }
412     }
413     foreach my $file ( @Files ) {
414         if ( $SigName ) {
415             print("Child got signal $SigName; quitting\n");
416             reportStats();
417             exit(0);
418         }
419         next if ( !-f $file );
420         if ( !BackupPC::FileZIO->compressCopy($file, "$file.z", undef,
421                                         $Compress, !$TestMode) ) {
422             print("compressCopy($file, $file.z, $Compress, !$TestMode)"
423                 . " failed\n");
424             $Errors++;
425         } elsif ( $TestMode ) {
426             checkReadLine($file, "$file.z") if ( $ReadCheck );
427             unlink("$file.z");
428         }
429     }
430 }
431
432 sub updateHostBackupInfo
433 {
434     my($host) = @_;
435     if ( !$TestMode ) {
436         my @Backups = $bpc->BackupInfoRead($host);
437         for ( my $i = 0 ; $i < @Backups ; $i++ ) {
438             $Backups[$i]{compress} = $Compress;
439         }
440         $bpc->BackupInfoWrite($host, @Backups);
441     }
442 }
443
444 my @Dirs = split(//, "0123456789abcdef");
445 my @Hosts = sort(keys(%{$bpc->HostInfoRead()}));
446 my $FDread;
447 my @Jobs;
448
449 #
450 # First make sure there are no existing compressed backups
451 #
452 my(%compHosts, $compCnt);
453 for ( my $j = 0 ; $j < @Hosts ; $j++ ) {
454     my $host = $Hosts[$j];
455     my @Backups = $bpc->BackupInfoRead($host);
456     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
457         next if ( !$Backups[$i]{compress} );
458         $compHosts{$host}++;
459         $compCnt++;
460     }
461 }
462 if ( $compCnt ) {
463     my $compHostStr = join("\n  + ", sort(keys(%compHosts)));
464     print STDERR <<EOF;
465 BackupPC_compressPool: there are $compCnt compressed backups.
466 BackupPC_compressPool can only be run when there are no existing
467 compressed backups. The following hosts have compressed backups:
468
469   + $compHostStr
470
471 If you really want to run BackupPC_compressPool you will need to remove
472 all the existing compressed backups (and /home/pcbackup/data/cpool).
473 Think carefully before you do this. Otherwise, you can just let new
474 compressed backups run and the old uncompressed backups and pool will
475 steadily expire.
476 EOF
477     exit(0);
478 }
479
480 #
481 # Next spawn $nChild children that actually do all the work.
482 #
483 for ( my $i = 0 ; $i < $nChild ; $i++ ) {
484     local(*CHILD);
485     my $pid;
486     if ( !defined($pid = open(CHILD, "-|")) ) {
487         print("Can't fork\n");
488         next;
489     }
490     my $nDirs  = @Dirs  / ($nChild - $i);
491     my $nHosts = @Hosts / ($nChild - $i);
492     if ( !$pid ) {
493         #
494         # This is the child.
495         # First process each of the hosts (compress per-pc log files etc).
496         #
497         for ( my $j = 0 ; $j < $nHosts ; $j++ ) {
498             compressHostFiles($Hosts[$j]);
499         }
500         #
501         # Count the total number of directories so we can estimate the
502         # completion time.  We ignore empty directories by reading each
503         # directory and making sure it has at least 3 entries (ie, ".",
504         # ".." and a file).
505         #
506         for ( my $j = 0 ; $j < $nDirs ; $j++ ) {
507             my $thisDir = $Dirs[$j];
508             next if ( !-d "$PoolDir/$thisDir" );
509             foreach my $dir ( <$PoolDir/$thisDir/*/*> ) {
510                 next if ( !opendir(DIR, $dir) );
511                 my @files = readdir(DIR);
512                 closedir(DIR);
513                 $SubDirCnt++ if ( @files > 2 );
514             }
515         }
516         #
517         # Now process each of the directories
518         #
519         for ( my $j = 0 ; $j < $nDirs ; $j++ ) {
520             my $thisDir = shift(@Dirs);
521             next if ( !-d "$PoolDir/$thisDir" );
522             find({wanted => sub { doCompress($File::Find::name); },
523                                    no_chdir => 1}, "$PoolDir/$thisDir");
524         }
525         #
526         # Last, update the backup info file for each of the hosts
527         #
528         for ( my $j = 0 ; $j < $nHosts ; $j++ ) {
529             updateHostBackupInfo($Hosts[$j]);
530         }
531         $SubDirDone = $SubDirCnt;
532         reportStats();
533         exit(0);
534     }
535     #
536     # This is the parent.  Peel off $nDirs directories, $nHosts hosts,
537     # and continue
538     #
539     $Jobs[$i]{fh}  = *CHILD;
540     $Jobs[$i]{pid} = $pid;
541     vec($FDread, fileno($Jobs[$i]{fh}), 1) = 1;
542     splice(@Dirs,  0, $nDirs);
543     splice(@Hosts, 0, $nHosts);
544 }
545
546 #
547 # compress the main log files (in the parents)
548 #
549 compressHostFiles(undef);
550
551 #
552 # Now wait for all the children to report results and finish up
553 #
554 my $TimeStart = time;
555 my $DonePct   = 0;
556 my $GotSignal = "";
557 while ( $FDread !~ /^\0*$/ ) {
558     my $ein = $FDread;
559     select(my $rout = $FDread, undef, $ein, undef);
560     if ( $SigName ne $GotSignal ) {
561         print("Got signal $SigName; waiting for $nChild children to cleanup\n");
562         $GotSignal = $SigName;
563     }
564     for ( my $i = 0 ; $i < $nChild ; $i++ ) {
565         next if ( !vec($rout, fileno($Jobs[$i]{fh}), 1) );
566         my $data;
567         if ( sysread($Jobs[$i]{fh}, $data, 1024) <= 0 ) {
568             vec($FDread, fileno($Jobs[$i]{fh}), 1) = 0;
569             close($Jobs[$i]{fh});
570             next;
571         }
572         $Jobs[$i]{mesg} .= $data;
573         while ( $Jobs[$i]{mesg} =~ /(.*?)[\n\r]+(.*)/s ) {
574             my $mesg = $1;
575             $Jobs[$i]{mesg} = $2;
576             if ( $mesg =~ /^stats: (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)/ ) {
577                 $Jobs[$i]{SubDirDone}     = $1;
578                 $Jobs[$i]{SubDirCnt}      = $2;
579                 $Jobs[$i]{FileCnt}        = $3;
580                 $Jobs[$i]{FileOrigSz}     = $4;
581                 $Jobs[$i]{FileCompressSz} = $5;
582                 $Jobs[$i]{Errors}         = $6;
583                 $SubDirDone = $SubDirCnt = $FileCnt = $FileOrigSz = 0;
584                 $FileCompressSz = $Errors = 0;
585                 my $numReports = 0;
586                 for ( my $j = 0 ; $j < $nChild ; $j++ ) {
587                     next if ( !defined($Jobs[$j]{SubDirDone}) );
588                     $SubDirDone     += $Jobs[$j]{SubDirDone};
589                     $SubDirCnt      += $Jobs[$j]{SubDirCnt};
590                     $FileCnt        += $Jobs[$j]{FileCnt};
591                     $FileOrigSz     += $Jobs[$j]{FileOrigSz};
592                     $FileCompressSz += $Jobs[$j]{FileCompressSz};
593                     $Errors         += $Jobs[$j]{Errors};
594                     $numReports++;
595                 }
596                 $SubDirCnt  ||= 1;
597                 $FileOrigSz ||= 1;
598                 my $pctDone = 100 * $SubDirDone / $SubDirCnt;
599                 if ( $numReports == $nChild && $pctDone >= $DonePct + 1 ) {
600                     $DonePct = int($pctDone);
601                     my $estSecLeft = 1.2 * (time - $TimeStart)
602                                          * (100 / $pctDone - 1);
603                     my $timeStamp = $bpc->timeStamp;
604                     printf("%sDone %2.0f%% (%d of %d dirs, %d files,"
605                             . " %.2fGB raw, %.1f%% reduce, %d errors)\n",
606                                 $timeStamp,
607                                 $pctDone, $SubDirDone, $SubDirCnt, $FileCnt,
608                                 $FileOrigSz / (1024 * 1024 * 1000),
609                                 100 * (1 - $FileCompressSz / $FileOrigSz));
610                     printf("%s    Est complete in %.1f hours (around %s)\n",
611                                 $timeStamp, $estSecLeft / 3600,
612                                 $bpc->timeStamp(time + $estSecLeft, 1))
613                                             if ( $DonePct < 100 );
614                 }
615             } else {
616                 print($mesg, "\n");
617             }
618         }
619     }
620 }
621 if ( $Errors ) {
622     print("Finished with $Errors errors!!!!\n");
623     exit(1);
624 }