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