Lots of changes:
[BackupPC.git] / lib / BackupPC / FileZIO.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::FileZIO package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::FileZIO class for doing
8 #   compressed or normal file I/O.
9 #
10 # AUTHOR
11 #   Craig Barratt  <cbarratt@users.sourceforge.net>
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2001-2003  Craig Barratt
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 2.1.0_CVS, released 8 Feb 2004.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 package BackupPC::FileZIO;
39
40 use strict;
41
42 use vars qw( $CompZlibOK );
43 use Carp;
44 use File::Path;
45 use File::Copy;
46
47 #
48 # For compressed files we have a to careful about running out of memory
49 # when we inflate a deflated file. For example, if a 500MB file of all
50 # zero-bytes is compressed, it will only occupy a few tens of kbytes. If
51 # we read the compressed file in decent-size chunks, a single inflate
52 # will try to allocate 500MB. Not a good idea.
53 #
54 # Instead, we compress the file in chunks of $CompMaxWrite. If a
55 # deflated chunk produces less than $CompMaxRead bytes, then we flush
56 # and continue. This adds a few bytes to the compressed output file, but
57 # only in extreme cases where the compression ratio is very close to
58 # 100%. The result is that, provided we read the compressed file in
59 # chunks of $CompMaxRead or less, the biggest inflated data will be
60 # $CompMaxWrite.
61 #
62 my $CompMaxRead  = 131072;          # 128K
63 my $CompMaxWrite = 6291456;         # 6MB
64
65 #
66 # We maintain a write buffer for small writes for both compressed and
67 # uncompressed files.  This is the size of the write buffer.
68 #
69 my $WriteBufSize = 65536;
70
71 BEGIN {
72     eval "use Compress::Zlib;";
73     if ( $@ ) {
74         #
75         # Compress::Zlib doesn't exist.  Define some dummy constant
76         # subs so that the code below doesn't barf.
77         #
78         eval {
79             sub Z_OK         { return 0; }
80             sub Z_STREAM_END { return 1; }
81         };
82         $CompZlibOK = 0;
83     } else {
84         $CompZlibOK = 1;
85     }
86 };
87
88 sub open
89 {
90     my($class, $fileName, $write, $compLevel) = @_;
91     local(*FH);
92     my($fh);
93
94     if ( ref(\$fileName) eq "GLOB" ) {
95         $fh = $fileName;
96     } else {
97         if ( $write ) {
98             open(FH, ">", $fileName) || return;
99         } else {
100             open(FH, "<", $fileName) || return;
101         }
102         binmode(FH);
103         $fh = *FH;
104     }
105     $compLevel  = 0 if ( !$CompZlibOK );
106     my $self = bless {
107         fh           => $fh,
108         name         => $fileName,
109         write        => $write,
110         writeZeroCnt => 0,
111         compress     => $compLevel,
112     }, $class;
113     if ( $compLevel ) {
114         if ( $write ) {
115             $self->{deflate} = $self->myDeflateInit;
116         } else {
117             $self->{inflate} = $self->myInflateInit;
118             $self->{inflateStart} = 1;
119         }
120     }
121     return $self;
122 }
123
124 sub compOk
125 {
126     return $CompZlibOK;
127 }
128
129 sub myDeflateInit
130 {
131     my $self = shift;
132
133     return deflateInit(
134                 -Bufsize => 65536,
135                 -Level   => $self->{compress},
136            );
137 }
138
139 sub myInflateInit
140 {
141     my $self = shift;
142
143     return inflateInit(
144                 -Bufsize => 65536,
145            );
146 }
147
148 sub read
149 {
150     my($self, $dataRef, $nRead) = @_;
151     my($n);
152
153     return if ( $self->{write} );
154     return sysread($self->{fh}, $$dataRef, $nRead) if ( !$self->{compress} );
155     while ( !$self->{eof} && $nRead > length($self->{dataOut}) ) {
156         if ( !length($self->{dataIn}) ) {
157             $n = sysread($self->{fh}, $self->{dataIn}, $CompMaxRead);
158             return $n if ( $n < 0 );
159             $self->{eof} = 1 if ( $n == 0 );
160         }
161         if ( $self->{inflateStart} && $self->{dataIn} ne "" ) {
162             my $chr = substr($self->{dataIn}, 0, 1);
163
164             $self->{inflateStart} = 0;
165             if ( $chr eq chr(0xd6) ) {
166                 #
167                 # Flag 0xd6 means this is a compressed file with
168                 # appended md4 block checksums for rsync.  Change
169                 # the first byte back to 0x78 and proceed.
170                 #
171                 ##print("Got 0xd6 block: normal\n");
172                 substr($self->{dataIn}, 0, 1) = chr(0x78);
173             } elsif ( $chr eq chr(0xb3) ) {
174                 #
175                 # Flag 0xb3 means this is the start of the rsync
176                 # block checksums, so consider this as EOF for
177                 # the compressed file.  Also seek the file so
178                 # it is positioned at the 0xb3.
179                 #
180                 seek($self->{fh}, -length($self->{dataIn}), 1);
181                 $self->{eof} = 1;
182                 $self->{dataIn} = "";
183                 ##print("Got 0xb3 block: considering eof\n");
184                 last;
185             } else {
186                 #
187                 # normal case: nothing to do
188                 #
189             }
190         }
191         my($data, $err) = $self->{inflate}->inflate($self->{dataIn});
192         $self->{dataOut} .= $data;
193         if ( $err == Z_STREAM_END ) {
194             #print("R");
195             $self->{inflate} = $self->myInflateInit;
196             $self->{inflateStart} = 1;
197         } elsif ( $err != Z_OK ) {
198             $$dataRef = "";
199             return -1;
200         }
201     }
202     if ( $nRead >= length($self->{dataOut}) ) {
203         $n = length($self->{dataOut});
204         $$dataRef = $self->{dataOut};
205         $self->{dataOut} = '';
206         return $n;
207     } else {
208         $$dataRef = substr($self->{dataOut}, 0, $nRead);
209         $self->{dataOut} = substr($self->{dataOut}, $nRead);
210         return $nRead;
211     }
212 }
213
214 #
215 # Provide a line-at-a-time interface.  This splits and buffers the
216 # lines, you cannot mix calls to read() and readLine().
217 #
218 sub readLine
219 {
220     my($self) = @_;
221     my $str;
222
223     while ( defined($self->{readLineBuf}) && !@{$self->{readLineBuf}} ) {
224         $self->read(\$str, $CompMaxRead);
225         if ( $str eq "" ) {
226             $str = $self->{readLineFrag};
227             $self->{readLineFrag} = "";
228             return $str;
229         }
230         @{$self->{readLineBuf}} = split(/\n/, $self->{readLineFrag} . $str);
231         if ( substr($str, -1, 1) ne "\n" ) {
232             $self->{readLineFrag} = pop(@{$self->{readLineBuf}});
233         } else {
234             $self->{readLineFrag} = "";
235         }
236     }
237     return shift(@{$self->{readLineBuf}}) . "\n";
238 }
239
240 sub rewind
241 {
242     my($self) = @_;
243
244     return if ( $self->{write} );
245     return sysseek($self->{fh}, 0, 0) if ( !$self->{compress} );
246     $self->{dataOut} = '';
247     $self->{dataIn}  = '';
248     $self->{eof}     = 0;
249     $self->{inflate} = $self->myInflateInit;
250     $self->{inflateStart} = 1;
251     return sysseek($self->{fh}, 0, 0);
252 }
253
254 sub writeBuffered
255 {
256     my $self = shift;
257     my($data, $force) = @_;
258
259     #
260     # Buffer small writes using a buffer size of up to $WriteBufSize.
261     #
262     if ( $force || length($self->{writeBuf}) + length($data) > $WriteBufSize ) {
263         if ( length($self->{writeBuf}) ) {
264             my $wrData = $self->{writeBuf} . $data;
265             return -1 if ( syswrite($self->{fh}, $wrData) != length($wrData) );
266             $self->{writeBuf} = undef;
267         } else {
268             return if ( length($data) == 0 );
269             return -1 if ( syswrite($self->{fh}, $data) != length($data) );
270         }
271     } else {
272         $self->{writeBuf} .= $data;
273     }
274     return 0;
275 }
276
277 sub write
278 {
279     my($self, $dataRef) = @_;
280     my $n = length($$dataRef);
281
282     return if ( !$self->{write} );
283     print(STDERR $$dataRef) if ( $self->{writeTeeStderr} );
284     return 0 if ( $n == 0 );
285     if ( !$self->{compress} ) {
286         #
287         # If smbclient gets a read error on the client (due to a file lock)
288         # it will write a dummy file of zeros.  We detect this so we can
289         # store the file efficiently as a sparse file.  writeZeroCnt is
290         # the number of consecutive 0 bytes at the start of the file.
291         #
292         my $skip = 0;
293         if ( $self->{writeZeroCnt} >= 0 && $$dataRef =~ /^(\0+)/s ) {
294             $skip = length($1);
295             $self->{writeZeroCnt} += $skip;
296             return $n if ( $skip == $n );
297         }
298         #
299         # We now have some non-zero bytes, so time to seek to the right
300         # place and turn off zero-byte detection.
301         #
302         if ( $self->{writeZeroCnt} > 0 ) {
303             sysseek($self->{fh}, $self->{writeZeroCnt}, 0);
304             $self->{writeZeroCnt} = -1;
305         } elsif ( $self->{writeZeroCnt} == 0 ) {
306             $self->{writeZeroCnt} = -1;
307         }
308         return -1 if ( $self->writeBuffered(substr($$dataRef, $skip)) < 0 );
309         return $n;
310     }
311     for ( my $i = 0 ; $i < $n ; $i += $CompMaxWrite ) {
312         my $dataIn  = substr($$dataRef, $i, $CompMaxWrite);
313         my $dataOut = $self->{deflate}->deflate($dataIn);
314         return -1 if ( $self->writeBuffered($dataOut) < 0 );
315         $self->{deflateIn}  += length($dataIn);
316         $self->{deflateOut} += length($dataOut);
317         if ( $self->{deflateIn} >= $CompMaxWrite ) {
318             if ( $self->{deflateOut} < $CompMaxRead ) {
319                 #
320                 # Compression is too high: to avoid huge memory requirements
321                 # on read we need to flush().
322                 #
323                 $dataOut = $self->{deflate}->flush();
324                 #print("F");
325                 $self->{deflate} = $self->myDeflateInit;
326                 return -1 if ( $self->writeBuffered($dataOut) < 0 );
327             }
328             $self->{deflateIn} = $self->{deflateOut} = 0;
329         }
330     }
331     return $n;
332 }
333
334 sub name
335 {
336     my($self) = @_;
337
338     return $self->{name};
339 }
340
341 sub writeTeeStderr
342 {
343     my($self, $param) = @_;
344
345     $self->{writeTeeStderr} = $param if ( defined($param) );
346     return $self->{writeTeeStderr};
347 }
348
349 sub close
350 {
351     my($self) = @_;
352     my $err = 0;
353
354     if ( $self->{write} && $self->{compress} ) {
355         my $data = $self->{deflate}->flush();
356         $err = 1 if ( $self->writeBuffered($data) < 0 );
357     } elsif ( $self->{write} && !$self->{compress} ) {
358         if ( $self->{writeZeroCnt} > 0 ) {
359             #
360             # We got a file of all zero bytes.  Write a single zero byte
361             # at the end of the file.  On most file systems this is an
362             # efficient way to store the file.
363             #
364             $err = 1 if ( sysseek($self->{fh}, $self->{writeZeroCnt} - 1, 0)
365                                             != $self->{writeZeroCnt} - 1
366                         || syswrite($self->{fh}, "\0") != 1 );
367         }
368     }
369     $self->writeBuffered(undef, 1);
370     close($self->{fh});
371     return $err ? -1 : 0;
372 }
373
374 #
375 # If $compress is >0, copy and compress $srcFile putting the output
376 # in $destFileZ.  Otherwise, copy the file to $destFileNoZ, or do
377 # nothing if $destFileNoZ is undef.  Finally, if rename is set, then
378 # the source file is removed.
379 #
380 sub compressCopy
381 {
382     my($class, $srcFile, $destFileZ, $destFileNoZ, $compress, $rmSrc) = @_;
383     my(@s) = stat($srcFile);
384     my $atime = $s[8] =~ /(.*)/ && $1;
385     my $mtime = $s[9] =~ /(.*)/ && $1;
386     if ( $CompZlibOK && $compress > 0 ) {
387         my $fh = BackupPC::FileZIO->open($destFileZ, 1, $compress);
388         my $data;
389         if ( defined($fh) && open(LOG, "<", $srcFile) ) {
390             binmode(LOG);
391             while ( sysread(LOG, $data, 65536) > 0 ) {
392                 $fh->write(\$data);
393             }
394             close(LOG);
395             $fh->close();
396             unlink($srcFile) if ( $rmSrc );
397             utime($atime, $mtime, $destFileZ);
398             return 1;
399         } else {
400             $fh->close() if ( defined($fh) );
401             return 0;
402         }
403     }
404     return 0 if ( !defined($destFileNoZ) );
405     if ( $rmSrc ) {
406         return rename($srcFile, $destFileNoZ);
407     } else {
408         return 0 if ( !copy($srcFile, $destFileNoZ) );
409         utime($atime, $mtime, $destFileNoZ);
410     }
411 }
412
413 1;