#!/bin/perl -T #============================================================= -*-perl-*- # # BackupPC_zipCreate: create a zip archive of an existing dump # for restore on a client. # # DESCRIPTION # # Usage: BackupPC_zipCreate [-t] [-h host] [-n dumpNum] [-s shareName] # [-r pathRemove] [-p pathAdd] [-c compressionLevel] # files/directories... # # Flags: # Required options: # # -h host host from which the zip archive is created # -n dumpNum dump number from which the zip archive is created # -s shareName share name from which the zip archive is created # # Other options: # -t print summary totals # -r pathRemove path prefix that will be replaced with pathAdd # -p pathAdd new path prefix # -c level compression level (default is 0, no compression) # # The -h, -n and -s options specify which dump is used to generate # the zip archive. The -r and -p options can be used to relocate # the paths in the zip archive so extracted files can be placed # in a location different from their original location. # # AUTHOR # Guillaume Filion # Based on Backup_tarCreate by Craig Barratt # # COPYRIGHT # Copyright (C) 2002 Craig Barratt and Guillaume Filion # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #======================================================================== # # Version 1.5.0, released 2 Aug 2002. # # See http://backuppc.sourceforge.net. # #======================================================================== use strict; use lib "__INSTALLDIR__/lib"; use Archive::Zip qw(:ERROR_CODES); use File::Path; use Getopt::Std; use IO::Handle; use BackupPC::Lib; use BackupPC::Attrib qw(:all); use BackupPC::FileZIO; use BackupPC::Zip::FileMember; die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) ); my $TopDir = $bpc->TopDir(); my $BinDir = $bpc->BinDir(); my %Conf = $bpc->Conf(); my %opts; getopts("th:n:p:r:s:c:", \%opts); if ( @ARGV < 1 ) { print(STDERR "usage: $0 [-t] [-h host] [-n dumpNum] [-s shareName]" . " [-r pathRemove] [-p pathAdd] [-c compressionLevel]" . " files/directories...\n"); exit(1); } if ( $opts{h} !~ /^([\w\.-]+)$/ ) { print(STDERR "$0: bad host name '$opts{h}'\n"); exit(1); } my $Host = $opts{h}; if ( $opts{n} !~ /^(\d+)$/ ) { print(STDERR "$0: bad dump number '$opts{n}'\n"); exit(1); } my $Num = $opts{n}; $opts{c} = 0 if ( $opts{c} eq "" ); if ( $opts{c} !~ /^(\d+)$/ ) { print(STDERR "$0: invalid compression level '$opts{c}'. 0=none, 9=max\n"); exit(1); } my $compLevel = $opts{c}; my @Backups = $bpc->BackupInfoRead($Host); my($Compress, $Mangle, $CompressF, $MangleF, $NumF, $i); my $FileCnt = 0; my $ByteCnt = 0; my $DirCnt = 0; my $SpecialCnt = 0; my $ErrorCnt = 0; for ( $i = 0 ; $i < @Backups ; $i++ ) { if ( !$Backups[$i]{noFill} ) { # # Remember the most recent filled backup # $NumF = $Backups[$i]{num}; $MangleF = $Backups[$i]{mangle}; $CompressF = $Backups[$i]{compress}; } next if ( $Backups[$i]{num} != $Num ); $Compress = $Backups[$i]{compress}; $Mangle = $Backups[$i]{mangle}; if ( !$Backups[$i]{noFill} ) { # no need to back-fill a filled backup $NumF = $MangleF = $CompressF = undef; } last; } if ( $i >= @Backups ) { print(STDERR "$0: bad backup number $Num for host $Host\n"); exit(1); } my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ ); my $PathAdd = $1 if ( $opts{p} =~ /(.+)/ ); if ( $opts{s} !~ /^([\w\s\.\/\$-]+)$/ ) { print(STDERR "$0: bad share name '$opts{s}'\n"); exit(1); } my $ShareNameOrig = $opts{s}; my $ShareName = $Mangle ? $bpc->fileNameEltMangle($ShareNameOrig) : $ShareNameOrig; my $ShareNameF = $MangleF ? $bpc->fileNameEltMangle($ShareNameOrig) : $ShareNameOrig; my $BufSize = 1048576; # 1MB or 2^20 my(%UidCache, %GidCache); #my $fh = *STDOUT; my $fh = new IO::Handle; $fh->fdopen(fileno(STDOUT),"w"); my $zipfh = Archive::Zip->new(); foreach my $dir ( @ARGV ) { archiveWrite($zipfh, $dir); } sub archiveWrite { my($zipfh, $dir, $zipPathOverride) = @_; if ( $dir =~ m{(^|/)\.\.(/|$)} || $dir !~ /^(.*)$/ ) { print(STDERR "$0: bad directory '$dir'\n"); $ErrorCnt++; next; } (my $DirOrig = $1) =~ s{/+$}{}; $DirOrig =~ s{^\.?/+}{}; my($Dir, $DirF, $FullPath, $FullPathF); if ( $DirOrig eq "" ) { $Dir = $DirF = ""; $FullPath = "$TopDir/pc/$Host/$Num/$ShareName"; $FullPathF = "$TopDir/pc/$Host/$NumF/$ShareNameF" if ( defined($NumF) ); } else { $Dir = $Mangle ? $bpc->fileNameMangle($DirOrig) : $DirOrig; $DirF = $MangleF ? $bpc->fileNameMangle($DirOrig) : $DirOrig; $FullPath = "$TopDir/pc/$Host/$Num/$ShareName/$Dir"; $FullPathF = "$TopDir/pc/$Host/$NumF/$ShareNameF/$DirF" if ( defined($NumF) ); } if ( -f $FullPath ) { ZipWriteFile($zipfh, $FullPath, $Mangle, $Compress, $zipPathOverride); } elsif ( -d $FullPath || (defined($NumF) && -d $FullPathF) ) { MergeFind($zipfh, $FullPath, $FullPathF); } elsif ( defined($NumF) && -f $FullPathF ) { ZipWriteFile($zipfh, $FullPathF, $MangleF, $CompressF, $zipPathOverride); } else { print(STDERR "$0: $Host, backup $Num, doesn't have a directory or file" . " $ShareNameOrig/$DirOrig\n"); $ErrorCnt++; } } # Create Zip file print STDERR "Can't write Zip file\n" unless $zipfh->writeToFileHandle($fh, 0) == Archive::Zip::AZ_OK; # # print out totals if requested # if ( $opts{t} ) { print STDERR "Done: $FileCnt files, $ByteCnt bytes, $DirCnt dirs,", " $SpecialCnt specials ignored, $ErrorCnt errors\n"; } exit(0); ########################################################################### # Subroutines ########################################################################### sub UidLookup { my($uid) = @_; $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) ); return $UidCache{$uid}; } sub GidLookup { my($gid) = @_; $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) ); return $GidCache{$gid}; } my $Attr; my $AttrDir; sub ZipWriteFile { my($zipfh, $fullName, $mangle, $compress, $zipPathOverride) = @_; my($tarPath); if ( $fullName =~ m{^\Q$TopDir/pc/$Host/$Num/$ShareName\E(.*)} || (defined($NumF) && $fullName =~ m{^\Q$TopDir/pc/$Host/$NumF/$ShareNameF\E(.*)}) ) { $tarPath = $mangle ? $bpc->fileNameUnmangle($1) : $1; } else { print(STDERR "Unexpected file name from find: $fullName\n"); return; } $tarPath = $zipPathOverride if ( defined($zipPathOverride) ); (my $dir = $fullName) =~ s{/([^/]*)$}{}; my $fileName = $mangle ? $bpc->fileNameUnmangle($1) : $1; if ( $mangle && $AttrDir ne $dir ) { $AttrDir = $dir; $Attr = BackupPC::Attrib->new({ compress => $compress }); if ( -f $Attr->fileName($dir) && !$Attr->read($dir) ) { print(STDERR "Can't read attribute file in $dir\n"); $ErrorCnt++; $Attr = undef; } } my $hdr = $Attr->get($fileName) if ( defined($Attr) ); if ( !defined($hdr) ) { # # No attributes. Must be an old style backup. Reconstruct # what we can. Painful part is computing the size if compression # is on: only method is to uncompress the file. # my @s = stat($fullName); $hdr = { type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE, mode => $s[2], uid => $s[4], gid => $s[5], size => -f _ ? $s[7] : 0, mtime => $s[9], }; if ( $compress && -f _ ) { # # Compute the correct size by reading the whole file # my $f = BackupPC::FileZIO->open($fullName, 0, $compress); if ( !defined($f) ) { print(STDERR "Unable to open file $fullName\n"); $ErrorCnt++; return; } my($data, $size); while ( $f->read(\$data, $BufSize) > 0 ) { $size += length($data); } $f->close; $hdr->{size} = $size; } } if ( defined($PathRemove) && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) { substr($tarPath, 0, length($PathRemove)) = $PathAdd; } $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// ); $tarPath =~ s{//+}{/}g; $hdr->{name} = $tarPath; my $zipmember; # Container to hold the file/directory to zip. if ( $hdr->{type} == BPC_FTYPE_DIR ) { # # Directory: just write the header # $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} ); $zipmember = Archive::Zip::Member->newDirectoryNamed($hdr->{name}); $DirCnt++; } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) { # # Regular file: write the header and file # $zipmember = BackupPC::Zip::FileMember->newFromFileNamed( $fullName, $hdr->{name}, $hdr->{size}, $compress ); $FileCnt++; $ByteCnt += $hdr->{size}; } elsif ( $hdr->{type} == BPC_FTYPE_HARDLINK ) { # # Hardlink file: not supported by Zip, so just make a copy # of the pointed-to file. # # Start by reading the contents of the link. # my $f = BackupPC::FileZIO->open($fullName, 0, $compress); if ( !defined($f) ) { print(STDERR "Unable to open file $fullName\n"); $ErrorCnt++; return; } my $data; while ( $f->read(\$data, $BufSize) > 0 ) { $hdr->{linkname} .= $data; } $f->close; # # Dump the original file. Just call the top-level # routine, so that we save the hassle of dealing with # mangling, merging and attributes. # archiveWrite($zipfh, $hdr->{linkname}, $hdr->{name}); } elsif ( $hdr->{type} == BPC_FTYPE_SYMLINK ) { # # Symlinks can't be Zipped. 8( # We could zip the pointed-to dir/file (just like hardlink), but we # have to avoid the infinite-loop case of a symlink pointed to a # directory above us. Ignore for now. Could be a comand-line # option later. # $SpecialCnt++; } elsif ( $hdr->{type} == BPC_FTYPE_CHARDEV || $hdr->{type} == BPC_FTYPE_BLOCKDEV || $hdr->{type} == BPC_FTYPE_FIFO ) { # # Special files can't be Zipped. 8( # $SpecialCnt++; } else { print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n"); $ErrorCnt++; } return if ( !$zipmember ); # Set the attributes and permissions $zipmember->setLastModFileDateTimeFromUnix($hdr->{mtime}); $zipmember->unixFileAttributes($hdr->{mode}); # Zip files don't accept uid and gid, so we put them in the comment field. $zipmember->fileComment("uid=".$hdr->{uid}." gid=".$hdr->{gid}) if ( $hdr->{uid} || $hdr->{gid} ); # Specify the compression level for this member $zipmember->desiredCompressionLevel($compLevel) if ($compLevel =~ /[0-9]/); # Finally Zip the member $zipfh->addMember($zipmember); } # # Does a recursive find of $dir, filling in from the (filled dump) # directory $dirF. Handles the cases where $dir and $dirF might # or might not be mangled etc. # sub MergeFind { my($zipfh, $dir, $dirF) = @_; my(@Dir, $fLast); if ( -d $dir ) { ZipWriteFile($zipfh, $dir, $Mangle, $Compress); } elsif ( -d $dirF ) { ZipWriteFile($zipfh, $dirF, $MangleF, $CompressF); } if ( opendir(DIR, $dir) ) { @Dir = readdir(DIR); closedir(DIR); } if ( defined($NumF) && opendir(DIR, $dirF) ) { if ( $Mangle == $MangleF ) { @Dir = (@Dir, readdir(DIR)); } else { foreach my $f ( readdir(DIR) ) { if ( $Mangle ) { push(@Dir, $bpc->fileNameMangle($f)); } else { push(@Dir, $bpc->fileNameUnmangle($f)); } } } } foreach my $f ( sort({$a cmp $b} @Dir) ) { next if ( $f eq "." || $f eq ".." || $f eq $fLast || ($Mangle && $f eq "attrib") ); $fLast = $f; my($fF) = $f; if ( $Mangle != $MangleF ) { $fF = $Mangle ? $bpc->fileNameUnmangle($f) : $bpc->fileNameMangle($f); } if ( -e "$dir/$f" ) { if ( -d "$dir/$f" ) { MergeFind($zipfh, "$dir/$f", "$dirF/$fF"); } else { ZipWriteFile($zipfh, "$dir/$f", $Mangle, $Compress); } } elsif ( -e "$dirF/$fF" ) { if ( -d "$dirF/$fF" ) { MergeFind($zipfh, "$dir/$f", "$dirF/$fF"); } else { ZipWriteFile($zipfh, "$dirF/$fF", $MangleF, $CompressF); } } else { print(STDERR "$0: Botch on $dir, $dirF, $f, $fF\n"); $ErrorCnt++; } } }