Changes for 2.1.3:
[BackupPC.git] / bin / BackupPC_tarExtract
1 #!/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_tarExtract: extract data from a dump
5 #
6 # DESCRIPTION
7 #
8 # AUTHOR
9 #   Craig Barratt  <cbarratt@users.sourceforge.net>
10 #
11 # COPYRIGHT
12 #   Copyright (C) 2001-2003  Craig Barratt
13 #
14 #   This program is free software; you can redistribute it and/or modify
15 #   it under the terms of the GNU General Public License as published by
16 #   the Free Software Foundation; either version 2 of the License, or
17 #   (at your option) any later version.
18 #
19 #   This program is distributed in the hope that it will be useful,
20 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
21 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 #   GNU General Public License for more details.
23 #
24 #   You should have received a copy of the GNU General Public License
25 #   along with this program; if not, write to the Free Software
26 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
27 #
28 #========================================================================
29 #
30 # Version 2.1.3, released 21 Jan 2007.
31 #
32 # See http://backuppc.sourceforge.net.
33 #
34 #========================================================================
35
36 use strict;
37 no  utf8;
38 use lib "/usr/local/BackupPC2.1.0/lib";
39 use BackupPC::Lib;
40 use BackupPC::Attrib qw(:all);
41 use BackupPC::FileZIO;
42 use BackupPC::PoolWrite;
43 use File::Path;
44
45 use constant S_IFMT       => 0170000;   # type of file
46
47 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
48 my $TopDir = $bpc->TopDir();
49 my $BinDir = $bpc->BinDir();
50 my %Conf   = $bpc->Conf();
51
52 if ( @ARGV != 3 ) {
53     print("usage: $0 <client> <shareName> <compressLevel>\n");
54     exit(1);
55 }
56 if ( $ARGV[0] !~ /^([\w\.\s-]+)$/ ) {
57     print("$0: bad client name '$ARGV[0]'\n");
58     exit(1);
59 }
60 my $client = $1;
61 if ( $ARGV[1] !~ /^([\w\s.\/$(){}[\]-]+)$/ ) {
62     print("$0: bad share name '$ARGV[1]'\n");
63     exit(1);
64 }
65 my $ShareNameUM = $1;
66 my $ShareName = $bpc->fileNameEltMangle($ShareNameUM);
67 if ( $ARGV[2] !~ /^(\d+)$/ ) {
68     print("$0: bad compress level '$ARGV[2]'\n");
69     exit(1);
70 }
71 my $Compress = $1;
72 my $Abort = 0;
73 my $AbortReason;
74
75 #
76 # Re-read config file, so we can include the PC-specific config
77 #
78 if ( defined(my $error = $bpc->ConfigRead($client)) ) {
79     print("BackupPC_tarExtract: Can't read PC's config file: $error\n");
80     exit(1);
81 }
82 %Conf = $bpc->Conf();
83
84 #
85 # Catch various signals
86 #
87 $SIG{INT}  = \&catch_signal;
88 $SIG{ALRM} = \&catch_signal;
89 $SIG{TERM} = \&catch_signal;
90 $SIG{PIPE} = \&catch_signal;
91 $SIG{STOP} = \&catch_signal;
92 $SIG{TSTP} = \&catch_signal;
93 $SIG{TTIN} = \&catch_signal;
94
95 #
96 # This constant and the line of code below that uses it is borrowed
97 # from Archive::Tar.  Thanks to Calle Dybedahl and Stephen Zander.
98 # See www.cpan.org.
99 #
100 # Archive::Tar is Copyright 1997 Calle Dybedahl. All rights reserved.
101 #                 Copyright 1998 Stephen Zander. All rights reserved.
102 #
103 my $tar_unpack_header
104     = 'Z100 A8 A8 A8 a12 A12 A8 A1 Z100 A6 A2 Z32 Z32 A8 A8 A155 x12';
105 my $tar_header_length = 512;
106
107 my $BufSize  = 1048576;     # 1MB or 2^20
108 my $MaxFiles = 20;
109 my $Errors   = 0;
110 my $OutDir   = "$TopDir/pc/$client/new";
111 my %Attrib   = ();
112
113 my $ExistFileCnt      = 0;
114 my $ExistFileSize     = 0;
115 my $ExistFileCompSize = 0;
116 my $TotalFileCnt      = 0;
117 my $TotalFileSize     = 0;
118 my $TarReadHdrCnt     = 0;
119
120 sub TarRead
121 {
122     my($fh, $totBytes) = @_;
123     my($numBytes, $newBytes, $data);
124
125     $data = "\0" x $totBytes;
126     while ( $numBytes < $totBytes ) {
127         return if ( $Abort );
128         $newBytes = sysread($fh,
129                         substr($data, $numBytes, $totBytes - $numBytes),
130                         $totBytes - $numBytes);
131         if ( $newBytes <= 0 ) {
132             return if ( $TarReadHdrCnt == 1 );   # empty tar file ok
133             print("Unexpected end of tar archive (tot = $totBytes,"
134                    . " num = $numBytes, posn = " . sysseek($fh, 0, 1) . ")\n");
135             $Abort = 1;
136             $AbortReason = "Unexpected end of tar archive";
137             $Errors++;
138             return;
139         }
140         $numBytes += $newBytes;
141     }
142     return $data;
143 }
144
145 sub TarReadHeader
146 {
147     my($fh) = @_;
148
149     $TarReadHdrCnt++;
150     return $1 if ( TarRead($fh, $tar_header_length) =~ /(.*)/s );
151     return;
152 }
153
154 sub TarFlush
155 {
156     my($fh, $size) = @_;
157
158     if ( $size % $tar_header_length ) {
159         TarRead($fh, $tar_header_length - ($size % $tar_header_length));
160     }
161 }
162
163 sub TarReadFileInfo
164 {
165     my($fh) = @_;
166     my($head, $longName, $longLink);
167     my($name, $mode, $uid, $gid, $size, $mtime, $chksum, $type,
168        $linkname, $magic, $version, $uname, $gname, $devmajor,
169        $devminor, $prefix);
170
171     while ( 1 ) {
172         $head = TarReadHeader($fh);
173         return if ( $Abort || $head eq ""
174                            || $head eq "\0" x $tar_header_length );
175         ($name,         # string
176             $mode,      # octal number
177             $uid,       # octal number
178             $gid,       # octal number
179             $size,      # octal number
180             $mtime,     # octal number
181             $chksum,    # octal number
182             $type,      # character
183             $linkname,  # string
184             $magic,     # string
185             $version,   # two bytes
186             $uname,     # string
187             $gname,     # string
188             $devmajor,  # octal number
189             $devminor,  # octal number
190             $prefix) = unpack($tar_unpack_header, $head);
191
192         $mode     = oct $mode;
193         $uid      = oct $uid;
194         $gid      = oct $gid;
195         if ( ord($size) == 128 ) {
196             #
197             # GNU tar extension: for >=8GB files the size is stored
198             # in big endian binary.
199             #
200             $size = 65536 * 65536 * unpack("N", substr($size, 4, 4))
201                                   + unpack("N", substr($size, 8, 4));
202         } else {
203             #
204             # We used to have a patch here for smbclient 2.2.x.  For file
205             # sizes between 2 and 4GB it sent the wrong size.  But since
206             # samba 3.0.0 has been released we no longer support this
207             # patch since valid files could have sizes that start with
208             # 6 or 7 in octal (eg: 6-8GB files).
209             #
210             # $size =~ s/^6/2/;       # fix bug in smbclient for >=2GB files
211             # $size =~ s/^7/3/;       # fix bug in smbclient for >=2GB files
212             #
213             # To avoid integer overflow in case we are in the 4GB - 8GB
214             # range, we do the conversion in two parts.
215             #
216             if ( $size =~ /([0-9]{9,})/ ) {
217                 my $len = length($1);
218                 $size = oct(substr($1, 0, $len - 8)) * (1 << 24)
219                       + oct(substr($1, $len - 8));
220             } else {
221                 $size = oct($size);
222             }
223         }
224         $mtime    = oct $mtime;
225         $chksum   = oct $chksum;
226         $devmajor = oct $devmajor;
227         $devminor = oct $devminor;
228         $name     = "$prefix/$name" if $prefix;
229         $prefix   = "";
230         substr ($head, 148, 8) = "        ";
231         if (unpack ("%16C*", $head) != $chksum) {
232            print("$name: checksum error at "
233                         . sysseek($fh, 0, 1) , "\n");
234            $Errors++;
235         }
236         if ( $type eq "L" ) {
237             $longName = TarRead($fh, $size) || return;
238             # remove trailing NULL
239             $longName = substr($longName, 0, $size - 1);
240             TarFlush($fh, $size);
241             next;
242         } elsif ( $type eq "K" ) {
243             $longLink = TarRead($fh, $size) || return;
244             # remove trailing NULL
245             $longLink = substr($longLink, 0, $size - 1);
246             TarFlush($fh, $size);
247             next;
248         }
249         printf("Got file '%s', mode 0%o, size %g, type %d\n",
250                 $name, $mode, $size, $type) if ( $Conf{XferLogLevel} >= 3 );
251         $name     = $longName if ( defined($longName) );
252         $linkname = $longLink if ( defined($longLink) );
253         $name     =~ s{^\./+}{};
254         $name     =~ s{/+\.?$}{};
255         $name     =~ s{//+}{/}g;
256         return {
257             name       => $name,
258             mangleName => $bpc->fileNameMangle($name),
259             mode       => $mode,
260             uid        => $uid,
261             gid        => $gid,
262             size       => $size,
263             mtime      => $mtime,
264             type       => $type,
265             linkname   => $linkname,
266             devmajor   => $devmajor,
267             devminor   => $devminor,
268         };
269     }
270 }
271
272 sub TarReadFile
273 {
274     my($fh) = @_;
275     my $f = TarReadFileInfo($fh) || return;
276     my($dir, $file);
277
278     if ( $f->{name} eq "" ) {
279         # top-level dir
280         $dir = "";
281         $file = $ShareNameUM;
282     } else {
283         ($file = $f->{name}) =~ s{.*?([^/]*)$}{$1};         # unmangled file
284         if ( ($dir = $f->{mangleName}) =~ m{(.*)/.*} ) {
285             $dir = "$ShareName/$1";
286         } else {
287             $dir = $ShareName;
288         }
289     }
290     if ( !defined($Attrib{$dir}) ) {
291         foreach my $d ( keys(%Attrib) ) {
292             next if ( $dir =~ m{^\Q$d/} );
293             attributeWrite($d);
294         }
295         $Attrib{$dir} = BackupPC::Attrib->new({ compress => $Compress });
296         if ( -f $Attrib{$dir}->fileName("$OutDir/$dir")
297                     && !$Attrib{$dir}->read("$OutDir/$dir") ) {
298             printf("Unable to read attribute file %s\n",
299                                 $Attrib{$dir}->fileName("$OutDir/$dir"));
300             $Errors++;
301         }
302     }
303     if ( $f->{type} == BPC_FTYPE_DIR ) {
304         #
305         # Directory
306         #
307         logFileAction("create", $f) if ( $Conf{XferLogLevel} >= 1 );
308         mkpath("$OutDir/$ShareName/$f->{mangleName}", 0, 0777)
309                             if ( !-d "$OutDir/$ShareName/$f->{mangleName}" );
310     } elsif ( $f->{type} == BPC_FTYPE_FILE ) {
311         #
312         # Regular file
313         #
314         my($nRead);
315         #print("Reading $f->{name}, $f->{size} bytes, type $f->{type}\n");
316         pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
317         my $poolWrite = BackupPC::PoolWrite->new($bpc,
318                                          "$OutDir/$ShareName/$f->{mangleName}",
319                                          $f->{size}, $Compress);
320         while ( $nRead < $f->{size} ) {
321             my $thisRead = $f->{size} - $nRead < $BufSize
322                                 ? $f->{size} - $nRead : $BufSize;
323             my $data = TarRead($fh, $thisRead);
324             if ( $data eq "" ) {
325                 if ( !$Abort ) {
326                     print("Unexpected end of tar archive during read\n");
327                     $AbortReason = "Unexpected end of tar archive";
328                     $Errors++;
329                 }
330                 $poolWrite->abort;
331                 $Abort = 1;
332                 unlink("$OutDir/$ShareName/$f->{mangleName}");
333                 print("Removing partial file $f->{name}\n");
334                 return;
335             }
336             $poolWrite->write(\$data);
337             $nRead += $thisRead;
338         }
339         my $exist = processClose($poolWrite, "$ShareName/$f->{mangleName}",
340                                  $f->{size});
341         logFileAction($exist ? "pool" : "create", $f)
342                                  if ( $Conf{XferLogLevel} >= 1 );
343         TarFlush($fh, $f->{size});
344     } elsif ( $f->{type} == BPC_FTYPE_HARDLINK ) {
345         #
346         # Hardlink to another file.  GNU tar is clever about files
347         # that are hardlinks to each other.  The first link will be
348         # sent as a regular file.  The additional links will be sent
349         # as this type.  We store the hardlink just like a symlink:
350         # the link name (path of the linked-to file) is stored in
351         # a plain file.
352         #
353         $f->{size} = length($f->{linkname});
354         pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
355         my $poolWrite = BackupPC::PoolWrite->new($bpc,
356                                          "$OutDir/$ShareName/$f->{mangleName}",
357                                          $f->{size}, $Compress);
358         $poolWrite->write(\$f->{linkname});
359         my $exist = processClose($poolWrite, "$ShareName/$f->{mangleName}",
360                                  $f->{size});
361         logFileAction($exist ? "pool" : "create", $f)
362                                  if ( $Conf{XferLogLevel} >= 1 );
363     } elsif ( $f->{type} == BPC_FTYPE_SYMLINK ) {
364         #
365         # Symbolic link: write the value of the link to a plain file,
366         # that we pool as usual (ie: we don't create a symlink).
367         # The attributes remember the original file type.
368         # We also change the size to reflect the size of the link
369         # contents.
370         #
371         $f->{size} = length($f->{linkname});
372         pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
373         my $poolWrite = BackupPC::PoolWrite->new($bpc,
374                                          "$OutDir/$ShareName/$f->{mangleName}",
375                                          $f->{size}, $Compress);
376         $poolWrite->write(\$f->{linkname});
377         my $exist = processClose($poolWrite, "$ShareName/$f->{mangleName}",
378                                  $f->{size});
379         logFileAction($exist ? "pool" : "create", $f)
380                                  if ( $Conf{XferLogLevel} >= 1 );
381     } elsif ( $f->{type} == BPC_FTYPE_CHARDEV
382            || $f->{type} == BPC_FTYPE_BLOCKDEV
383            || $f->{type} == BPC_FTYPE_FIFO ) {
384         #
385         # Special files: for char and block special we write the
386         # major and minor numbers to a plain file, that we pool
387         # as usual.  For a pipe file we create an empty file.
388         # The attributes remember the original file type.
389         #
390         my $data;
391         if ( $f->{type} == BPC_FTYPE_FIFO ) {
392             $data = "";
393         } else {
394             $data = "$f->{devmajor},$f->{devminor}";
395         }
396         pathCreate($dir, "$OutDir/$ShareName/$f->{mangleName}", $f);
397         my $poolWrite = BackupPC::PoolWrite->new($bpc,
398                                          "$OutDir/$ShareName/$f->{mangleName}",
399                                          length($data), $Compress);
400         $poolWrite->write(\$data);
401         $f->{size} = length($data);
402         my $exist = processClose($poolWrite, "$ShareName/$f->{mangleName}",
403                                  length($data));
404         logFileAction($exist ? "pool" : "create", $f)
405                                  if ( $Conf{XferLogLevel} >= 1 );
406     } else {
407         print("Got unknown type $f->{type} for $f->{name}\n");
408         $Errors++;
409     }
410     $Attrib{$dir}->set($file, {
411                             type  => $f->{type},
412                             mode  => $f->{mode},
413                             uid   => $f->{uid},
414                             gid   => $f->{gid},
415                             size  => $f->{size},
416                             mtime => $f->{mtime},
417                        });
418     return 1;
419 }
420
421 sub attributeWrite
422 {
423     my($d) = @_;
424     my($poolWrite);
425
426     return if ( !defined($Attrib{$d}) );
427     if ( $Attrib{$d}->fileCount ) {
428         my $data = $Attrib{$d}->writeData;
429         my $fileName = $Attrib{$d}->fileName("$OutDir/$d");
430         my $poolWrite = BackupPC::PoolWrite->new($bpc, $fileName,
431                                          length($data), $Compress);
432         $poolWrite->write(\$data);
433         processClose($poolWrite, $Attrib{$d}->fileName($d), length($data), 1);
434     }
435     delete($Attrib{$d});
436 }
437
438 sub processClose
439 {
440     my($poolWrite, $fileName, $origSize, $noStats) = @_;
441     my($exists, $digest, $outSize, $errs) = $poolWrite->close;
442
443     if ( @$errs ) {
444         print(join("", @$errs));
445         $Errors += @$errs;
446     }
447     if ( !$noStats ) {
448         $TotalFileCnt++;
449         $TotalFileSize += $origSize;
450     }
451     if ( $exists ) {
452         if ( !$noStats ) {
453             $ExistFileCnt++;
454             $ExistFileSize     += $origSize;
455             $ExistFileCompSize += $outSize;
456         }
457     } elsif ( $outSize > 0 ) {
458         print(NEW_FILES "$digest $origSize $fileName\n");
459     }
460     return $exists && $origSize > 0;
461 }
462
463 #
464 # Generate a log file message for a completed file
465 #
466 sub logFileAction
467 {
468     my($action, $f) = @_;
469     my $owner = "$f->{uid}/$f->{gid}";
470     my $name = $f->{name};
471     $name = "." if ( $name eq "" );
472     my $type  = (("", "p", "c", "", "d", "", "b", "", "", "", "l", "", "s"))
473                     [($f->{mode} & S_IFMT) >> 12];
474     $type = "h" if ( $f->{type} == BPC_FTYPE_HARDLINK );
475
476     printf("  %-6s %1s%4o %9s %11.0f %s\n",
477                                 $action,
478                                 $type,
479                                 $f->{mode} & 07777,
480                                 $owner,
481                                 $f->{size},
482                                 $name);
483 }
484
485 #
486 # Create the parent directory of $file if necessary
487 #
488 sub pathCreate
489 {
490     my($dir, $fullPath, $f) = @_;
491
492     #
493     # Get parent directory of each of $dir and $fullPath
494     #
495     # print("pathCreate: dir = $dir, fullPath = $fullPath\n");
496     $dir      =~ s{/([^/]*)$}{};
497     my $file  = $bpc->fileNameUnmangle($1);
498     $fullPath =~ s{/[^/]*$}{};
499     return if ( -d $fullPath || $file eq "" );
500     unlink($fullPath) if ( -e $fullPath );
501     mkpath($fullPath, 0, 0777);
502     $Attrib{$dir} = BackupPC::Attrib->new({ compress => $Compress })
503                                 if ( !defined($Attrib{$dir}) );
504     # print("pathCreate: adding file = $file to dir = $dir\n");
505     $Attrib{$dir}->set($file, {
506                             type  => BPC_FTYPE_DIR,
507                             mode  => 0755,
508                             uid   => $f->{uid},
509                             gid   => $f->{gid},
510                             size  => 0,
511                             mtime => 0,
512                        });
513 }
514
515 sub catch_signal
516 {
517     my $sigName = shift;
518
519     #
520     # The first time we receive a signal we try to gracefully
521     # abort the backup.  This allows us to keep a partial dump
522     # with the in-progress file deleted and attribute caches
523     # flushed to disk etc.
524     #
525     print("BackupPC_tarExtract: got signal $sigName\n");
526     if ( !$Abort ) {
527         $Abort++;
528         $AbortReason = "received signal $sigName";
529         return;
530     }
531
532     #
533     # This is a second signal: time to clean up.
534     #
535     print("BackupPC_tarExtract: quitting on second signal $sigName\n");
536     close(NEW_FILES);
537     exit(1)
538 }
539
540 mkpath("$OutDir/$ShareName", 0, 0777);
541 open(NEW_FILES, ">>", "$TopDir/pc/$client/NewFileList")
542                  || die("can't open $TopDir/pc/$client/NewFileList");
543 binmode(NEW_FILES);
544 binmode(STDIN);
545 1 while ( !$Abort && TarReadFile(*STDIN) );
546 1 while ( !$Abort && sysread(STDIN, my $discard, 1024) );
547
548 #
549 # Flush out remaining attributes.
550 #
551 foreach my $d ( keys(%Attrib) ) {
552     attributeWrite($d);
553 }
554 close(NEW_FILES);
555
556 if ( $Abort ) {
557     print("BackupPC_tarExtact aborting ($AbortReason)\n");
558 }
559
560 #
561 # Report results to BackupPC_dump
562 #
563 print("Done: $Errors errors, $ExistFileCnt filesExist,"
564     . " $ExistFileSize sizeExist, $ExistFileCompSize sizeExistComp,"
565     . " $TotalFileCnt filesTotal, $TotalFileSize sizeTotal\n");