d51a21fea3654fa452b71599aec011a92024fb14
[BackupPC.git] / bin / BackupPC_zipCreate
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_zipCreate: create a zip archive of an existing dump
5 # for restore on a client.
6 #
7 # DESCRIPTION
8 #  
9 #   Usage: BackupPC_zipCreate [-t] [-h host] [-n dumpNum] [-s shareName]
10 #                   [-r pathRemove] [-p pathAdd] [-c compressionLevel]
11 #                   files/directories...
12 #
13 #   Flags:
14 #     Required options:
15 #
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
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 #       -c level        compression level (default is 0, no compression)
25 #
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.
30 #
31 # AUTHOR
32 #   Guillaume Filion <gfk@users.sourceforge.net>
33 #   Based on Backup_tarCreate by Craig Barratt <cbarratt@users.sourceforge.net>
34 #
35 # COPYRIGHT
36 #   Copyright (C) 2002  Craig Barratt and Guillaume Filion
37 #
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.
42 #
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.
47 #
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
51 #
52 #========================================================================
53 #
54 # Version 1.5.0, released 2 Aug 2002.
55 #
56 # See http://backuppc.sourceforge.net.
57 #
58 #========================================================================
59
60 use strict;
61 use lib "__INSTALLDIR__/lib";
62 use Archive::Zip qw(:ERROR_CODES);
63 use File::Path;
64 use Getopt::Std;
65 use IO::Handle;
66 use BackupPC::Lib;
67 use BackupPC::Attrib qw(:all);
68 use BackupPC::FileZIO;
69 use BackupPC::Zip::FileMember;
70
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();
75
76 my %opts;
77 getopts("th:n:p:r:s:c:", \%opts);
78
79 if ( @ARGV < 1 ) {
80     print(STDERR "usage: $0 [-t] [-h host] [-n dumpNum] [-s shareName]"
81                . " [-r pathRemove] [-p pathAdd] [-c compressionLevel]"
82                . " files/directories...\n");
83     exit(1);
84 }
85
86 if ( $opts{h} !~ /^([\w\.-]+)$/ ) {
87     print(STDERR "$0: bad host name '$opts{h}'\n");
88     exit(1);
89 }
90 my $Host = $opts{h};
91
92 if ( $opts{n} !~ /^(\d+)$/ ) {
93     print(STDERR "$0: bad dump number '$opts{n}'\n");
94     exit(1);
95 }
96 my $Num = $opts{n};
97
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");
101     exit(1);
102 }
103 my $compLevel = $opts{c};
104
105 my @Backups = $bpc->BackupInfoRead($Host);
106 my($Compress, $Mangle, $CompressF, $MangleF, $NumF, $i);
107 my $FileCnt = 0;
108 my $ByteCnt = 0;
109 my $DirCnt = 0;
110 my $SpecialCnt = 0;
111 my $ErrorCnt = 0;
112
113 for ( $i = 0 ; $i < @Backups ; $i++ ) {
114     if ( !$Backups[$i]{noFill} ) {
115         #
116         # Remember the most recent filled backup
117         #
118         $NumF      = $Backups[$i]{num};
119         $MangleF   = $Backups[$i]{mangle};
120         $CompressF = $Backups[$i]{compress};
121     }
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;
128     }
129     last;
130 }
131 if ( $i >= @Backups ) {
132     print(STDERR "$0: bad backup number $Num for host $Host\n");
133     exit(1);
134 }
135
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");
140     exit(1);
141 }
142 my $ShareNameOrig = $opts{s};
143 my $ShareName  = $Mangle  ? $bpc->fileNameEltMangle($ShareNameOrig)
144                           : $ShareNameOrig;
145 my $ShareNameF = $MangleF ? $bpc->fileNameEltMangle($ShareNameOrig)
146                           : $ShareNameOrig;
147
148 my $BufSize    = 1048576;     # 1MB or 2^20
149 my(%UidCache, %GidCache);
150 #my $fh = *STDOUT;
151 my $fh = new IO::Handle;      
152 $fh->fdopen(fileno(STDOUT),"w");
153 my $zipfh = Archive::Zip->new();
154
155 foreach my $dir ( @ARGV ) {
156     archiveWrite($zipfh, $dir);
157 }
158
159 sub archiveWrite
160 {
161     my($zipfh, $dir, $zipPathOverride) = @_;
162     if ( $dir =~ m{(^|/)\.\.(/|$)} || $dir !~ /^(.*)$/ ) {
163         print(STDERR "$0: bad directory '$dir'\n");
164         $ErrorCnt++;
165         next;
166     }
167     (my $DirOrig  = $1) =~ s{/+$}{};
168     $DirOrig      =~ s{^\.?/+}{};
169     my($Dir, $DirF, $FullPath, $FullPathF);
170     if ( $DirOrig eq "" ) {
171         $Dir = $DirF = "";
172         $FullPath  = "$TopDir/pc/$Host/$Num/$ShareName";
173         $FullPathF = "$TopDir/pc/$Host/$NumF/$ShareNameF"
174                                             if ( defined($NumF) );
175     } else {
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) );
181     }
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,
188                                                    $zipPathOverride);
189     } else {
190         print(STDERR "$0: $Host, backup $Num, doesn't have a directory or file"
191                    . " $ShareNameOrig/$DirOrig\n");
192         $ErrorCnt++;
193     }
194 }
195
196 # Create Zip file
197 print STDERR "Can't write Zip file\n"
198      unless $zipfh->writeToFileHandle($fh, 0) == Archive::Zip::AZ_OK;
199
200 #
201 # print out totals if requested
202 #
203 if ( $opts{t} ) {
204     print STDERR "Done: $FileCnt files, $ByteCnt bytes, $DirCnt dirs,",
205                  " $SpecialCnt specials ignored, $ErrorCnt errors\n";
206 }
207 exit(0);
208
209 ###########################################################################
210 # Subroutines
211 ###########################################################################
212
213 sub UidLookup
214 {
215     my($uid) = @_;
216
217     $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) );
218     return $UidCache{$uid};
219 }
220
221 sub GidLookup
222 {
223     my($gid) = @_;
224
225     $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) );
226     return $GidCache{$gid};
227 }
228
229 my $Attr;
230 my $AttrDir;
231
232 sub ZipWriteFile
233 {
234     my($zipfh, $fullName, $mangle, $compress, $zipPathOverride) = @_;
235     my($tarPath);
236
237     if ( $fullName =~ m{^\Q$TopDir/pc/$Host/$Num/$ShareName\E(.*)}
238         || (defined($NumF)
239             && $fullName =~ m{^\Q$TopDir/pc/$Host/$NumF/$ShareNameF\E(.*)}) ) {
240         $tarPath = $mangle ? $bpc->fileNameUnmangle($1) : $1;
241     } else {
242         print(STDERR "Unexpected file name from find: $fullName\n");
243         return;
244     }
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 ) {
249         $AttrDir = $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");
253             $ErrorCnt++;
254             $Attr = undef;
255         }
256     }
257     my $hdr = $Attr->get($fileName) if ( defined($Attr) );
258     if ( !defined($hdr) ) {
259         #
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.
263         #
264         my @s = stat($fullName);
265         $hdr = {
266             type  => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE,
267             mode  => $s[2],
268             uid   => $s[4],
269             gid   => $s[5],
270             size  => -f _ ? $s[7] : 0,
271             mtime => $s[9],
272         };
273         if ( $compress && -f _ ) {
274             #
275             # Compute the correct size by reading the whole file
276             #
277             my $f = BackupPC::FileZIO->open($fullName, 0, $compress);
278             if ( !defined($f) ) {
279                 print(STDERR "Unable to open file $fullName\n");
280                 $ErrorCnt++;
281                 return;
282             }
283             my($data, $size);
284             while ( $f->read(\$data, $BufSize) > 0 ) {
285                 $size += length($data);
286             }
287             $f->close;
288             $hdr->{size} = $size;
289         }
290     }
291     if ( defined($PathRemove)
292             && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) {
293         substr($tarPath, 0, length($PathRemove)) = $PathAdd;
294     }
295     $tarPath = "./" . $tarPath if ( $tarPath !~ /^\.\// );
296     $tarPath =~ s{//+}{/}g;
297     $hdr->{name} = $tarPath;
298     my $zipmember; # Container to hold the file/directory to zip.
299
300     if ( $hdr->{type} == BPC_FTYPE_DIR ) {
301         #
302         # Directory: just write the header
303         #
304         $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
305         $zipmember = Archive::Zip::Member->newDirectoryNamed($hdr->{name});
306         $DirCnt++;
307     } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
308         #
309         # Regular file: write the header and file
310         #
311         $zipmember = BackupPC::Zip::FileMember->newFromFileNamed(
312                                             $fullName,
313                                             $hdr->{name},
314                                             $hdr->{size},
315                                             $compress
316                                     );
317         $FileCnt++;
318         $ByteCnt += $hdr->{size};
319     } elsif ( $hdr->{type} == BPC_FTYPE_HARDLINK ) {
320         #
321         # Hardlink file: not supported by Zip, so just make a copy
322         # of the pointed-to file.
323         #
324         # Start by reading the contents of the link.
325         #
326         my $f = BackupPC::FileZIO->open($fullName, 0, $compress);
327         if ( !defined($f) ) {
328             print(STDERR "Unable to open file $fullName\n");
329             $ErrorCnt++;
330             return;
331         }
332         my $data;
333         while ( $f->read(\$data, $BufSize) > 0 ) {
334             $hdr->{linkname} .= $data;
335         }
336         $f->close;
337         #
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.
341         #
342         archiveWrite($zipfh, $hdr->{linkname}, $hdr->{name});
343     } elsif ( $hdr->{type} == BPC_FTYPE_SYMLINK ) {
344         #
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
349         # option later.
350         #
351         $SpecialCnt++;
352     } elsif ( $hdr->{type} == BPC_FTYPE_CHARDEV
353            || $hdr->{type} == BPC_FTYPE_BLOCKDEV
354            || $hdr->{type} == BPC_FTYPE_FIFO ) {
355         #
356         # Special files can't be Zipped. 8(
357         #
358         $SpecialCnt++;
359     } else {
360         print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n");
361         $ErrorCnt++;
362     }
363     return if ( !$zipmember );
364     
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} );
371     
372     # Specify the compression level for this member
373     $zipmember->desiredCompressionLevel($compLevel) if ($compLevel =~ /[0-9]/);
374     
375     # Finally Zip the member
376     $zipfh->addMember($zipmember);
377 }
378
379 #
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.
383 #
384 sub MergeFind
385 {
386     my($zipfh, $dir, $dirF) = @_;
387
388     my(@Dir, $fLast);
389     if ( -d $dir ) {
390         ZipWriteFile($zipfh, $dir, $Mangle, $Compress);
391     } elsif ( -d $dirF ) {
392         ZipWriteFile($zipfh, $dirF, $MangleF, $CompressF);
393     }
394     if ( opendir(DIR, $dir) ) {
395         @Dir = readdir(DIR);
396         closedir(DIR);
397     }
398     if ( defined($NumF) && opendir(DIR, $dirF) ) {
399         if ( $Mangle == $MangleF ) {
400             @Dir = (@Dir, readdir(DIR));
401         } else {
402             foreach my $f ( readdir(DIR) ) {
403                 if ( $Mangle ) {
404                     push(@Dir, $bpc->fileNameMangle($f));
405                 } else {
406                     push(@Dir, $bpc->fileNameUnmangle($f));
407                 }
408             }
409         }
410     }
411     foreach my $f ( sort({$a cmp $b} @Dir) ) {
412         next if ( $f eq "." || $f eq ".."
413                || $f eq $fLast || ($Mangle && $f eq "attrib") );
414         $fLast = $f;
415         my($fF) = $f;
416         if ( $Mangle != $MangleF ) {
417             $fF = $Mangle ? $bpc->fileNameUnmangle($f)
418                           : $bpc->fileNameMangle($f);
419         }
420         if ( -e "$dir/$f" ) {
421             if ( -d "$dir/$f" ) {
422                 MergeFind($zipfh, "$dir/$f", "$dirF/$fF");
423             } else {
424                 ZipWriteFile($zipfh, "$dir/$f", $Mangle, $Compress);
425             }
426         } elsif ( -e "$dirF/$fF" ) {
427             if ( -d "$dirF/$fF" ) {
428                 MergeFind($zipfh, "$dir/$f", "$dirF/$fF");
429             } else {
430                 ZipWriteFile($zipfh, "$dirF/$fF", $MangleF, $CompressF);
431             }
432         } else {
433             print(STDERR "$0: Botch on $dir, $dirF, $f, $fF\n");
434             $ErrorCnt++;
435         }
436     }
437 }