Fixed some typos.
[BackupPC.git] / bin / BackupPC_zipCreate
1 #!/bin/perl
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 [options] files/directories...
10 #
11 #   Flags:
12 #     Required options:
13 #       -h host         host from which the zip archive is created
14 #       -n dumpNum      dump number from which the zip archive is created
15 #                       A negative number means relative to the end (eg -1
16 #                       means the most recent dump, -2 2nd most recent etc).
17 #       -s shareName    share name from which the zip archive is created
18 #
19 #     Other options:
20 #       -t              print summary totals
21 #       -r pathRemove   path prefix that will be replaced with pathAdd
22 #       -p pathAdd      new path prefix
23 #       -c level        compression level (default is 0, no compression)
24 #       -e charset      charset for encoding file names (default: value of
25 #                       $Conf{ClientCharset} when backup was done)
26 #
27 #     The -h, -n and -s options specify which dump is used to generate
28 #     the zip archive.  The -r and -p options can be used to relocate
29 #     the paths in the zip archive so extracted files can be placed
30 #     in a location different from their original location.
31 #
32 # AUTHOR
33 #   Guillaume Filion <gfk@users.sourceforge.net>
34 #   Based on Backup_tarCreate by Craig Barratt <cbarratt@users.sourceforge.net>
35 #
36 # COPYRIGHT
37 #   Copyright (C) 2002-2003  Craig Barratt and Guillaume Filion
38 #
39 #   This program is free software; you can redistribute it and/or modify
40 #   it under the terms of the GNU General Public License as published by
41 #   the Free Software Foundation; either version 2 of the License, or
42 #   (at your option) any later version.
43 #
44 #   This program is distributed in the hope that it will be useful,
45 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
46 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
47 #   GNU General Public License for more details.
48 #
49 #   You should have received a copy of the GNU General Public License
50 #   along with this program; if not, write to the Free Software
51 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
52 #
53 #========================================================================
54 #
55 # Version 3.0.0alpha, released 23 Jan 2006.
56 #
57 # See http://backuppc.sourceforge.net.
58 #
59 #========================================================================
60
61 use strict;
62 no  utf8;
63 use lib "/usr/local/BackupPC/lib";
64 use Archive::Zip qw(:ERROR_CODES);
65 use File::Path;
66 use Getopt::Std;
67 use Encode qw/from_to/;
68 use IO::Handle;
69 use BackupPC::Lib;
70 use BackupPC::Attrib qw(:all);
71 use BackupPC::FileZIO;
72 use BackupPC::Zip::FileMember;
73 use BackupPC::View;
74
75 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
76 my $TopDir = $bpc->TopDir();
77 my $BinDir = $bpc->BinDir();
78 my %Conf   = $bpc->Conf();
79
80 my %opts;
81
82 if ( !getopts("te:h:n:p:r:s:c:", \%opts) || @ARGV < 1 ) {
83     print STDERR <<EOF;
84 usage: $0 [options] files/directories...
85   Required options:
86      -h host         host from which the zip archive is created
87      -n dumpNum      dump number from which the tar archive is created
88                      A negative number means relative to the end (eg -1
89                      means the most recent dump, -2 2nd most recent etc).
90      -s shareName    share name from which the zip archive is created
91
92   Other options:
93      -t              print summary totals
94      -r pathRemove   path prefix that will be replaced with pathAdd
95      -p pathAdd      new path prefix
96      -c level        compression level (default is 0, no compression)
97      -e charset      charset for encoding file names (default: value of
98                      \$Conf{ClientCharset} when backup was done)
99 EOF
100     exit(1);
101 }
102
103 if ( $opts{h} !~ /^([\w\.\s-]+)$/
104         || $opts{h} =~ m{(^|/)\.\.(/|$)} ) {
105     print(STDERR "$0: bad host name '$opts{h}'\n");
106     exit(1);
107 }
108 my $Host = $opts{h};
109
110 if ( $opts{n} !~ /^(-?\d+)$/ ) {
111     print(STDERR "$0: bad dump number '$opts{n}'\n");
112     exit(1);
113 }
114 my $Num = $opts{n};
115
116 $opts{c} = 0 if ( $opts{c} eq "" );
117 if ( $opts{c} !~ /^(\d+)$/ ) {
118     print(STDERR "$0: invalid compression level '$opts{c}'. 0=none, 9=max\n");
119     exit(1);
120 }
121 my $compLevel = $opts{c};
122
123 my @Backups = $bpc->BackupInfoRead($Host);
124 my $FileCnt = 0;
125 my $ByteCnt = 0;
126 my $DirCnt = 0;
127 my $SpecialCnt = 0;
128 my $ErrorCnt = 0;
129
130 my $i;
131 $Num = $Backups[@Backups + $Num]{num} if ( -@Backups <= $Num && $Num < 0 );
132 for ( $i = 0 ; $i < @Backups ; $i++ ) {
133     last if ( $Backups[$i]{num} == $Num );
134 }
135 if ( $i >= @Backups ) {
136     print(STDERR "$0: bad backup number $Num for host $Host\n");
137     exit(1);
138 }
139
140 my $Charset = $Backups[$i]{charset};
141 $Charset = $opts{e} if ( $opts{e} ne "" );
142
143 my $PathRemove = $1 if ( $opts{r} =~ /(.+)/ );
144 my $PathAdd    = $1 if ( $opts{p} =~ /(.+)/ );
145 if ( $opts{s} !~ /^([\w\s.@\/$(){}[\]-]+)$/
146         || $opts{s} =~ m{(^|/)\.\.(/|$)} ) {
147     print(STDERR "$0: bad share name '$opts{s}'\n");
148     exit(1);
149 }
150 my $ShareName = $opts{s};
151
152 my $BufSize    = 1048576;     # 1MB or 2^20
153 my(%UidCache, %GidCache);
154 #my $fh = *STDOUT;
155 my $fh = new IO::Handle;      
156 $fh->fdopen(fileno(STDOUT),"w");
157 my $zipfh = Archive::Zip->new();
158
159 binmode(STDOUT);
160 foreach my $dir ( @ARGV ) {
161     archiveWrite($zipfh, $dir);
162 }
163
164 sub archiveWrite
165 {
166     my($zipfh, $dir, $zipPathOverride) = @_;
167
168     my $view = BackupPC::View->new($bpc, $Host, \@Backups);
169
170     if ( $dir =~ m{(^|/)\.\.(/|$)} || $dir !~ /^(.*)$/ ) {
171         print(STDERR "$0: bad directory '$dir'\n");
172         $ErrorCnt++;
173         return;
174     }
175     $dir = "/" if ( $dir eq "." );
176     $view->find($Num, $ShareName, $dir, 0, \&ZipWriteFile,
177                 $zipfh, $zipPathOverride);
178 }
179
180 # Create Zip file
181 print STDERR "Can't write Zip file\n"
182      unless $zipfh->writeToFileHandle($fh, 0) == Archive::Zip::AZ_OK;
183
184 #
185 # print out totals if requested
186 #
187 if ( $opts{t} ) {
188     print STDERR "Done: $FileCnt files, $ByteCnt bytes, $DirCnt dirs,",
189                  " $SpecialCnt specials ignored, $ErrorCnt errors\n";
190 }
191 exit(0);
192
193 ###########################################################################
194 # Subroutines
195 ###########################################################################
196
197 sub UidLookup
198 {
199     my($uid) = @_;
200
201     $UidCache{$uid} = (getpwuid($uid))[0] if ( !exists($UidCache{$uid}) );
202     return $UidCache{$uid};
203 }
204
205 sub GidLookup
206 {
207     my($gid) = @_;
208
209     $GidCache{$gid} = (getgrgid($gid))[0] if ( !exists($GidCache{$gid}) );
210     return $GidCache{$gid};
211 }
212
213 my $Attr;
214 my $AttrDir;
215
216 sub ZipWriteFile
217 {
218     my($hdr, $zipfh, $zipPathOverride) = @_;
219
220     my $tarPath = $hdr->{relPath};
221     $tarPath = $zipPathOverride if ( defined($zipPathOverride) );
222
223     if ( defined($PathRemove)
224             && substr($tarPath, 0, length($PathRemove)) eq $PathRemove ) {
225         substr($tarPath, 0, length($PathRemove)) = $PathAdd;
226     }
227     $tarPath = $1 if ( $tarPath =~ m{^\.?/+(.*)} );
228     $tarPath =~ s{//+}{/}g;
229     $hdr->{name} = $tarPath;
230     return if ( $tarPath eq "." || $tarPath eq "./" || $tarPath eq "" );
231
232     my $zipmember; # Container to hold the file/directory to zip.
233
234     if ( $hdr->{type} == BPC_FTYPE_DIR ) {
235         #
236         # Directory: just write the header
237         #
238         $hdr->{name} .= "/" if ( $hdr->{name} !~ m{/$} );
239         from_to($hdr->{name}, "utf8", $Charset) if ( $Charset ne "" );
240         $zipmember = Archive::Zip::Member->newDirectoryNamed($hdr->{name});
241         $DirCnt++;
242     } elsif ( $hdr->{type} == BPC_FTYPE_FILE ) {
243         #
244         # Regular file: write the header and file
245         #
246         from_to($hdr->{name}, "utf8", $Charset) if ( $Charset ne "" );
247         $zipmember = BackupPC::Zip::FileMember->newFromFileNamed(
248                                             $hdr->{fullPath},
249                                             $hdr->{name},
250                                             $hdr->{size},
251                                             $hdr->{compress}
252                                     );
253         $FileCnt++;
254         $ByteCnt += $hdr->{size};
255     } elsif ( $hdr->{type} == BPC_FTYPE_HARDLINK ) {
256         #
257         # Hardlink file: not supported by Zip, so just make a copy
258         # of the pointed-to file.
259         #
260         # Start by reading the contents of the link.
261         #
262         my $f = BackupPC::FileZIO->open($hdr->{fullPath}, 0, $hdr->{compress});
263         if ( !defined($f) ) {
264             print(STDERR "Unable to open file $hdr->{fullPath}\n");
265             $ErrorCnt++;
266             return;
267         }
268         my $data;
269         while ( $f->read(\$data, $BufSize) > 0 ) {
270             $hdr->{linkname} .= $data;
271         }
272         $f->close;
273         #
274         # Dump the original file.  Just call the top-level
275         # routine, so that we save the hassle of dealing with
276         # mangling, merging and attributes.
277         #
278         archiveWrite($zipfh, $hdr->{linkname}, $hdr->{name});
279     } elsif ( $hdr->{type} == BPC_FTYPE_SYMLINK ) {
280         #
281         # Symlinks can't be Zipped. 8(
282         # We could zip the pointed-to dir/file (just like hardlink), but we
283         # have to avoid the infinite-loop case of a symlink pointed to a
284         # directory above us.  Ignore for now.  Could be a comand-line
285         # option later.
286         #
287         $SpecialCnt++;
288     } elsif ( $hdr->{type} == BPC_FTYPE_CHARDEV
289            || $hdr->{type} == BPC_FTYPE_BLOCKDEV
290            || $hdr->{type} == BPC_FTYPE_FIFO ) {
291         #
292         # Special files can't be Zipped. 8(
293         #
294         $SpecialCnt++;
295     } else {
296         print(STDERR "Got unknown type $hdr->{type} for $hdr->{name}\n");
297         $ErrorCnt++;
298     }
299     return if ( !$zipmember );
300     
301     #
302     # Set the attributes and permissions.  The standard zip file
303     # header cannot handle dates prior to 1/1/1980, or 315561600
304     # unix seconds, so we round up the mtime.
305     #
306     my $mtime = $hdr->{mtime};
307     $mtime = 315561600 if ( $mtime < 315561600 );
308     $zipmember->setLastModFileDateTimeFromUnix($mtime);
309     $zipmember->unixFileAttributes($hdr->{mode});
310     # Zip files don't accept uid and gid, so we put them in the comment field.
311     $zipmember->fileComment("uid=".$hdr->{uid}." gid=".$hdr->{gid})
312             if ( $hdr->{uid} || $hdr->{gid} );
313     
314     # Specify the compression level for this member
315     $zipmember->desiredCompressionLevel($compLevel) if ($compLevel =~ /[0-9]/);
316     
317     # Finally Zip the member
318     $zipfh->addMember($zipmember);
319 }