- updates to ChangeLog
[BackupPC.git] / bin / BackupPC_tarCreate
1 #!/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_tarCreate: create a tar archive of an existing dump
5 # for restore on a client.
6 #
7 # DESCRIPTION
8 #  
9 #   Usage: BackupPC_tarCreate [options] files/directories...
10 #
11 #   Flags:
12 #     Required options:
13 #
14 #       -h host         Host from which the tar archive is created.
15 #       -n dumpNum      Dump number from which the tar archive is created.
16 #                       A negative number means relative to the end (eg -1
17 #                       means the most recent dump, -2 2nd most recent etc).
18 #       -s shareName    Share name from which the tar archive is created.
19 #
20 #     Other options:
21 #       -t              print summary totals
22 #       -r pathRemove   path prefix that will be replaced with pathAdd
23 #       -p pathAdd      new path prefix
24 #       -b BLOCKS       BLOCKS x 512 bytes per record (default 20; same as tar)
25 #       -w writeBufSz   write buffer size (default 1MB)
26 #       -e charset      charset for encoding file names (default: value of
27 #                       $Conf{ClientCharset} when backup was done)
28 #
29 #     The -h, -n and -s options specify which dump is used to generate
30 #     the tar archive.  The -r and -p options can be used to relocate
31 #     the paths in the tar archive so extracted files can be placed
32 #     in a location different from their original location.
33 #
34 # AUTHOR
35 #   Craig Barratt  <cbarratt@users.sourceforge.net>
36 #
37 # COPYRIGHT
38 #   Copyright (C) 2001-2003  Craig Barratt
39 #
40 #   This program is free software; you can redistribute it and/or modify
41 #   it under the terms of the GNU General Public License as published by
42 #   the Free Software Foundation; either version 2 of the License, or
43 #   (at your option) any later version.
44 #
45 #   This program is distributed in the hope that it will be useful,
46 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
47 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
48 #   GNU General Public License for more details.
49 #
50 #   You should have received a copy of the GNU General Public License
51 #   along with this program; if not, write to the Free Software
52 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
53 #
54 #========================================================================
55 #
56 # Version 3.0.0alpha, released 23 Jan 2006.
57 #
58 # See http://backuppc.sourceforge.net.
59 #
60 #========================================================================
61
62 use strict;
63 no  utf8;
64 use lib "/usr/local/BackupPC/lib";
65 use File::Path;
66 use Getopt::Std;
67 use Encode qw/from_to/;
68 use BackupPC::Lib;
69 use BackupPC::Attrib qw(:all);
70 use BackupPC::FileZIO;
71 use BackupPC::View;
72
73 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
74
75 my %opts;
76
77 if ( !getopts("te:h:n:p:r:s:b:w:", \%opts) || @ARGV < 1 ) {
78     print STDERR <<EOF;
79 usage: $0 [options] files/directories...
80   Required options:
81      -h host         host from which the tar archive is created
82      -n dumpNum      dump number from which the tar archive is created
83                      A negative number means relative to the end (eg -1
84                      means the most recent dump, -2 2nd most recent etc).
85      -s shareName    share name from which the tar archive is created
86
87   Other options:
88      -t              print summary totals
89      -r pathRemove   path prefix that will be replaced with pathAdd
90      -p pathAdd      new path prefix
91      -b BLOCKS       BLOCKS x 512 bytes per record (default 20; same as tar)
92      -w writeBufSz   write buffer size (default 1048576 = 1MB)
93      -e charset      charset for encoding file names (default: value of
94                      \$Conf{ClientCharset} when backup was done)
95 EOF
96     exit(1);
97 }
98
99 if ( $opts{h} !~ /^([\w\.\s-]+)$/ ) {
100     print(STDERR "$0: bad host name '$opts{h}'\n");
101     exit(1);
102 }
103 my $Host = $opts{h};
104
105 if ( $opts{n} !~ /^(-?\d+)$/ ) {
106     print(STDERR "$0: bad dump number '$opts{n}'\n");
107     exit(1);
108 }
109 my $Num = $opts{n};
110
111 my @Backups = $bpc->BackupInfoRead($Host);
112 my $FileCnt = 0;
113 my $ByteCnt = 0;
114 my $DirCnt = 0;
115 my $SpecialCnt = 0;
116 my $ErrorCnt = 0;
117
118 my $i;
119 $Num = $Backups[@Backups + $Num]{num} if ( -@Backups <= $Num && $Num < 0 );
120 for ( $i = 0 ; $i < @Backups ; $i++ ) {
121     last if ( $Backups[$i]{num} == $Num );
122 }
123 if ( $i >= @Backups ) {
124     print(STDERR "$0: bad backup number $Num for host $Host\n");
125     exit(1);
126 }
127
128 my $Charset = $Backups[$i]{charset};
129 $Charset = $opts{e} if ( $opts{e} ne "" );
130
131 my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ );
132 my $PathAdd    = $1 if ( $opts{p} =~ /(.+)/ );
133 if ( $opts{s} !~ /^([\w\s.\/$(){}[\]-]+)$/ && $opts{s} ne "*" ) {
134     print(STDERR "$0: bad share name '$opts{s}'\n");
135     exit(1);
136 }
137
138 our $ShareName = $opts{s};
139 our $view = BackupPC::View->new($bpc, $Host, \@Backups);
140
141 #
142 # This constant and the line of code below that uses it are borrowed
143 # from Archive::Tar.  Thanks to Calle Dybedahl and Stephen Zander.
144 # See www.cpan.org.
145 #
146 # Archive::Tar is Copyright 1997 Calle Dybedahl. All rights reserved.
147 #                 Copyright 1998 Stephen Zander. All rights reserved.
148 #
149 my $tar_pack_header
150     = 'a100 a8 a8 a8 a12 a12 A8 a1 a100 a6 a2 a32 a32 a8 a8 a155 x12';
151 my $tar_header_length = 512;
152
153 my $BufSize    = $opts{w} || 1048576;     # 1MB or 2^20
154 my $WriteBuf   = "";
155 my $WriteBufSz = ($opts{b} || 20) * $tar_header_length;
156
157 my(%UidCache, %GidCache);
158 my(%HardLinkExtraFiles, @HardLinks);
159
160 #
161 # Write out all the requested files/directories
162 #
163 binmode(STDOUT);
164 my $fh = *STDOUT;
165 if ( $ShareName eq "*" ) {
166     my $PathRemoveOrig = $PathRemove;
167     my $PathAddOrig    = $PathAdd;
168     foreach $ShareName ( $view->shareList($Num) ) {
169         #print(STDERR "Doing share ($ShareName)\n");
170         $PathRemove = "/" if ( !defined($PathRemoveOrig) );
171         ($PathAdd = "/$ShareName/$PathAddOrig") =~ s{//+}{/}g;
172         foreach my $dir ( @ARGV ) {
173             archiveWrite($fh, $dir);
174         }
175         archiveWriteHardLinks($fh);
176     }
177 } else {
178     foreach my $dir ( @ARGV ) {
179         archiveWrite($fh, $dir);
180     }
181     archiveWriteHardLinks($fh);
182 }
183
184 #
185 # Finish with two null 512 byte headers, and then round out a full
186 # block.
187
188 my $data = "\0" x ($tar_header_length * 2);
189 TarWrite($fh, \$data);
190 TarWrite($fh, undef);
191
192 #
193 # print out totals if requested
194 #
195 if ( $opts{t} ) {
196     print STDERR "Done: $FileCnt files, $ByteCnt bytes, $DirCnt dirs,",
197                  " $SpecialCnt specials, $ErrorCnt errors\n";
198 }
199 if ( $ErrorCnt && !$FileCnt && !$DirCnt ) {
200     #
201     # Got errors, with no files or directories; exit with non-zero
202     # status
203     #
204     exit(1);
205 }
206 exit(0);
207
208 ###########################################################################
209 # Subroutines
210 ###########################################################################
211
212 sub archiveWrite
213 {
214     my($fh, $dir, $tarPathOverride) = @_;
215
216     if ( $dir =~ m{(^|/)\.\.(/|$)} ) {
217         print(STDERR "$0: bad directory '$dir'\n");
218         $ErrorCnt++;
219         return;
220     }
221     $dir = "/" if ( $dir eq "." );
222     #print(STDERR "calling find with $Num, $ShareName, $dir\n");
223     if ( $view->find($Num, $ShareName, $dir, 0, \&TarWriteFile,
224                 $fh, $tarPathOverride) < 0 ) {
225         print(STDERR "$0: bad share or directory '$ShareName/$dir'\n");
226         $ErrorCnt++;
227         return;
228     }
229 }
230
231 #
232 # Write out any hardlinks (if any)
233 #
234 sub archiveWriteHardLinks
235 {
236     my($fh) = @_;
237     foreach my $hdr ( @HardLinks ) {
238         $hdr->{size} = 0;
239         my $name = $hdr->{linkname};
240         $name =~ s{^\./}{/};
241         if ( defined($HardLinkExtraFiles{$name}) ) {
242             $hdr->{linkname} = $HardLinkExtraFiles{$name};
243         }
244         if ( defined($PathRemove)
245               && substr($hdr->{linkname}, 0, length($PathRemove)+1)
246                         eq ".$PathRemove" ) {
247             substr($hdr->{linkname}, 0, length($PathRemove)+1) = ".$PathAdd";
248         }
249         TarWriteFileInfo($fh, $hdr);
250     }
251     @HardLinks = ();
252     %HardLinkExtraFiles = ();
253 }
254
255 sub UidLookup
256 {
257     my($uid) = @_;
258
259     $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) );
260     return $UidCache{$uid};
261 }
262
263 sub GidLookup
264 {
265     my($gid) = @_;
266
267     $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) );
268     return $GidCache{$gid};
269 }
270
271 sub TarWrite
272 {
273     my($fh, $dataRef) = @_;
274
275     if ( !defined($dataRef) ) {
276         #
277         # do flush by padding to a full $WriteBufSz
278         #
279         my $data = "\0" x ($WriteBufSz - length($WriteBuf));
280         $dataRef = \$data;
281     }
282     if ( length($WriteBuf) + length($$dataRef) < $WriteBufSz ) {
283         #
284         # just buffer and return
285         #
286         $WriteBuf .= $$dataRef;
287         return;
288     }
289     my $done = $WriteBufSz - length($WriteBuf);
290     if ( syswrite($fh, $WriteBuf . substr($$dataRef, 0, $done))
291                                 != $WriteBufSz ) {
292         print(STDERR "Unable to write to output file ($!)\n");
293         exit(1);
294     }
295     while ( $done + $WriteBufSz <= length($$dataRef) ) {
296         if ( syswrite($fh, substr($$dataRef, $done, $WriteBufSz))
297                             != $WriteBufSz ) {
298             print(STDERR "Unable to write to output file ($!)\n");
299             exit(1);
300         }
301         $done += $WriteBufSz;
302     }
303     $WriteBuf = substr($$dataRef, $done);
304 }
305
306 sub TarWritePad
307 {
308     my($fh, $size) = @_;
309
310     if ( $size % $tar_header_length ) {
311         my $data = "\0" x ($tar_header_length - ($size % $tar_header_length));
312         TarWrite($fh, \$data);
313     }
314 }
315
316 sub TarWriteHeader
317 {
318     my($fh, $hdr) = @_;
319
320     $hdr->{uname} = UidLookup($hdr->{uid}) if ( !defined($hdr->{uname}) );
321     $hdr->{gname} = GidLookup($hdr->{gid}) if ( !defined($hdr->{gname}) );
322     my $devmajor = defined($hdr->{devmajor}) ? sprintf("%07o", $hdr->{devmajor})
323                                              : "";
324     my $devminor = defined($hdr->{devminor}) ? sprintf("%07o", $hdr->{devminor})
325                                              : "";
326     my $sizeStr;
327     if ( $hdr->{size} >= 2 * 65536 * 65536 ) {
328         #
329         # GNU extension for files >= 8GB: send size in big-endian binary
330         #
331         $sizeStr = pack("c4 N N", 0x80, 0, 0, 0,
332                                   $hdr->{size} / (65536 * 65536),
333                                   $hdr->{size} % (65536 * 65536));
334     } elsif ( $hdr->{size} >= 1 * 65536 * 65536 ) {
335         #
336         # sprintf octal only handles up to 2^32 - 1
337         #
338         $sizeStr = sprintf("%03o", $hdr->{size} / (1 << 24))
339                  . sprintf("%08o", $hdr->{size} % (1 << 24));
340     } else {
341         $sizeStr = sprintf("%011o", $hdr->{size});
342     }
343     my $data = pack($tar_pack_header,
344                      substr($hdr->{name}, 0, 99),
345                      sprintf("%07o", $hdr->{mode}),
346                      sprintf("%07o", $hdr->{uid}),
347                      sprintf("%07o", $hdr->{gid}),
348                      $sizeStr,
349                      sprintf("%011o", $hdr->{mtime}),
350                      "",        #checksum field - space padded by pack("A8")
351                      $hdr->{type},
352                      substr($hdr->{linkname}, 0, 99),
353                      $hdr->{magic} || 'ustar ',
354                      $hdr->{version} || ' ',
355                      $hdr->{uname},
356                      $hdr->{gname},
357                      $devmajor,
358                      $devminor,
359                      ""         # prefix is empty
360                  );
361     substr($data, 148, 7) = sprintf("%06o\0", unpack("%16C*",$data));
362     TarWrite($fh, \$data);
363 }
364
365 sub TarWriteFileInfo
366 {
367     my($fh, $hdr) = @_;
368
369     #
370     # Convert path names to requested (eg: client) charset
371     #
372     if ( $Charset ne "" ) {
373         from_to($hdr->{name},     "utf8", $Charset);
374         from_to($hdr->{linkname}, "utf8", $Charset);
375     }
376
377     #
378     # Handle long link names (symbolic links)
379     #
380     if ( length($hdr->{linkname}) > 99 ) {
381         my %h;
382         my $data = $hdr->{linkname} . "\0";
383         $h{name} = "././\@LongLink";
384         $h{type} = "K";
385         $h{size} = length($data);
386         TarWriteHeader($fh, \%h);
387         TarWrite($fh, \$data);
388         TarWritePad($fh, length($data));
389     }
390
391     #
392     # Handle long file names
393     #
394     if ( length($hdr->{name}) > 99 ) {
395         my %h;
396         my $data = $hdr->{name} . "\0";
397         $h{name} = "././\@LongLink";
398         $h{type} = "L";
399         $h{size} = length($data);
400         TarWriteHeader($fh, \%h);
401         TarWrite($fh, \$data);
402         TarWritePad($fh, length($data));
403     }
404     TarWriteHeader($fh, $hdr);
405 }
406
407 my $Attr;
408 my $AttrDir;
409
410 sub TarWriteFile
411 {
412     my($hdr, $fh, $tarPathOverride) = @_;
413
414     my $tarPath = $hdr->{relPath};
415     $tarPath = $tarPathOverride if ( defined($tarPathOverride) );
416
417     $tarPath =~ s{//+}{/}g;
418     if ( defined($PathRemove)
419             && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) {
420         substr($tarPath, 0, length($PathRemove)) = $PathAdd;
421     }
422     $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// );
423     $tarPath =~ s{//+}{/}g;
424     $hdr->{name} = $tarPath;
425
426     if ( $hdr->{type} == BPC_FTYPE_DIR ) {
427         #
428         # Directory: just write the header
429         #
430         $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
431         TarWriteFileInfo($fh, $hdr);
432         $DirCnt++;
433     } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
434         #
435         # Regular file: write the header and file
436         #
437         my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0, $hdr->{compress});
438         if ( !defined($f) ) {
439             print(STDERR "Unable to open file $hdr->{fullPath}\n");
440             $ErrorCnt++;
441             return;
442         }
443         TarWriteFileInfo($fh, $hdr);
444         my($data, $size);
445         while ( $f->read(\$data, $BufSize) > 0 ) {
446             if ( $size + length($data) > $hdr->{size} ) {
447                 print(STDERR "Error: truncating $hdr->{fullPath} to"
448                            . " $hdr->{size} bytes\n");
449                 $data = substr($data, 0, $hdr->{size} - $size);
450                 $ErrorCnt++;
451             }
452             TarWrite($fh, \$data);
453             $size += length($data);
454         }
455         $f->close;
456         if ( $size != $hdr->{size} ) {
457             print(STDERR "Error: padding $hdr->{fullPath} to $hdr->{size}"
458                        . " bytes from $size bytes\n");
459             $ErrorCnt++;
460             while ( $size < $hdr->{size} ) {
461                 my $len = $hdr->{size} - $size;
462                 $len = $BufSize if ( $len > $BufSize );
463                 $data = "\0" x $len;
464                 TarWrite($fh, \$data);
465                 $size += $len;
466             }
467         }
468         TarWritePad($fh, $size);
469         $FileCnt++;
470         $ByteCnt += $size;
471     } elsif ( $hdr->{type} == BPC_FTYPE_HARDLINK ) {
472         #
473         # Hardlink file: either write a hardlink or the complete file
474         # depending upon whether the linked-to file will be written
475         # to the archive.
476         #
477         # Start by reading the contents of the link.
478         #
479         my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0, $hdr->{compress});
480         if ( !defined($f) ) {
481             print(STDERR "Unable to open file $hdr->{fullPath}\n");
482             $ErrorCnt++;
483             return;
484         }
485         my $data;
486         while ( $f->read(\$data, $BufSize) > 0 ) {
487             $hdr->{linkname} .= $data;
488         }
489         $f->close;
490         #
491         # Check @ARGV and the list of hardlinked files we have explicity
492         # dumped to see if we have dumped this file or not
493         #
494         my $done = 0;
495         my $name = $hdr->{linkname};
496         $name =~ s{^\./}{/};
497         if ( defined($HardLinkExtraFiles{$name}) ) {
498             $done = 1;
499         } else {
500             foreach my $arg ( @ARGV ) {
501                 $arg = "/" if ( $arg eq "." );
502                 $arg =~ s{^\./+}{/};
503                 $arg =~ s{/+$}{};
504                 $done = 1 if ( $name eq $arg || $name =~ /^\Q$arg\// );
505             }
506         }
507         if ( $done ) {
508             #
509             # Target file will be or was written, so just remember
510             # the hardlink so we can dump it later.
511             #
512             push(@HardLinks, $hdr);
513             $SpecialCnt++;
514         } else {
515             #
516             # Have to dump the original file.  Just call the top-level
517             # routine, so that we save the hassle of dealing with
518             # mangling, merging and attributes.
519             #
520             my $name = $hdr->{linkname};
521             $name =~ s{^\./}{/};
522             $HardLinkExtraFiles{$name} = $hdr->{name};
523             archiveWrite($fh, $name, $hdr->{name});
524         }
525     } elsif ( $hdr->{type} == BPC_FTYPE_SYMLINK ) {
526         #
527         # Symbolic link: read the symbolic link contents into the header
528         # and write the header.
529         #
530         my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0, $hdr->{compress});
531         if ( !defined($f) ) {
532             print(STDERR "Unable to open symlink file $hdr->{fullPath}\n");
533             $ErrorCnt++;
534             return;
535         }
536         my $data;
537         while ( $f->read(\$data, $BufSize) > 0 ) {
538             $hdr->{linkname} .= $data;
539         }
540         $f->close;
541         $hdr->{size} = 0;
542         TarWriteFileInfo($fh, $hdr);
543         $SpecialCnt++;
544     } elsif ( $hdr->{type} == BPC_FTYPE_CHARDEV
545            || $hdr->{type} == BPC_FTYPE_BLOCKDEV
546            || $hdr->{type} == BPC_FTYPE_FIFO ) {
547         #
548         # Special files: for char and block special we read the
549         # major and minor numbers from a plain file.
550         #
551         if ( $hdr->{type} != BPC_FTYPE_FIFO ) {
552             my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0,
553                                                 $hdr->{compress});
554             my $data;
555             if ( !defined($f) || $f->read(\$data, $BufSize) < 0 ) {
556                 print(STDERR "Unable to open/read char/block special file"
557                            . " $hdr->{fullPath}\n");
558                 $f->close if ( defined($f) );
559                 $ErrorCnt++;
560                 return;
561             }
562             $f->close;
563             if ( $data =~ /(\d+),(\d+)/ ) {
564                 $hdr->{devmajor} = $1;
565                 $hdr->{devminor} = $2;
566             }
567         }
568         $hdr->{size} = 0;
569         TarWriteFileInfo($fh, $hdr);
570         $SpecialCnt++;
571     } else {
572         print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n");
573         $ErrorCnt++;
574     }
575 }