2 #============================================================= -*-perl-*-
4 # BackupPC_zipCreate: create a zip archive of an existing dump
5 # for restore on a client.
9 # Usage: BackupPC_zipCreate [-t] [-h host] [-n dumpNum] [-s shareName]
10 # [-r pathRemove] [-p pathAdd] [-c compressionLevel]
11 # files/directories...
16 # -h host host from which the zip archive is created
17 # -n dumpNum dump number from which the zip archive is created
18 # -s shareName share name from which the zip archive is created
21 # -t print summary totals
22 # -r pathRemove path prefix that will be replaced with pathAdd
23 # -p pathAdd new path prefix
24 # -c level compression level (default is 0, no compression)
26 # The -h, -n and -s options specify which dump is used to generate
27 # the zip archive. The -r and -p options can be used to relocate
28 # the paths in the zip archive so extracted files can be placed
29 # in a location different from their original location.
32 # Guillaume Filion <gfk@users.sourceforge.net>
33 # Based on Backup_tarCreate by Craig Barratt <cbarratt@users.sourceforge.net>
36 # Copyright (C) 2002 Craig Barratt and Guillaume Filion
38 # This program is free software; you can redistribute it and/or modify
39 # it under the terms of the GNU General Public License as published by
40 # the Free Software Foundation; either version 2 of the License, or
41 # (at your option) any later version.
43 # This program is distributed in the hope that it will be useful,
44 # but WITHOUT ANY WARRANTY; without even the implied warranty of
45 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46 # GNU General Public License for more details.
48 # You should have received a copy of the GNU General Public License
49 # along with this program; if not, write to the Free Software
50 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
52 #========================================================================
54 # Version 1.5.0, released 2 Aug 2002.
56 # See http://backuppc.sourceforge.net.
58 #========================================================================
61 use lib "__INSTALLDIR__/lib";
62 use Archive::Zip qw(:ERROR_CODES);
67 use BackupPC::Attrib qw(:all);
68 use BackupPC::FileZIO;
69 use BackupPC::Zip::FileMember;
71 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
72 my $TopDir = $bpc->TopDir();
73 my $BinDir = $bpc->BinDir();
74 my %Conf = $bpc->Conf();
77 getopts("th:n:p:r:s:c:", \%opts);
80 print(STDERR "usage: $0 [-t] [-h host] [-n dumpNum] [-s shareName]"
81 . " [-r pathRemove] [-p pathAdd] [-c compressionLevel]"
82 . " files/directories...\n");
86 if ( $opts{h} !~ /^([\w\.-]+)$/ ) {
87 print(STDERR "$0: bad host name '$opts{h}'\n");
92 if ( $opts{n} !~ /^(\d+)$/ ) {
93 print(STDERR "$0: bad dump number '$opts{n}'\n");
98 $opts{c} = 0 if ( $opts{c} eq "" );
99 if ( $opts{c} !~ /^(\d+)$/ ) {
100 print(STDERR "$0: invalid compression level '$opts{c}'. 0=none, 9=max\n");
103 my $compLevel = $opts{c};
105 my @Backups = $bpc->BackupInfoRead($Host);
106 my($Compress, $Mangle, $CompressF, $MangleF, $NumF, $i);
113 for ( $i = 0 ; $i < @Backups ; $i++ ) {
114 if ( !$Backups[$i]{noFill} ) {
116 # Remember the most recent filled backup
118 $NumF = $Backups[$i]{num};
119 $MangleF = $Backups[$i]{mangle};
120 $CompressF = $Backups[$i]{compress};
122 next if ( $Backups[$i]{num} != $Num );
123 $Compress = $Backups[$i]{compress};
124 $Mangle = $Backups[$i]{mangle};
125 if ( !$Backups[$i]{noFill} ) {
126 # no need to back-fill a filled backup
127 $NumF = $MangleF = $CompressF = undef;
131 if ( $i >= @Backups ) {
132 print(STDERR "$0: bad backup number $Num for host $Host\n");
136 my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ );
137 my $PathAdd = $1 if ( $opts{p} =~ /(.+)/ );
138 if ( $opts{s} !~ /^([\w\s\.\/\$-]+)$/ ) {
139 print(STDERR "$0: bad share name '$opts{s}'\n");
142 my $ShareNameOrig = $opts{s};
143 my $ShareName = $Mangle ? $bpc->fileNameEltMangle($ShareNameOrig)
145 my $ShareNameF = $MangleF ? $bpc->fileNameEltMangle($ShareNameOrig)
148 my $BufSize = 1048576; # 1MB or 2^20
149 my(%UidCache, %GidCache);
151 my $fh = new IO::Handle;
152 $fh->fdopen(fileno(STDOUT),"w");
153 my $zipfh = Archive::Zip->new();
155 foreach my $dir ( @ARGV ) {
156 archiveWrite($zipfh, $dir);
161 my($zipfh, $dir, $zipPathOverride) = @_;
162 if ( $dir =~ m{(^|/)\.\.(/|$)} || $dir !~ /^(.*)$/ ) {
163 print(STDERR "$0: bad directory '$dir'\n");
167 (my $DirOrig = $1) =~ s{/+$}{};
168 $DirOrig =~ s{^\.?/+}{};
169 my($Dir, $DirF, $FullPath, $FullPathF);
170 if ( $DirOrig eq "" ) {
172 $FullPath = "$TopDir/pc/$Host/$Num/$ShareName";
173 $FullPathF = "$TopDir/pc/$Host/$NumF/$ShareNameF"
174 if ( defined($NumF) );
176 $Dir = $Mangle ? $bpc->fileNameMangle($DirOrig) : $DirOrig;
177 $DirF = $MangleF ? $bpc->fileNameMangle($DirOrig) : $DirOrig;
178 $FullPath = "$TopDir/pc/$Host/$Num/$ShareName/$Dir";
179 $FullPathF = "$TopDir/pc/$Host/$NumF/$ShareNameF/$DirF"
180 if ( defined($NumF) );
182 if ( -f $FullPath ) {
183 ZipWriteFile($zipfh, $FullPath, $Mangle, $Compress, $zipPathOverride);
184 } elsif ( -d $FullPath || (defined($NumF) && -d $FullPathF) ) {
185 MergeFind($zipfh, $FullPath, $FullPathF);
186 } elsif ( defined($NumF) && -f $FullPathF ) {
187 ZipWriteFile($zipfh, $FullPathF, $MangleF, $CompressF,
190 print(STDERR "$0: $Host, backup $Num, doesn't have a directory or file"
191 . " $ShareNameOrig/$DirOrig\n");
197 print STDERR "Can't write Zip file\n"
198 unless $zipfh->writeToFileHandle($fh, 0) == Archive::Zip::AZ_OK;
201 # print out totals if requested
204 print STDERR "Done: $FileCnt files, $ByteCnt bytes, $DirCnt dirs,",
205 " $SpecialCnt specials ignored, $ErrorCnt errors\n";
209 ###########################################################################
211 ###########################################################################
217 $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) );
218 return $UidCache{$uid};
225 $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) );
226 return $GidCache{$gid};
234 my($zipfh, $fullName, $mangle, $compress, $zipPathOverride) = @_;
237 if ( $fullName =~ m{^\Q$TopDir/pc/$Host/$Num/$ShareName\E(.*)}
239 && $fullName =~ m{^\Q$TopDir/pc/$Host/$NumF/$ShareNameF\E(.*)}) ) {
240 $tarPath = $mangle ? $bpc->fileNameUnmangle($1) : $1;
242 print(STDERR "Unexpected file name from find: $fullName\n");
245 $tarPath = $zipPathOverride if ( defined($zipPathOverride) );
246 (my $dir = $fullName) =~ s{/([^/]*)$}{};
247 my $fileName = $mangle ? $bpc->fileNameUnmangle($1) : $1;
248 if ( $mangle && $AttrDir ne $dir ) {
250 $Attr = BackupPC::Attrib->new({ compress => $compress });
251 if ( -f $Attr->fileName($dir) && !$Attr->read($dir) ) {
252 print(STDERR "Can't read attribute file in $dir\n");
257 my $hdr = $Attr->get($fileName) if ( defined($Attr) );
258 if ( !defined($hdr) ) {
260 # No attributes. Must be an old style backup. Reconstruct
261 # what we can. Painful part is computing the size if compression
262 # is on: only method is to uncompress the file.
264 my @s = stat($fullName);
266 type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE,
270 size => -f _ ? $s[7] : 0,
273 if ( $compress && -f _ ) {
275 # Compute the correct size by reading the whole file
277 my $f = BackupPC::FileZIO->open($fullName, 0, $compress);
278 if ( !defined($f) ) {
279 print(STDERR "Unable to open file $fullName\n");
284 while ( $f->read(\$data, $BufSize) > 0 ) {
285 $size += length($data);
288 $hdr->{size} = $size;
291 if ( defined($PathRemove)
292 && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) {
293 substr($tarPath, 0, length($PathRemove)) = $PathAdd;
295 $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// );
296 $tarPath =~ s{//+}{/}g;
297 $hdr->{name} = $tarPath;
298 my $zipmember; # Container to hold the file/directory to zip.
300 if ( $hdr->{type} == BPC_FTYPE_DIR ) {
302 # Directory: just write the header
304 $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
305 $zipmember = Archive::Zip::Member->newDirectoryNamed($hdr->{name});
307 } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
309 # Regular file: write the header and file
311 $zipmember = BackupPC::Zip::FileMember->newFromFileNamed(
318 $ByteCnt += $hdr->{size};
319 } elsif ( $hdr->{type} == BPC_FTYPE_HARDLINK ) {
321 # Hardlink file: not supported by Zip, so just make a copy
322 # of the pointed-to file.
324 # Start by reading the contents of the link.
326 my $f = BackupPC::FileZIO->open($fullName, 0, $compress);
327 if ( !defined($f) ) {
328 print(STDERR "Unable to open file $fullName\n");
333 while ( $f->read(\$data, $BufSize) > 0 ) {
334 $hdr->{linkname} .= $data;
338 # Dump the original file. Just call the top-level
339 # routine, so that we save the hassle of dealing with
340 # mangling, merging and attributes.
342 archiveWrite($zipfh, $hdr->{linkname}, $hdr->{name});
343 } elsif ( $hdr->{type} == BPC_FTYPE_SYMLINK ) {
345 # Symlinks can't be Zipped. 8(
346 # We could zip the pointed-to dir/file (just like hardlink), but we
347 # have to avoid the infinite-loop case of a symlink pointed to a
348 # directory above us. Ignore for now. Could be a comand-line
352 } elsif ( $hdr->{type} == BPC_FTYPE_CHARDEV
353 || $hdr->{type} == BPC_FTYPE_BLOCKDEV
354 || $hdr->{type} == BPC_FTYPE_FIFO ) {
356 # Special files can't be Zipped. 8(
360 print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n");
363 return if ( !$zipmember );
365 # Set the attributes and permissions
366 $zipmember->setLastModFileDateTimeFromUnix($hdr->{mtime});
367 $zipmember->unixFileAttributes($hdr->{mode});
368 # Zip files don't accept uid and gid, so we put them in the comment field.
369 $zipmember->fileComment("uid=".$hdr->{uid}." gid=".$hdr->{gid})
370 if ( $hdr->{uid} || $hdr->{gid} );
372 # Specify the compression level for this member
373 $zipmember->desiredCompressionLevel($compLevel) if ($compLevel =~ /[0-9]/);
375 # Finally Zip the member
376 $zipfh->addMember($zipmember);
380 # Does a recursive find of $dir, filling in from the (filled dump)
381 # directory $dirF. Handles the cases where $dir and $dirF might
382 # or might not be mangled etc.
386 my($zipfh, $dir, $dirF) = @_;
390 ZipWriteFile($zipfh, $dir, $Mangle, $Compress);
391 } elsif ( -d $dirF ) {
392 ZipWriteFile($zipfh, $dirF, $MangleF, $CompressF);
394 if ( opendir(DIR, $dir) ) {
398 if ( defined($NumF) && opendir(DIR, $dirF) ) {
399 if ( $Mangle == $MangleF ) {
400 @Dir = (@Dir, readdir(DIR));
402 foreach my $f ( readdir(DIR) ) {
404 push(@Dir, $bpc->fileNameMangle($f));
406 push(@Dir, $bpc->fileNameUnmangle($f));
411 foreach my $f ( sort({$a cmp $b} @Dir) ) {
412 next if ( $f eq "." || $f eq ".."
413 || $f eq $fLast || ($Mangle && $f eq "attrib") );
416 if ( $Mangle != $MangleF ) {
417 $fF = $Mangle ? $bpc->fileNameUnmangle($f)
418 : $bpc->fileNameMangle($f);
420 if ( -e "$dir/$f" ) {
421 if ( -d "$dir/$f" ) {
422 MergeFind($zipfh, "$dir/$f", "$dirF/$fF");
424 ZipWriteFile($zipfh, "$dir/$f", $Mangle, $Compress);
426 } elsif ( -e "$dirF/$fF" ) {
427 if ( -d "$dirF/$fF" ) {
428 MergeFind($zipfh, "$dir/$f", "$dirF/$fF");
430 ZipWriteFile($zipfh, "$dirF/$fF", $MangleF, $CompressF);
433 print(STDERR "$0: Botch on $dir, $dirF, $f, $fF\n");