Initial add of specialized Config modules. Some parts are not fully implemented.
[BackupPC.git] / lib / BackupPC / Xfer / RsyncFileIO.pm
1 #============================================================= -*-perl-*-
2 #
3 # Rsync package
4 #
5 # DESCRIPTION
6 #
7 # AUTHOR
8 #   Craig Barratt  <cbarratt@users.sourceforge.net>
9 #
10 # COPYRIGHT
11 #   Copyright (C) 2002  Craig Barratt
12 #
13 #========================================================================
14 #
15 # Version 1.6.0_CVS, released 10 Dec 2002.
16 #
17 # See http://backuppc.sourceforge.net.
18 #
19 #========================================================================
20
21 package BackupPC::Xfer::RsyncFileIO;
22
23 use strict;
24 use File::Path;
25 use BackupPC::Attrib qw(:all);
26 use BackupPC::FileZIO;
27 use BackupPC::PoolWrite;
28 use Data::Dumper;
29
30 use constant S_IFMT       => 0170000;   # type of file
31 use constant S_IFDIR      => 0040000;   # directory
32 use constant S_IFCHR      => 0020000;   # character special
33 use constant S_IFBLK      => 0060000;   # block special
34 use constant S_IFREG      => 0100000;   # regular
35 use constant S_IFLNK      => 0120000;   # symbolic link
36 use constant S_IFSOCK     => 0140000;   # socket
37 use constant S_IFIFO      => 0010000;   # fifo
38
39 use vars qw( $RsyncLibOK );
40
41 BEGIN {
42     eval "use File::RsyncP::Digest";
43     if ( $@ ) {
44         #
45         # Rsync module doesn't exist.
46         #
47         $RsyncLibOK = 0;
48     } else {
49         $RsyncLibOK = 1;
50     }
51 };
52
53 sub new
54 {
55     my($class, $options) = @_;
56
57     return if ( !$RsyncLibOK );
58     $options ||= {};
59     my $fio = bless {
60         blockSize    => 700,
61         logLevel     => 0,
62         digest       => File::RsyncP::Digest->new,
63         checksumSeed => 0,
64         attrib       => {},
65         %$options,
66     }, $class;
67
68     $fio->{shareM}   = $fio->{bpc}->fileNameEltMangle($fio->{xfer}{shareName});
69     $fio->{outDir}   = "$fio->{xfer}{outDir}/new/";
70     $fio->{outDirSh} = "$fio->{outDir}/$fio->{shareM}/";
71     $fio->{view}     = BackupPC::View->new($fio->{bpc}, $fio->{host},
72                                          $fio->{backups});
73     $fio->{full} = $fio->{xfer}{type} eq "full" ? 1 : 0;
74     $fio->{newFilesFH} = $fio->{xfer}{newFilesFH};
75     $fio->{lastBkupNum} = $fio->{xfer}{lastBkupNum};
76     return $fio;
77 }
78
79 sub blockSize
80 {
81     my($fio, $value) = @_;
82
83     $fio->{blockSize} = $value if ( defined($value) );
84     return $fio->{blockSize};
85 }
86
87 #
88 # Setup rsync checksum computation for the given file.
89 #
90 sub csumStart
91 {
92     my($fio, $f) = @_;
93     my $attr = $fio->attribGet($f);
94
95     $fio->{file} = $f;
96     $fio->csumEnd if ( defined($fio->{fh}) );
97     return if ( $attr->{type} != BPC_FTYPE_FILE );
98     if ( !defined($fio->{fh} = BackupPC::FileZIO->open($attr->{fullPath},
99                                                        0,
100                                                        $attr->{compress})) ) {
101         $fio->log("Can't open $attr->{fullPath}");
102         return -1;
103     }
104 }
105
106 sub csumGet
107 {
108     my($fio, $num, $csumLen, $blockSize) = @_;
109     my($fileData);
110
111     $num     ||= 100;
112     $csumLen ||= 16;
113
114     return if ( !defined($fio->{fh}) );
115     if ( $fio->{fh}->read(\$fileData, $blockSize * $num) <= 0 ) {
116         return $fio->csumEnd;
117     }
118     #$fileData = substr($fileData, 0, $blockSize * $num - 2);
119     $fio->log(sprintf("%s: getting csum ($num,$csumLen,%d,0x%x)\n",
120                             $fio->{file}{name},
121                             length($fileData),
122                             $fio->{checksumSeed}))
123                 if ( $fio->{logLevel} >= 10 );
124     return $fio->{digest}->rsyncChecksum($fileData, $blockSize,
125                                          $csumLen, $fio->{checksumSeed});
126 }
127
128 sub csumEnd
129 {
130     my($fio) = @_;
131
132     return if ( !defined($fio->{fh}) );
133     $fio->{fh}->close();
134     delete($fio->{fh});
135 }
136
137 sub readStart
138 {
139     my($fio, $f) = @_;
140     my $attr = $fio->attribGet($f);
141
142     $fio->{file} = $f;
143     $fio->readEnd if ( defined($fio->{fh}) );
144     if ( !defined(my $fh = BackupPC::FileZIO->open($attr->{fullPath},
145                                            0,
146                                            $attr->{compress})) ) {
147         $fio->log("Can't open $attr->{fullPath}");
148         return;
149     }
150 }
151
152 sub read
153 {
154     my($fio, $num) = @_;
155     my($fileData);
156
157     $num ||= 32768;
158     return if ( !defined($fio->{fh}) );
159     if ( $fio->{fh}->read(\$fileData, $num) <= 0 ) {
160         return $fio->readEnd;
161     }
162     return \$fileData;
163 }
164
165 sub readEnd
166 {
167     my($fio) = @_;
168
169     return if ( !defined($fio->{fh}) );
170     $fio->{fh}->close;
171     delete($fio->{fh});
172 }
173
174 sub checksumSeed
175 {
176     my($fio, $checksumSeed) = @_;
177
178     $fio->{checksumSeed} = $checksumSeed;
179 }
180
181 sub dirs
182 {
183     my($fio, $localDir, $remoteDir) = @_;
184
185     $fio->{localDir}  = $localDir;
186     $fio->{remoteDir} = $remoteDir;
187 }
188
189 sub viewCacheDir
190 {
191     my($fio, $share, $dir) = @_;
192     my $shareM;
193
194     #$fio->log("viewCacheDir($share, $dir)");
195     if ( !defined($share) ) {
196         $share  = $fio->{xfer}{shareName};
197         $shareM = $fio->{shareM};
198     } else {
199         $shareM = $fio->{bpc}->fileNameEltMangle($share);
200     }
201     $shareM = "$shareM/$dir" if ( $dir ne "" );
202     return if ( defined($fio->{viewCache}{$shareM}) );
203     #
204     # purge old cache entries (ie: those that don't match the
205     # first part of $dir).
206     #
207     foreach my $d ( keys(%{$fio->{viewCache}}) ) {
208         delete($fio->{viewCache}{$d}) if ( $shareM !~ m{^\Q$d/} );
209     }
210     #
211     # fetch new directory attributes
212     #
213     $fio->{viewCache}{$shareM}
214                 = $fio->{view}->dirAttrib($fio->{lastBkupNum}, $share, $dir);
215 }
216
217 sub attribGet
218 {
219     my($fio, $f) = @_;
220     my($dir, $fname, $share, $shareM);
221
222     if ( $f->{name} =~ m{(.*)/(.*)} ) {
223         $shareM = $fio->{shareM};
224         $dir = $1;
225         $fname = $2;
226     } elsif ( $f->{name} ne "." ) {
227         $shareM = $fio->{shareM};
228         $dir = "";
229         $fname = $f->{name};
230     } else {
231         $share = "";
232         $shareM = "";
233         $dir = "";
234         $fname = $fio->{xfer}{shareName};
235     }
236     $fio->viewCacheDir($share, $dir);
237     $shareM .= "/$dir" if ( $dir ne "" );
238     return $fio->{viewCache}{$shareM}{$fname};
239 }
240
241 sub mode2type
242 {
243     my($fio, $mode) = @_;
244
245     if ( ($mode & S_IFMT) == S_IFREG ) {
246         return BPC_FTYPE_FILE;
247     } elsif ( ($mode & S_IFMT) == S_IFDIR ) {
248         return BPC_FTYPE_DIR;
249     } elsif ( ($mode & S_IFMT) == S_IFLNK ) {
250         return BPC_FTYPE_SYMLINK;
251     } elsif ( ($mode & S_IFMT) == S_IFCHR ) {
252         return BPC_FTYPE_CHARDEV;
253     } elsif ( ($mode & S_IFMT) == S_IFBLK ) {
254         return BPC_FTYPE_BLOCKDEV;
255     } elsif ( ($mode & S_IFMT) == S_IFIFO ) {
256         return BPC_FTYPE_FIFO;
257     } elsif ( ($mode & S_IFMT) == S_IFSOCK ) {
258         return BPC_FTYPE_SOCKET;
259     } else {
260         return BPC_FTYPE_UNKNOWN;
261     }
262 }
263
264 #
265 # Set the attributes for a file.  Returns non-zero on error.
266 #
267 sub attribSet
268 {
269     my($fio, $f, $placeHolder) = @_;
270     my($dir, $file);
271
272     if ( $f->{name} =~ m{(.*)/(.*)} ) {
273         $file = $2;
274         $dir  = "$fio->{shareM}/" . $1;
275     } elsif ( $f->{name} eq "." ) {
276         $dir  = "";
277         $file = $fio->{xfer}{shareName};
278     } else {
279         $dir  = $fio->{shareM};
280         $file = $f->{name};
281     }
282
283     if ( !defined($fio->{attribLastDir}) || $fio->{attribLastDir} ne $dir ) {
284         #
285         # Flush any directories that don't match the first part
286         # of the new directory
287         #
288         foreach my $d ( keys(%{$fio->{attrib}}) ) {
289             next if ( $d eq "" || "$dir/" =~ m{^\Q$d/} );
290             $fio->attribWrite($d);
291         }
292         $fio->{attribLastDir} = $dir;
293     }
294     if ( !exists($fio->{attrib}{$dir}) ) {
295         $fio->{attrib}{$dir} = BackupPC::Attrib->new({
296                                      compress => $fio->{xfer}{compress},
297                                 });
298         my $path = $fio->{outDir} . $dir;
299         if ( -f $fio->{attrib}{$dir}->fileName($path)
300                     && !$fio->{attrib}{$dir}->read($path) ) {
301             $fio->log(sprintf("Unable to read attribute file %s",
302                             $fio->{attrib}{$dir}->fileName($path)));
303         }
304     }
305     $fio->log("attribSet(dir=$dir, file=$file)") if ( $fio->{logLevel} >= 4 );
306
307     $fio->{attrib}{$dir}->set($file, {
308                             type  => $fio->mode2type($f->{mode}),
309                             mode  => $f->{mode},
310                             uid   => $f->{uid},
311                             gid   => $f->{gid},
312                             size  => $placeHolder ? -1 : $f->{size},
313                             mtime => $f->{mtime},
314                        });
315     return;
316 }
317
318 sub attribWrite
319 {
320     my($fio, $d) = @_;
321     my($poolWrite);
322
323     if ( !defined($d) ) {
324         #
325         # flush all entries (in reverse order)
326         #
327         foreach $d ( sort({$b cmp $a} keys(%{$fio->{attrib}})) ) {
328             $fio->attribWrite($d);
329         }
330         return;
331     }
332     return if ( !defined($fio->{attrib}{$d}) );
333     #
334     # Set deleted files in the attributes.  Any file in the view
335     # that doesn't have attributes is deleted.  All files sent by
336     # rsync have attributes temporarily set so we can do deletion
337     # detection.  We also prune these temporary attributes.
338     #
339     if ( $d ne "" ) {
340         my $dir;
341         my $share;
342
343         $dir = $1 if ( $d =~ m{.+?/(.*)} );
344         $fio->viewCacheDir(undef, $dir);
345         ##print("attribWrite $d,$dir\n");
346         ##$Data::Dumper::Indent = 1;
347         ##$fio->log("attribWrite $d,$dir");
348         ##$fio->log("viewCacheLogKeys = ", keys(%{$fio->{viewCache}}));
349         ##$fio->log("attribKeys = ", keys(%{$fio->{attrib}}));
350         ##print "viewCache = ", Dumper($fio->{attrib});
351         ##print "attrib = ", Dumper($fio->{attrib});
352         if ( defined($fio->{viewCache}{$d}) ) {
353             foreach my $f ( keys(%{$fio->{viewCache}{$d}}) ) {
354                 my $name = $f;
355                 $name = "$1/$name" if ( $d =~ m{.*?/(.*)} );
356                 if ( defined(my $a = $fio->{attrib}{$d}->get($f)) ) {
357                     #
358                     # delete temporary attributes (skipped files)
359                     #
360                     if ( $a->{size} < 0 ) {
361                         $fio->{attrib}{$d}->set($f, undef);
362                         $fio->logFileAction("skip", {
363                                     %{$fio->{viewCache}{$d}{$f}},
364                                     name => $name,
365                                 }) if ( $fio->{logLevel} >= 2 );
366                     }
367                 } else {
368                     ##print("Delete file $f\n");
369                     $fio->logFileAction("delete", {
370                                 %{$fio->{viewCache}{$d}{$f}},
371                                 name => $name,
372                             }) if ( $fio->{logLevel} >= 1 );
373                     $fio->{attrib}{$d}->set($f, {
374                                     type  => BPC_FTYPE_DELETED,
375                                     mode  => 0,
376                                     uid   => 0,
377                                     gid   => 0,
378                                     size  => 0,
379                                     mtime => 0,
380                                });
381                 }
382             }
383         }
384     }
385     if ( $fio->{attrib}{$d}->fileCount ) {
386         my $data = $fio->{attrib}{$d}->writeData;
387         my $dirM = $d;
388
389         $dirM = $1 . "/" . $fio->{bpc}->fileNameMangle($2)
390                         if ( $dirM =~ m{(.*?)/(.*)} );
391         my $fileName = $fio->{attrib}{$d}->fileName("$fio->{outDir}$dirM");
392         $fio->log("attribWrite(dir=$d) -> $fileName")
393                                 if ( $fio->{logLevel} >= 4 );
394         my $poolWrite = BackupPC::PoolWrite->new($fio->{bpc}, $fileName,
395                                      length($data), $fio->{xfer}{compress});
396         $poolWrite->write(\$data);
397         $fio->processClose($poolWrite, $fio->{attrib}{$d}->fileName($d),
398                            length($data), 0);
399     }
400     delete($fio->{attrib}{$d});
401 }
402
403 sub processClose
404 {
405     my($fio, $poolWrite, $fileName, $origSize, $doStats) = @_;
406     my($exists, $digest, $outSize, $errs) = $poolWrite->close;
407
408     $fileName =~ s{^/+}{};
409     $fio->log(@$errs) if ( defined($errs) && @$errs );
410     if ( $doStats ) {
411         $fio->{stats}{TotalFileCnt}++;
412         $fio->{stats}{TotalFileSize} += $origSize;
413     }
414     if ( $exists ) {
415         if ( $doStats ) {
416             $fio->{stats}{ExistFileCnt}++;
417             $fio->{stats}{ExistFileSize}     += $origSize;
418             $fio->{stats}{ExistFileCompSize} += $outSize;
419         }
420     } elsif ( $outSize > 0 ) {
421         my $fh = $fio->{newFilesFH};
422         print($fh "$digest $origSize $fileName\n") if ( defined($fh) );
423     }
424     return $exists && $origSize > 0;
425 }
426
427 sub statsGet
428 {
429     my($fio) = @_;
430
431     return $fio->{stats};
432 }
433
434 #
435 # Make a given directory.  Returns non-zero on error.
436 #
437 sub mkpath
438 {
439     my($fio, $f) = @_;
440     my $name = $1 if ( $f->{name} =~ /(.*)/ );
441     my $path;
442
443     if ( $name eq "." ) {
444         $path = $fio->{outDirSh};
445     } else {
446         $path = $fio->{outDirSh} . $fio->{bpc}->fileNameMangle($name);
447     }
448     $fio->logFileAction("create", $f) if ( $fio->{logLevel} >= 1 );
449     $fio->log("mkpath($path, 0777)") if ( $fio->{logLevel} >= 5 );
450     $path = $1 if ( $path =~ /(.*)/ );
451     File::Path::mkpath($path, 0, 0777) if ( !-d $path );
452     return $fio->attribSet($f) if ( -d $path );
453     $fio->log("Can't create directory $path");
454     return -1;
455 }
456
457 #
458 # Make a special file.  Returns non-zero on error.
459 #
460 sub mkspecial
461 {
462     my($fio, $f) = @_;
463     my $name = $1 if ( $f->{name} =~ /(.*)/ );
464     my $fNameM = $fio->{bpc}->fileNameMangle($name);
465     my $path = $fio->{outDirSh} . $fNameM;
466     my $attr = $fio->attribGet($f);
467     my $str = "";
468     my $type = $fio->mode2type($f->{mode});
469
470     $fio->log("mkspecial($path, $type, $f->{mode})")
471                     if ( $fio->{logLevel} >= 5 );
472     if ( $type == BPC_FTYPE_CHARDEV || $type == BPC_FTYPE_BLOCKDEV ) {
473         my($major, $minor, $fh, $fileData);
474
475         $major = $f->{rdev} >> 8;
476         $minor = $f->{rdev} & 0xff;
477         $str = "$major,$minor";
478     } elsif ( ($f->{mode} & S_IFMT) == S_IFLNK ) {
479         $str = $f->{link};
480     }
481     #
482     # Now see if the file is different, or this is a full, in which
483     # case we create the new file.
484     #
485     my($fh, $fileData);
486     if ( $fio->{full}
487             || !defined($attr)
488             || $attr->{type}  != $fio->mode2type($f->{mode})
489             || $attr->{mtime} != $f->{mtime}
490             || $attr->{size}  != $f->{size}
491             || $attr->{gid}   != $f->{gid}
492             || $attr->{mode}  != $f->{mode}
493             || !defined($fh = BackupPC::FileZIO->open($attr->{fullPath}, 0,
494                                                       $attr->{compress}))
495             || $fh->read(\$fileData, length($str) + 1) != length($str)
496             || $fileData ne $str ) {
497         $fh->close if ( defined($fh) );
498         $fh = BackupPC::PoolWrite->new($fio->{bpc}, $path,
499                                      length($str), $fio->{xfer}{compress});
500         $fh->write(\$str);
501         my $exist = $fio->processClose($fh, "$fio->{shareM}/$fNameM",
502                                        length($str), 1);
503         $fio->logFileAction($exist ? "pool" : "create", $f)
504                             if ( $fio->{logLevel} >= 1 );
505         return $fio->attribSet($f);
506     } else {
507         $fio->logFileAction("skip", $f) if ( $fio->{logLevel} >= 2 );
508     }
509     $fh->close if ( defined($fh) );
510 }
511
512 sub unlink
513 {
514     my($fio, $path) = @_;
515     
516     $fio->log("Unexpected call BackupPC::Xfer::RsyncFileIO->unlink($path)"); 
517 }
518
519 #
520 # Appends to list of log messages
521 #
522 sub log
523 {
524     my($fio, @msg) = @_;
525
526     $fio->{log} ||= [];
527     push(@{$fio->{log}}, @msg);
528 }
529
530 #
531 # Generate a log file message for a completed file
532 #
533 sub logFileAction
534 {
535     my($fio, $action, $f) = @_;
536     my $owner = "$f->{uid}/$f->{gid}";
537     my $type  = (("", "p", "c", "", "d", "", "b", "", "", "", "l", "", "s"))
538                     [($f->{mode} & S_IFMT) >> 12];
539
540     $fio->log(sprintf("  %-6s %1s%4o %9s %11.0f %s",
541                                 $action,
542                                 $type,
543                                 $f->{mode} & 07777,
544                                 $owner,
545                                 $f->{size},
546                                 $f->{name}));
547 }
548
549 #
550 # Returns a list of log messages
551 #
552 sub logMsg
553 {
554     my($fio) = @_;
555     my $log = $fio->{log} || [];
556
557     delete($fio->{log});
558     return @$log;
559 }
560
561 #
562 # Start receive of file deltas for a particular file.
563 #
564 sub fileDeltaRxStart
565 {
566     my($fio, $f, $cnt, $size, $remainder) = @_;
567
568     $fio->{rxFile}      = $f;           # remote file attributes
569     $fio->{rxLocalAttr} = $fio->attribGet($f); # local file attributes
570     $fio->{rxBlkCnt}    = $cnt;         # how many blocks we will receive
571     $fio->{rxBlkSize}   = $size;        # block size
572     $fio->{rxRemainder} = $remainder;   # size of the last block
573     $fio->{rxMatchBlk}  = 0;            # current start of match
574     $fio->{rxMatchNext} = 0;            # current next block of match
575     my $rxSize = ($cnt - 1) * $size + $remainder;
576     if ( $fio->{rxFile}{size} != $rxSize ) {
577         $fio->{rxMatchBlk} = undef;     # size different, so no file match
578         $fio->log("$fio->{rxFile}{name}: size doesn't match"
579                   . " ($fio->{rxFile}{size} vs $rxSize)")
580                         if ( $fio->{logLevel} >= 5 );
581     }
582     delete($fio->{rxInFd});
583     delete($fio->{rxOutFd});
584     delete($fio->{rxDigest});
585     delete($fio->{rxInData});
586 }
587
588 #
589 # Process the next file delta for the current file.  Returns 0 if ok,
590 # -1 if not.  Must be called with either a block number, $blk, or new data,
591 # $newData, (not both) defined.
592 #
593 sub fileDeltaRxNext
594 {
595     my($fio, $blk, $newData) = @_;
596
597     if ( defined($blk) ) {
598         if ( defined($fio->{rxMatchBlk}) && $fio->{rxMatchNext} == $blk ) {
599             #
600             # got the next block in order; just keep track.
601             #
602             $fio->{rxMatchNext}++;
603             return;
604         }
605     }
606     my $newDataLen = length($newData);
607     $fio->log("$fio->{rxFile}{name}: blk=$blk, newData=$newDataLen, rxMatchBlk=$fio->{rxMatchBlk}, rxMatchNext=$fio->{rxMatchNext}")
608                     if ( $fio->{logLevel} >= 8 );
609     if ( !defined($fio->{rxOutFd}) ) {
610         #
611         # maybe the file has no changes
612         #
613         if ( $fio->{rxMatchNext} == $fio->{rxBlkCnt}
614                 && !defined($blk) && !defined($newData) ) {
615             #$fio->log("$fio->{rxFile}{name}: file is unchanged");
616             #               if ( $fio->{logLevel} >= 8 );
617             return;
618         }
619
620         #
621         # need to open an output file where we will build the
622         # new version.
623         #
624         $fio->{rxFile}{name} =~ /(.*)/;
625         my $rxOutFileRel = "$fio->{shareM}/" . $fio->{bpc}->fileNameMangle($1);
626         my $rxOutFile    = $fio->{outDir} . $rxOutFileRel;
627         $fio->{rxOutFd}  = BackupPC::PoolWrite->new($fio->{bpc},
628                                            $rxOutFile, $fio->{rxFile}{size},
629                                            $fio->{xfer}{compress});
630         $fio->log("$fio->{rxFile}{name}: opening output file $rxOutFile")
631                         if ( $fio->{logLevel} >= 10 );
632         $fio->{rxOutFile} = $rxOutFile;
633         $fio->{rxOutFileRel} = $rxOutFileRel;
634         $fio->{rxDigest} = File::RsyncP::Digest->new;
635         $fio->{rxDigest}->add(pack("V", $fio->{checksumSeed}));
636     }
637     if ( defined($fio->{rxMatchBlk})
638                 && $fio->{rxMatchBlk} != $fio->{rxMatchNext} ) {
639         #
640         # Need to copy the sequence of blocks that matched.  If the file
641         # is compressed we need to make a copy of the uncompressed file,
642         # since the compressed file is not seekable.  Future optimizations
643         # would be to keep the uncompressed file in memory (eg, up to say
644         # 10MB), only create an uncompressed copy if the matching
645         # blocks were not monotonic, and to only do this if there are
646         # matching blocks (eg, maybe the entire file is new).
647         #
648         my $attr = $fio->{rxLocalAttr};
649         my $fh;
650         if ( !defined($fio->{rxInFd}) && !defined($fio->{rxInData}) ) {
651             if ( $attr->{compress} ) {
652                 if ( !defined($fh = BackupPC::FileZIO->open(
653                                                    $attr->{fullPath},
654                                                    0,
655                                                    $attr->{compress})) ) {
656                     $fio->log("Can't open $attr->{fullPath}");
657                     return -1;
658                 }
659                 if ( $attr->{size} < 10 * 1024 * 1024 ) {
660                     #
661                     # Cache the entire old file if it is less than 10MB
662                     #
663                     my $data;
664                     $fio->{rxInData} = "";
665                     while ( $fh->read(\$data, 10 * 1024 * 1024) > 0 ) {
666                         $fio->{rxInData} .= $data;
667                     }
668                 } else {
669                     #
670                     # Create and write a temporary output file
671                     #
672                     unlink("$fio->{outDirSh}RStmp")
673                                     if  ( -f "$fio->{outDirSh}RStmp" );
674                     if ( open(F, ">+$fio->{outDirSh}RStmp") ) {
675                         my $data;
676                         while ( $fh->read(\$data, 1024 * 1024) > 0 ) {
677                             if ( syswrite(F, $data) != length($data) ) {
678                                 $fio->log(sprintf("Can't write len=%d to %s",
679                                       length($data) , "$fio->{outDirSh}RStmp"));
680                                 $fh->close;
681                                 return -1;
682                             }
683                         }
684                         $fio->{rxInFd} = *F;
685                         $fio->{rxInName} = "$fio->{outDirSh}RStmp";
686                         seek($fio->{rxInFd}, 0, 0);
687                     } else {
688                         $fio->log("Unable to open $fio->{outDirSh}RStmp");
689                         $fh->close;
690                         return -1;
691                     }
692                 }
693                 $fh->close;
694             } else {
695                 if ( open(F, $attr->{fullPath}) ) {
696                     $fio->{rxInFd} = *F;
697                     $fio->{rxInName} = $attr->{fullPath};
698                 } else {
699                     $fio->log("Unable to open $attr->{fullPath}");
700                     return -1;
701                 }
702             }
703         }
704         my $lastBlk = $fio->{rxMatchNext} - 1;
705         $fio->log("$fio->{rxFile}{name}: writing blocks $fio->{rxMatchBlk}.."
706                   . "$lastBlk")
707                         if ( $fio->{logLevel} >= 10 );
708         my $seekPosn = $fio->{rxMatchBlk} * $fio->{rxBlkSize};
709         if ( defined($fio->{rxInFd}) && !seek($fio->{rxInFd}, $seekPosn, 0) ) {
710             $fio->log("Unable to seek $attr->{fullPath} to $seekPosn");
711             return -1;
712         }
713         my $cnt = $fio->{rxMatchNext} - $fio->{rxMatchBlk};
714         my($thisCnt, $len, $data);
715         for ( my $i = 0 ; $i < $cnt ; $i += $thisCnt ) {
716             $thisCnt = $cnt - $i;
717             $thisCnt = 512 if ( $thisCnt > 512 );
718             if ( $fio->{rxMatchBlk} + $i + $thisCnt == $fio->{rxBlkCnt} ) {
719                 $len = ($thisCnt - 1) * $fio->{rxBlkSize} + $fio->{rxRemainder};
720             } else {
721                 $len = $thisCnt * $fio->{rxBlkSize};
722             }
723             if ( defined($fio->{rxInData}) ) {
724                 $data = substr($fio->{rxInData}, $seekPosn, $len);
725             } else {
726                 if ( sysread($fio->{rxInFd}, $data, $len) != $len ) {
727                     $fio->log("Unable to read $len bytes from"
728                               . " $fio->{rxInName} "
729                               . "($i,$thisCnt,$fio->{rxBlkCnt})");
730                     return -1;
731                 }
732             }
733             $fio->{rxOutFd}->write(\$data);
734             $fio->{rxDigest}->add($data);
735         }
736         $fio->{rxMatchBlk} = undef;
737     }
738     if ( defined($blk) ) {
739         #
740         # Remember the new block number
741         #
742         $fio->{rxMatchBlk}  = $blk;
743         $fio->{rxMatchNext} = $blk + 1;
744     }
745     if ( defined($newData) ) {
746         #
747         # Write the new chunk
748         #
749         my $len = length($newData);
750         $fio->log("$fio->{rxFile}{name}: writing $len bytes new data")
751                         if ( $fio->{logLevel} >= 10 );
752         $fio->{rxOutFd}->write(\$newData);
753         $fio->{rxDigest}->add($newData);
754     }
755 }
756
757 #
758 # Finish up the current receive file.  Returns undef if ok, -1 if not.
759 # Returns 1 if the md4 digest doesn't match.
760 #
761 sub fileDeltaRxDone
762 {
763     my($fio, $md4) = @_;
764     my $name = $1 if ( $fio->{rxFile}{name} =~ /(.*)/ );
765
766     if ( !defined($fio->{rxDigest}) ) {
767         #
768         # File was exact match, but we still need to verify the
769         # MD4 checksum.  Therefore open and read the file.
770         #
771         $fio->{rxDigest} = File::RsyncP::Digest->new;
772         $fio->{rxDigest}->add(pack("V", $fio->{checksumSeed}));
773         my $attr = $fio->{rxLocalAttr};
774         if ( defined($attr) && defined(my $fh = BackupPC::FileZIO->open(
775                                                        $attr->{fullPath},
776                                                        0,
777                                                        $attr->{compress})) ) {
778             my $data;
779             while ( $fh->read(\$data, 4 * 65536) > 0 ) {
780                 $fio->{rxDigest}->add($data);
781             }
782             $fh->close;
783         } else {
784             # error
785         }
786         $fio->log("$name got exact match")
787                         if ( $fio->{logLevel} >= 5 );
788     }
789     close($fio->{rxInFd})  if ( defined($fio->{rxInFd}) );
790     unlink("$fio->{outDirSh}RStmp") if  ( -f "$fio->{outDirSh}RStmp" );
791     my $newDigest = $fio->{rxDigest}->rsyncDigest;
792     if ( $fio->{logLevel} >= 3 ) {
793         my $md4Str = unpack("H*", $md4);
794         my $newStr = unpack("H*", $newDigest);
795         $fio->log("$name got digests $md4Str vs $newStr")
796     }
797     if ( $md4 ne $newDigest ) {
798         $fio->log("$name md4 doesn't match")
799                     if ( $fio->{logLevel} >= 1 );
800         if ( defined($fio->{rxOutFd}) ) {
801             $fio->{rxOutFd}->close;
802             unlink($fio->{rxOutFile});
803         }
804         return 1;
805     }
806     #
807     # One special case is an empty file: if the file size is
808     # zero we need to open the output file to create it.
809     #
810     if ( $fio->{rxFile}{size} == 0 ) {
811         my $rxOutFileRel = "$fio->{shareM}/"
812                          . $fio->{bpc}->fileNameMangle($name);
813         my $rxOutFile    = $fio->{outDir} . $rxOutFileRel;
814         $fio->{rxOutFd}  = BackupPC::PoolWrite->new($fio->{bpc},
815                                            $rxOutFile, $fio->{rxFile}{size},
816                                            $fio->{xfer}{compress});
817     }
818     if ( !defined($fio->{rxOutFd}) ) {
819         #
820         # No output file, meaning original was an exact match.
821         #
822         $fio->log("$name: nothing to do")
823                         if ( $fio->{logLevel} >= 5 );
824         my $attr = $fio->{rxLocalAttr};
825         my $f = $fio->{rxFile};
826         $fio->logFileAction("same", $f) if ( $fio->{logLevel} >= 1 );
827         if ( $fio->{full}
828                 || $attr->{type}  != $f->{type}
829                 || $attr->{mtime} != $f->{mtime}
830                 || $attr->{size}  != $f->{size}
831                 || $attr->{gid}   != $f->{gid}
832                 || $attr->{mode}  != $f->{mode} ) {
833             #
834             # In the full case, or if the attributes are different,
835             # we need to make a link from the previous file and
836             # set the attributes.
837             #
838             my $rxOutFile = $fio->{outDirSh}
839                             . $fio->{bpc}->fileNameMangle($name);
840             if ( !link($attr->{fullPath}, $rxOutFile) ) {
841                 $fio->log("Unable to link $attr->{fullPath} to $rxOutFile");
842                 return -1;
843             }
844             #
845             # Cumulate the stats
846             #
847             $fio->{stats}{TotalFileCnt}++;
848             $fio->{stats}{TotalFileSize} += $fio->{rxFile}{size};
849             $fio->{stats}{ExistFileCnt}++;
850             $fio->{stats}{ExistFileSize} += $fio->{rxFile}{size};
851             $fio->{stats}{ExistFileCompSize} += -s $rxOutFile;
852             return;
853         }
854     }
855     if ( defined($fio->{rxOutFd}) ) {
856         my $exist = $fio->processClose($fio->{rxOutFd},
857                                        $fio->{rxOutFileRel},
858                                        $fio->{rxFile}{size}, 1);
859         $fio->logFileAction($exist ? "pool" : "create", $fio->{rxFile})
860                             if ( $fio->{logLevel} >= 1 );
861     }
862     delete($fio->{rxDigest});
863     delete($fio->{rxInData});
864     return;
865 }
866
867 sub fileListEltSend
868 {
869     my($fio, $name, $fList, $outputFunc) = @_;
870     my @s = stat($name);
871
872     (my $n = $name) =~ s/^\Q$fio->{localDir}/$fio->{remoteDir}/;
873     $fList->encode({
874             fname => $n,
875             dev   => $s[0],
876             inode => $s[1],
877             mode  => $s[2],
878             uid   => $s[4],
879             gid   => $s[5],
880             rdev  => $s[6],
881             mtime => $s[9],
882         });
883     &$outputFunc($fList->encodeData);
884 }
885
886 sub fileListSend
887 {
888     my($fio, $flist, $outputFunc) = @_;
889
890     $fio->log("fileListSend not implemented!!");
891     $fio->{view}->find($fio->{lastBkupNum}, $fio->{xfer}{shareName},
892                        $fio->{restoreFiles}, 1, \&fileListEltSend,
893                        $flist, $outputFunc);
894 }
895
896 sub finish
897 {
898     my($fio, $isChild) = @_;
899
900     #
901     # Flush the attributes if this is the child
902     #
903     $fio->attribWrite(undef)
904 }
905
906
907 sub is_tainted
908 {
909     return ! eval {
910         join('',@_), kill 0;
911         1;
912     };
913 }
914
915 1;