Minor tweaks for 3.1.0beta0 release.
[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-2007  Craig Barratt
12 #
13 #========================================================================
14 #
15 # Version 3.1.0beta0, released 3 Sep 2007.
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 Encode qw/from_to/;
26 use BackupPC::Attrib qw(:all);
27 use BackupPC::View;
28 use BackupPC::Xfer::RsyncDigest qw(:all);
29 use BackupPC::PoolWrite;
30
31 use constant S_HLINK_TARGET => 0400000;    # this file is hardlink target
32 use constant S_IFMT         => 0170000;    # type of file
33 use constant S_IFDIR        => 0040000;    # directory
34 use constant S_IFCHR        => 0020000;    # character special
35 use constant S_IFBLK        => 0060000;    # block special
36 use constant S_IFREG        => 0100000;    # regular
37 use constant S_IFLNK        => 0120000;    # symbolic link
38 use constant S_IFSOCK       => 0140000;    # socket
39 use constant S_IFIFO        => 0010000;    # fifo
40
41 use vars qw( $RsyncLibOK );
42
43 BEGIN {
44     eval "use File::RsyncP::Digest";
45     if ( $@ ) {
46         #
47         # Rsync module doesn't exist.
48         #
49         $RsyncLibOK = 0;
50     } else {
51         $RsyncLibOK = 1;
52     }
53 };
54
55 sub new
56 {
57     my($class, $options) = @_;
58
59     return if ( !$RsyncLibOK );
60     $options ||= {};
61     my $fio = bless {
62         blockSize    => 700,
63         logLevel     => 0,
64         digest       => File::RsyncP::Digest->new(),
65         checksumSeed => 0,
66         attrib       => {},
67         logHandler   => \&logHandler,
68         stats        => {
69             errorCnt          => 0,
70             TotalFileCnt      => 0,
71             TotalFileSize     => 0,
72             ExistFileCnt      => 0,
73             ExistFileSize     => 0,
74             ExistFileCompSize => 0,
75         },
76         %$options,
77     }, $class;
78
79     $fio->{digest}->protocol($fio->{protocol_version});
80     $fio->{shareM}   = $fio->{bpc}->fileNameEltMangle($fio->{share});
81     $fio->{outDir}   = "$fio->{xfer}{outDir}/new/";
82     $fio->{outDirSh} = "$fio->{outDir}/$fio->{shareM}/";
83     $fio->{view}     = BackupPC::View->new($fio->{bpc}, $fio->{client},
84                                          $fio->{backups});
85     $fio->{full}     = $fio->{xfer}{type} eq "full" ? 1 : 0;
86     $fio->{newFilesFH} = $fio->{xfer}{newFilesFH};
87     $fio->{partialNum} = undef if ( !$fio->{full} );
88     return $fio;
89 }
90
91 #
92 # We publish our version to File::RsyncP.  This is so File::RsyncP
93 # can provide backward compatibility to older FileIO code.
94 #
95 # Versions:
96 #
97 #   undef or 1:  protocol version 26, no hardlinks
98 #   2:           protocol version 28, supports hardlinks
99 #
100 sub version
101 {
102     return 2;
103 }
104
105 sub blockSize
106 {
107     my($fio, $value) = @_;
108
109     $fio->{blockSize} = $value if ( defined($value) );
110     return $fio->{blockSize};
111 }
112
113 sub protocol_version
114 {
115     my($fio, $value) = @_;
116
117     if ( defined($value) ) {
118         $fio->{protocol_version} = $value;
119         $fio->{digest}->protocol($fio->{protocol_version});
120     }
121     return $fio->{protocol_version};
122 }
123
124 sub preserve_hard_links
125 {
126     my($fio, $value) = @_;
127
128     $fio->{preserve_hard_links} = $value if ( defined($value) );
129     return $fio->{preserve_hard_links};
130 }
131
132 sub logHandlerSet
133 {
134     my($fio, $sub) = @_;
135     $fio->{logHandler} = $sub;
136     BackupPC::Xfer::RsyncDigest->logHandlerSet($sub);
137 }
138
139 #
140 # Setup rsync checksum computation for the given file.
141 #
142 sub csumStart
143 {
144     my($fio, $f, $needMD4, $defBlkSize, $phase) = @_;
145
146     $defBlkSize ||= $fio->{blockSize};
147     my $attr = $fio->attribGet($f, 1);
148     $fio->{file} = $f;
149     $fio->csumEnd if ( defined($fio->{csum}) );
150     return -1 if ( $attr->{type} != BPC_FTYPE_FILE );
151
152     #
153     # Rsync uses short checksums on the first phase.  If the whole-file
154     # checksum fails, then the file is repeated with full checksums.
155     # So on phase 2 we verify the checksums if they are cached.
156     #
157     if ( ($phase > 0 || rand(1) < $fio->{cacheCheckProb})
158             && $attr->{compress}
159             && $fio->{checksumSeed} == RSYNC_CSUMSEED_CACHE ) {
160         my($err, $d, $blkSize) = BackupPC::Xfer::RsyncDigest->digestStart(
161                                      $attr->{fullPath}, $attr->{size}, 0,
162                                      $defBlkSize, $fio->{checksumSeed},
163                                      0, $attr->{compress}, 0,
164                                      $fio->{protocol_version});
165         my($isCached, $isInvalid) = $d->isCached;
166         if ( $fio->{logLevel} >= 5 ) {
167             $fio->log("$attr->{fullPath} verify; cached = $isCached,"
168                     . " invalid = $isInvalid, phase = $phase");
169         }
170         if ( $isCached || $isInvalid ) {
171             my $ret = BackupPC::Xfer::RsyncDigest->digestAdd(
172                             $attr->{fullPath}, $blkSize,
173                             $fio->{checksumSeed}, 1,        # verify
174                             $fio->{protocol_version}
175                         );
176             if ( $ret != 1 ) {
177                 $fio->log("Bad cached digest for $attr->{fullPath} ($ret);"
178                         . " fixed");
179                 $fio->{stats}{errorCnt}++;
180             } else {
181                 $fio->log("$f->{name}: verified cached digest")
182                                     if ( $fio->{logLevel} >= 2 );
183             }
184         }
185         $d->digestEnd;
186     }
187     (my $err, $fio->{csum}, my $blkSize)
188          = BackupPC::Xfer::RsyncDigest->digestStart($attr->{fullPath},
189                          $attr->{size}, 0, $defBlkSize, $fio->{checksumSeed},
190                          $needMD4, $attr->{compress}, 1, $fio->{protocol_version});
191     if ( $err ) {
192         $fio->log("Can't get rsync digests from $attr->{fullPath}"
193                 . " (err=$err, name=$f->{name})");
194         $fio->{stats}{errorCnt}++;
195         return -1;
196     }
197     if ( $fio->{logLevel} >= 5 ) {
198         my($isCached, $invalid) = $fio->{csum}->isCached;
199         $fio->log("$attr->{fullPath} cache = $isCached,"
200                 . " invalid = $invalid, phase = $phase");
201     }
202     return $blkSize;
203 }
204
205 sub csumGet
206 {
207     my($fio, $num, $csumLen, $blockSize) = @_;
208     my($fileData);
209
210     $num     ||= 100;
211     $csumLen ||= 16;
212     return if ( !defined($fio->{csum}) );
213     return $fio->{csum}->digestGet($num, $csumLen);
214 }
215
216 sub csumEnd
217 {
218     my($fio) = @_;
219
220     return if ( !defined($fio->{csum}) );
221     return $fio->{csum}->digestEnd();
222 }
223
224 sub readStart
225 {
226     my($fio, $f) = @_;
227
228     my $attr = $fio->attribGet($f, 1);
229     $fio->{file} = $f;
230     $fio->readEnd if ( defined($fio->{fh}) );
231     if ( !defined($fio->{fh} = BackupPC::FileZIO->open($attr->{fullPath},
232                                            0,
233                                            $attr->{compress})) ) {
234         $fio->log("Can't open $attr->{fullPath} (name=$f->{name})");
235         $fio->{stats}{errorCnt}++;
236         return;
237     }
238     $fio->log("$f->{name}: opened for read") if ( $fio->{logLevel} >= 4 );
239 }
240
241 sub read
242 {
243     my($fio, $num) = @_;
244     my $fileData;
245
246     $num ||= 32768;
247     return if ( !defined($fio->{fh}) );
248     if ( $fio->{fh}->read(\$fileData, $num) <= 0 ) {
249         return $fio->readEnd;
250     }
251     $fio->log(sprintf("read returns %d bytes", length($fileData)))
252                                 if ( $fio->{logLevel} >= 8 );
253     return \$fileData;
254 }
255
256 sub readEnd
257 {
258     my($fio) = @_;
259
260     return if ( !defined($fio->{fh}) );
261     $fio->{fh}->close;
262     $fio->log("closing $fio->{file}{name})") if ( $fio->{logLevel} >= 8 );
263     delete($fio->{fh});
264     return;
265 }
266
267 sub checksumSeed
268 {
269     my($fio, $checksumSeed) = @_;
270
271     $fio->{checksumSeed} = $checksumSeed;
272     $fio->log("Checksum caching enabled (checksumSeed = $checksumSeed)")
273         if ( $fio->{logLevel} >= 1 && $checksumSeed == RSYNC_CSUMSEED_CACHE );
274     $fio->log("Checksum seed is $checksumSeed")
275         if ( $fio->{logLevel} >= 2 && $checksumSeed != RSYNC_CSUMSEED_CACHE );
276 }
277
278 sub dirs
279 {
280     my($fio, $localDir, $remoteDir) = @_;
281
282     $fio->{localDir}  = $localDir;
283     $fio->{remoteDir} = $remoteDir;
284 }
285
286 sub viewCacheDir
287 {
288     my($fio, $share, $dir) = @_;
289     my $shareM;
290
291     #$fio->log("viewCacheDir($share, $dir)");
292     if ( !defined($share) ) {
293         $share  = $fio->{share};
294         $shareM = $fio->{shareM};
295     } else {
296         $shareM = $fio->{bpc}->fileNameEltMangle($share);
297     }
298     $shareM = "$shareM/$dir" if ( $dir ne "" );
299     return if ( defined($fio->{viewCache}{$shareM}) );
300     #
301     # purge old cache entries (ie: those that don't match the
302     # first part of $dir).
303     #
304     foreach my $d ( keys(%{$fio->{viewCache}}) ) {
305         delete($fio->{viewCache}{$d}) if ( $shareM !~ m{^\Q$d/} );
306     }
307     #
308     # fetch new directory attributes
309     #
310     $fio->{viewCache}{$shareM}
311                 = $fio->{view}->dirAttrib($fio->{viewNum}, $share, $dir);
312     #
313     # also cache partial backup attrib data too
314     #
315     if ( defined($fio->{partialNum}) ) {
316         foreach my $d ( keys(%{$fio->{partialCache}}) ) {
317             delete($fio->{partialCache}{$d}) if ( $shareM !~ m{^\Q$d/} );
318         }
319         $fio->{partialCache}{$shareM}
320                     = $fio->{view}->dirAttrib($fio->{partialNum}, $share, $dir);
321     }
322 }
323
324 sub attribGetWhere
325 {
326     my($fio, $f, $noCache, $fname) = @_;
327     my($dir, $share, $shareM, $partial, $attr);
328
329     if ( !defined($fname) ) {
330         $fname = $f->{name};
331         $fname = "$fio->{xfer}{pathHdrSrc}/$fname"
332                        if ( defined($fio->{xfer}{pathHdrSrc}) );
333     }
334     $fname =~ s{//+}{/}g;
335     if ( $fname =~ m{(.*)/(.*)}s ) {
336         $shareM = $fio->{shareM};
337         $dir = $1;
338         $fname = $2;
339     } elsif ( $fname ne "." ) {
340         $shareM = $fio->{shareM};
341         $dir = "";
342     } else {
343         $share = "";
344         $shareM = "";
345         $dir = "";
346         $fname = $fio->{share};
347     }
348     $shareM .= "/$dir" if ( $dir ne "" );
349
350     if ( $noCache ) {
351         $share  = $fio->{share} if ( !defined($share) );
352         my $dirAttr = $fio->{view}->dirAttrib($fio->{viewNum}, $share, $dir);
353         $attr = $dirAttr->{$fname};
354     } else {
355         $fio->viewCacheDir($share, $dir);
356         if ( defined($attr = $fio->{viewCache}{$shareM}{$fname}) ) {
357             $partial = 0;
358         } elsif ( defined($attr = $fio->{partialCache}{$shareM}{$fname}) ) {
359             $partial = 1;
360         } else {
361             return;
362         }
363         if ( $attr->{mode} & S_HLINK_TARGET ) {
364             $attr->{hlink_self} = 1;
365             $attr->{mode} &= ~S_HLINK_TARGET;
366         }
367     }
368     return ($attr, $partial);
369 }
370
371 sub attribGet
372 {
373     my($fio, $f, $doHardLink) = @_;
374
375     my($attr) = $fio->attribGetWhere($f);
376     if ( $doHardLink && $attr->{type} == BPC_FTYPE_HARDLINK ) {
377         $fio->log("$attr->{fullPath}: opening for hardlink read"
378                 . " (name = $f->{name})") if ( $fio->{logLevel} >= 4 );
379         my $fh = BackupPC::FileZIO->open($attr->{fullPath}, 0,
380                                          $attr->{compress});
381         my $target;
382         if ( defined($fh) ) {
383             $fh->read(\$target,  65536);
384             $fh->close;
385             $target =~ s/^\.?\/+//;
386         } else {
387             $fio->log("$attr->{fullPath}: can't open for hardlink read");
388             $fio->{stats}{errorCnt}++;
389             $attr->{type} = BPC_FTYPE_FILE;
390             return $attr;
391         }
392         $target = "/$target" if ( $target !~ /^\// );
393         $fio->log("$attr->{fullPath}: redirecting to $target")
394                                     if ( $fio->{logLevel} >= 4 );
395         $target =~ s{^/+}{};
396         ($attr) = $fio->attribGetWhere($f, 1, $target);
397         $fio->log(" ... now got $attr->{fullPath}")
398                             if ( $fio->{logLevel} >= 4 );
399     }
400     return $attr;
401 }
402
403 sub mode2type
404 {
405     my($fio, $f) = @_;
406     my $mode = $f->{mode};
407
408     if ( ($mode & S_IFMT) == S_IFREG ) {
409         if ( defined($f->{hlink}) && !$f->{hlink_self} ) {
410             return BPC_FTYPE_HARDLINK;
411         } else {
412             return BPC_FTYPE_FILE;
413         }
414     } elsif ( ($mode & S_IFMT) == S_IFDIR ) {
415         return BPC_FTYPE_DIR;
416     } elsif ( ($mode & S_IFMT) == S_IFLNK ) {
417         return BPC_FTYPE_SYMLINK;
418     } elsif ( ($mode & S_IFMT) == S_IFCHR ) {
419         return BPC_FTYPE_CHARDEV;
420     } elsif ( ($mode & S_IFMT) == S_IFBLK ) {
421         return BPC_FTYPE_BLOCKDEV;
422     } elsif ( ($mode & S_IFMT) == S_IFIFO ) {
423         return BPC_FTYPE_FIFO;
424     } elsif ( ($mode & S_IFMT) == S_IFSOCK ) {
425         return BPC_FTYPE_SOCKET;
426     } else {
427         return BPC_FTYPE_UNKNOWN;
428     }
429 }
430
431 #
432 # Set the attributes for a file.  Returns non-zero on error.
433 #
434 sub attribSet
435 {
436     my($fio, $f, $placeHolder) = @_;
437     my($dir, $file);
438
439     return if ( $placeHolder && $fio->{phase} > 0 );
440
441     if ( $f->{name} =~ m{(.*)/(.*)}s ) {
442         $file = $2;
443         $dir  = "$fio->{shareM}/" . $1;
444     } elsif ( $f->{name} eq "." ) {
445         $dir  = "";
446         $file = $fio->{share};
447     } else {
448         $dir  = $fio->{shareM};
449         $file = $f->{name};
450     }
451
452     if ( $dir ne ""
453             && (!defined($fio->{attribLastDir}) || $fio->{attribLastDir} ne $dir) ) {
454         #
455         # Flush any directories that don't match the first part
456         # of the new directory.  Don't flush the top-level directory
457         # (ie: $dir eq "") since the "." might get sorted in the middle
458         # of other top-level directories or files.
459         #
460         foreach my $d ( keys(%{$fio->{attrib}}) ) {
461             next if ( $d eq "" || "$dir/" =~ m{^\Q$d/} );
462             $fio->attribWrite($d);
463         }
464         $fio->{attribLastDir} = $dir;
465     }
466     if ( !exists($fio->{attrib}{$dir}) ) {
467         $fio->log("attribSet: dir=$dir not found") if ( $fio->{logLevel} >= 4 );
468         $fio->{attrib}{$dir} = BackupPC::Attrib->new({
469                                      compress => $fio->{xfer}{compress},
470                                 });
471         my $dirM = $dir;
472         $dirM = $1 . "/" . $fio->{bpc}->fileNameMangle($2)
473                         if ( $dirM =~ m{(.*?)/(.*)}s );
474         my $path = $fio->{outDir} . $dirM;
475         if ( -f $fio->{attrib}{$dir}->fileName($path) ) {
476             if ( !$fio->{attrib}{$dir}->read($path) ) {
477                 $fio->log(sprintf("Unable to read attribute file %s",
478                             $fio->{attrib}{$dir}->fileName($path)));
479             } else {
480                 $fio->log(sprintf("attribRead file %s",
481                             $fio->{attrib}{$dir}->fileName($path)))
482                                      if ( $fio->{logLevel} >= 4 );
483             }
484         }
485     } else {
486         $fio->log("attribSet: dir=$dir exists") if ( $fio->{logLevel} >= 4 );
487     }
488     $fio->log("attribSet(dir=$dir, file=$file, size=$f->{size}, placeholder=$placeHolder)")
489                         if ( $fio->{logLevel} >= 4 );
490
491     my $mode = $f->{mode};
492
493     $mode |= S_HLINK_TARGET if ( $f->{hlink_self} );
494     $fio->{attrib}{$dir}->set($file, {
495                             type  => $fio->mode2type($f),
496                             mode  => $mode,
497                             uid   => $f->{uid},
498                             gid   => $f->{gid},
499                             size  => $placeHolder ? -1 : $f->{size},
500                             mtime => $f->{mtime},
501                        });
502     return;
503 }
504
505 sub attribWrite
506 {
507     my($fio, $d) = @_;
508     my($poolWrite);
509
510     if ( !defined($d) ) {
511         #
512         # flush all entries (in reverse order)
513         #
514         foreach $d ( sort({$b cmp $a} keys(%{$fio->{attrib}})) ) {
515             $fio->attribWrite($d);
516         }
517         return;
518     }
519     return if ( !defined($fio->{attrib}{$d}) );
520
521     #
522     # Set deleted files in the attributes.  Any file in the view
523     # that doesn't have attributes is flagged as deleted for
524     # incremental dumps.  All files sent by rsync have attributes
525     # temporarily set so we can do deletion detection.  We also
526     # prune these temporary attributes.
527     #
528     if ( $d ne "" ) {
529         my $dir;
530         my $share;
531
532         $dir = $1 if ( $d =~ m{.+?/(.*)}s );
533         $fio->viewCacheDir(undef, $dir);
534         ##print("attribWrite $d,$dir\n");
535         ##$Data::Dumper::Indent = 1;
536         ##$fio->log("attribWrite $d,$dir");
537         ##$fio->log("viewCacheLogKeys = ", keys(%{$fio->{viewCache}}));
538         ##$fio->log("attribKeys = ", keys(%{$fio->{attrib}}));
539         ##print "viewCache = ", Dumper($fio->{attrib});
540         ##print "attrib = ", Dumper($fio->{attrib});
541         if ( defined($fio->{viewCache}{$d}) ) {
542             foreach my $f ( keys(%{$fio->{viewCache}{$d}}) ) {
543                 my $name = $f;
544                 $name = "$1/$name" if ( $d =~ m{.*?/(.*)}s );
545                 if ( defined(my $a = $fio->{attrib}{$d}->get($f)) ) {
546                     #
547                     # delete temporary attributes (skipped files)
548                     #
549                     if ( $a->{size} < 0 ) {
550                         $fio->{attrib}{$d}->set($f, undef);
551                         $fio->logFileAction("skip", {
552                                     %{$fio->{viewCache}{$d}{$f}},
553                                     name => $name,
554                                 }) if ( $fio->{logLevel} >= 2
555                                       && $a->{type} == BPC_FTYPE_FILE );
556                     }
557                 } elsif ( $fio->{phase} == 0 && !$fio->{full} ) {
558                     ##print("Delete file $f\n");
559                     $fio->logFileAction("delete", {
560                                 %{$fio->{viewCache}{$d}{$f}},
561                                 name => $name,
562                             }) if ( $fio->{logLevel} >= 1 );
563                     $fio->{attrib}{$d}->set($f, {
564                                     type  => BPC_FTYPE_DELETED,
565                                     mode  => 0,
566                                     uid   => 0,
567                                     gid   => 0,
568                                     size  => 0,
569                                     mtime => 0,
570                                });
571                 }
572             }
573         }
574     }
575     if ( $fio->{attrib}{$d}->fileCount || $fio->{phase} > 0 ) {
576         my $data = $fio->{attrib}{$d}->writeData;
577         my $dirM = $d;
578
579         $dirM = $1 . "/" . $fio->{bpc}->fileNameMangle($2)
580                         if ( $dirM =~ m{(.*?)/(.*)}s );
581         my $fileName = $fio->{attrib}{$d}->fileName("$fio->{outDir}$dirM");
582         $fio->log("attribWrite(dir=$d) -> $fileName")
583                                 if ( $fio->{logLevel} >= 4 );
584         my $poolWrite = BackupPC::PoolWrite->new($fio->{bpc}, $fileName,
585                                      length($data), $fio->{xfer}{compress});
586         $poolWrite->write(\$data);
587         $fio->processClose($poolWrite, $fio->{attrib}{$d}->fileName($dirM),
588                            length($data), 0);
589     }
590     delete($fio->{attrib}{$d});
591 }
592
593 sub processClose
594 {
595     my($fio, $poolWrite, $fileName, $origSize, $doStats) = @_;
596     my($exists, $digest, $outSize, $errs) = $poolWrite->close;
597
598     $fileName =~ s{^/+}{};
599     $fio->log(@$errs) if ( defined($errs) && @$errs );
600     if ( $doStats ) {
601         $fio->{stats}{TotalFileCnt}++;
602         $fio->{stats}{TotalFileSize} += $origSize;
603     }
604     if ( $exists ) {
605         if ( $doStats ) {
606             $fio->{stats}{ExistFileCnt}++;
607             $fio->{stats}{ExistFileSize}     += $origSize;
608             $fio->{stats}{ExistFileCompSize} += $outSize;
609         }
610     } elsif ( $outSize > 0 ) {
611         my $fh = $fio->{newFilesFH};
612         print($fh "$digest $origSize $fileName\n") if ( defined($fh) );
613     }
614     return $exists && $origSize > 0;
615 }
616
617 sub statsGet
618 {
619     my($fio) = @_;
620
621     return $fio->{stats};
622 }
623
624 #
625 # Make a given directory.  Returns non-zero on error.
626 #
627 sub makePath
628 {
629     my($fio, $f) = @_;
630     my $name = $1 if ( $f->{name} =~ /(.*)/s );
631     my $path;
632
633     if ( $name eq "." ) {
634         $path = $fio->{outDirSh};
635     } else {
636         $path = $fio->{outDirSh} . $fio->{bpc}->fileNameMangle($name);
637     }
638     $fio->logFileAction("create", $f) if ( $fio->{logLevel} >= 1 );
639     $fio->log("makePath($path, 0777)") if ( $fio->{logLevel} >= 5 );
640     $path = $1 if ( $path =~ /(.*)/s );
641     File::Path::mkpath($path, 0, 0777) if ( !-d $path );
642     return $fio->attribSet($f) if ( -d $path );
643     $fio->log("Can't create directory $path");
644     $fio->{stats}{errorCnt}++;
645     return -1;
646 }
647
648 #
649 # Make a special file.  Returns non-zero on error.
650 #
651 sub makeSpecial
652 {
653     my($fio, $f) = @_;
654     my $name = $1 if ( $f->{name} =~ /(.*)/s );
655     my $fNameM = $fio->{bpc}->fileNameMangle($name);
656     my $path = $fio->{outDirSh} . $fNameM;
657     my $attr = $fio->attribGet($f);
658     my $str = "";
659     my $type = $fio->mode2type($f);
660
661     $fio->log("makeSpecial($path, $type, $f->{mode})")
662                     if ( $fio->{logLevel} >= 5 );
663     if ( $type == BPC_FTYPE_CHARDEV || $type == BPC_FTYPE_BLOCKDEV ) {
664         my($major, $minor, $fh, $fileData);
665
666         if ( defined($f->{rdev_major}) ) {
667             $major = $f->{rdev_major};
668             $minor = $f->{rdev_minor};
669         } else {
670             $major = $f->{rdev} >> 8;
671             $minor = $f->{rdev} & 0xff;
672         }
673         $str = "$major,$minor";
674     } elsif ( ($f->{mode} & S_IFMT) == S_IFLNK ) {
675         $str = $f->{link};
676     } elsif ( ($f->{mode} & S_IFMT) == S_IFREG ) {
677         #
678         # this is a hardlink
679         #
680         if ( !defined($f->{hlink}) ) {
681             $fio->log("Error: makeSpecial($path, $type, $f->{mode}) called"
682                     . " on a regular non-hardlink file");
683             return 1;
684         }
685         $str  = $f->{hlink};
686     }
687     #
688     # Now see if the file is different, or this is a full, in which
689     # case we create the new file.
690     #
691     my($fh, $fileData);
692     if ( $fio->{full}
693             || !defined($attr)
694             || $attr->{type}       != $type
695             || $attr->{mtime}      != $f->{mtime}
696             || $attr->{size}       != $f->{size}
697             || $attr->{uid}        != $f->{uid}
698             || $attr->{gid}        != $f->{gid}
699             || $attr->{mode}       != $f->{mode}
700             || $attr->{hlink_self} != $f->{hlink_self}
701             || !defined($fh = BackupPC::FileZIO->open($attr->{fullPath}, 0,
702                                                       $attr->{compress}))
703             || $fh->read(\$fileData, length($str) + 1) != length($str)
704             || $fileData ne $str ) {
705         $fh->close if ( defined($fh) );
706         $fh = BackupPC::PoolWrite->new($fio->{bpc}, $path,
707                                      length($str), $fio->{xfer}{compress});
708         $fh->write(\$str);
709         my $exist = $fio->processClose($fh, "$fio->{shareM}/$fNameM",
710                                        length($str), 1);
711         $fio->logFileAction($exist ? "pool" : "create", $f)
712                             if ( $fio->{logLevel} >= 1 );
713         return $fio->attribSet($f);
714     } else {
715         $fio->logFileAction("skip", $f) if ( $fio->{logLevel} >= 2 );
716     }
717     $fh->close if ( defined($fh) );
718 }
719
720 #
721 # Make a hardlink.  Returns non-zero on error.
722 # This actually gets called twice for each hardlink.
723 # Once as the file list is processed, and again at
724 # the end.  BackupPC does them as it goes (since it is
725 # just saving the hardlink info and not actually making
726 # hardlinks).
727 #
728 sub makeHardLink
729 {
730     my($fio, $f, $end) = @_;
731
732     return if ( $end );
733     return $fio->makeSpecial($f) if ( !$f->{hlink_self} );
734 }
735
736 sub unlink
737 {
738     my($fio, $path) = @_;
739     
740     $fio->log("Unexpected call BackupPC::Xfer::RsyncFileIO->unlink($path)"); 
741 }
742
743 #
744 # Default log handler
745 #
746 sub logHandler
747 {
748     my($str) = @_;
749
750     print(STDERR $str, "\n");
751 }
752
753 #
754 # Handle one or more log messages
755 #
756 sub log
757 {
758     my($fio, @logStr) = @_;
759
760     foreach my $str ( @logStr ) {
761         next if ( $str eq "" );
762         $fio->{logHandler}($str);
763     }
764 }
765
766 #
767 # Generate a log file message for a completed file
768 #
769 sub logFileAction
770 {
771     my($fio, $action, $f) = @_;
772     my $owner = "$f->{uid}/$f->{gid}";
773     my $type  = (("", "p", "c", "", "d", "", "b", "", "", "", "l", "", "s"))
774                     [($f->{mode} & S_IFMT) >> 12];
775     my $name = $f->{name};
776
777     if ( ($f->{mode} & S_IFMT) == S_IFLNK ) {
778         $name .= " -> $f->{link}";
779     } elsif ( ($f->{mode} & S_IFMT) == S_IFREG
780             && defined($f->{hlink}) && !$f->{hlink_self} ) {
781         $name .= " -> $f->{hlink}";
782     }
783     $name =~ s/\n/\\n/g;
784
785     $fio->log(sprintf("  %-6s %1s%4o %9s %11.0f %s",
786                                 $action,
787                                 $type,
788                                 $f->{mode} & 07777,
789                                 $owner,
790                                 $f->{size},
791                                 $name));
792 }
793
794 #
795 # If there is a partial and we are doing a full, we do an incremental
796 # against the partial and a full against the rest.  This subroutine
797 # is how we tell File::RsyncP which files to ignore attributes on
798 # (ie: against the partial dump we do consider the attributes, but
799 # otherwise we ignore attributes).
800 #
801 sub ignoreAttrOnFile
802 {
803     my($fio, $f) = @_;
804
805     return if ( !defined($fio->{partialNum}) );
806     my($attr, $isPartial) = $fio->attribGetWhere($f);
807     $fio->log("$f->{name}: just checking attributes from partial")
808                                 if ( $isPartial && $fio->{logLevel} >= 5 );
809     return !$isPartial;
810 }
811
812 #
813 # This is called by File::RsyncP when a file is skipped because the
814 # attributes match.
815 #
816 sub attrSkippedFile
817 {
818     my($fio, $f, $attr) = @_;
819
820     #
821     # Unless this is a partial, this is normal so ignore it.
822     #
823     return if ( !defined($fio->{partialNum}) );
824
825     $fio->log("$f->{name}: skipped in partial; adding link")
826                                     if ( $fio->{logLevel} >= 5 );
827     $fio->{rxLocalAttr} = $attr;
828     $fio->{rxFile} = $f;
829     $fio->{rxSize} = $attr->{size};
830     delete($fio->{rxInFd});
831     delete($fio->{rxOutFd});
832     delete($fio->{rxDigest});
833     delete($fio->{rxInData});
834     return $fio->fileDeltaRxDone();
835 }
836
837 #
838 # Start receive of file deltas for a particular file.
839 #
840 sub fileDeltaRxStart
841 {
842     my($fio, $f, $cnt, $size, $remainder) = @_;
843
844     $fio->{rxFile}      = $f;           # remote file attributes
845     $fio->{rxLocalAttr} = $fio->attribGet($f); # local file attributes
846     $fio->{rxBlkCnt}    = $cnt;         # how many blocks we will receive
847     $fio->{rxBlkSize}   = $size;        # block size
848     $fio->{rxRemainder} = $remainder;   # size of the last block
849     $fio->{rxMatchBlk}  = 0;            # current start of match
850     $fio->{rxMatchNext} = 0;            # current next block of match
851     $fio->{rxSize}      = 0;            # size of received file
852     my $rxSize = $cnt > 0 ? ($cnt - 1) * $size + $remainder : 0;
853     if ( $fio->{rxFile}{size} != $rxSize ) {
854         $fio->{rxMatchBlk} = undef;     # size different, so no file match
855         $fio->log("$fio->{rxFile}{name}: size doesn't match"
856                   . " ($fio->{rxFile}{size} vs $rxSize)")
857                         if ( $fio->{logLevel} >= 5 );
858     }
859     #
860     # If compression was off and now on, or on and now off, then
861     # don't do an exact match.
862     #
863     if ( defined($fio->{rxLocalAttr})
864             && !$fio->{rxLocalAttr}{compress} != !$fio->{xfer}{compress} ) {
865         $fio->{rxMatchBlk} = undef;     # compression changed, so no file match
866         $fio->log("$fio->{rxFile}{name}: compression changed, so no match"
867               . " ($fio->{rxLocalAttr}{compress} vs $fio->{xfer}{compress})")
868                     if ( $fio->{logLevel} >= 4 );
869     }
870     #
871     # If the local file is a hardlink then no match
872     #
873     if ( defined($fio->{rxLocalAttr})
874             && $fio->{rxLocalAttr}{type} == BPC_FTYPE_HARDLINK ) {
875         $fio->{rxMatchBlk} = undef;
876         $fio->log("$fio->{rxFile}{name}: no match on hardlinks")
877                                     if ( $fio->{logLevel} >= 4 );
878         my $fCopy;
879         # need to copy since hardlink attribGet overwrites the name
880         %{$fCopy} = %$f;
881         $fio->{rxHLinkAttr} = $fio->attribGet($fCopy, 1); # hardlink attributes
882     } else {
883         delete($fio->{rxHLinkAttr});
884     }
885     delete($fio->{rxInFd});
886     delete($fio->{rxOutFd});
887     delete($fio->{rxDigest});
888     delete($fio->{rxInData});
889 }
890
891 #
892 # Process the next file delta for the current file.  Returns 0 if ok,
893 # -1 if not.  Must be called with either a block number, $blk, or new data,
894 # $newData, (not both) defined.
895 #
896 sub fileDeltaRxNext
897 {
898     my($fio, $blk, $newData) = @_;
899
900     if ( defined($blk) ) {
901         if ( defined($fio->{rxMatchBlk}) && $fio->{rxMatchNext} == $blk ) {
902             #
903             # got the next block in order; just keep track.
904             #
905             $fio->{rxMatchNext}++;
906             return;
907         }
908     }
909     my $newDataLen = length($newData);
910     $fio->log("$fio->{rxFile}{name}: blk=$blk, newData=$newDataLen, rxMatchBlk=$fio->{rxMatchBlk}, rxMatchNext=$fio->{rxMatchNext}")
911                     if ( $fio->{logLevel} >= 8 );
912     if ( !defined($fio->{rxOutFd}) ) {
913         #
914         # maybe the file has no changes
915         #
916         if ( $fio->{rxMatchNext} == $fio->{rxBlkCnt}
917                 && !defined($blk) && !defined($newData) ) {
918             #$fio->log("$fio->{rxFile}{name}: file is unchanged");
919             #               if ( $fio->{logLevel} >= 8 );
920             return;
921         }
922
923         #
924         # need to open an output file where we will build the
925         # new version.
926         #
927         $fio->{rxFile}{name} =~ /(.*)/s;
928         my $rxOutFileRel = "$fio->{shareM}/" . $fio->{bpc}->fileNameMangle($1);
929         my $rxOutFile    = $fio->{outDir} . $rxOutFileRel;
930         $fio->{rxOutFd}  = BackupPC::PoolWrite->new($fio->{bpc},
931                                            $rxOutFile, $fio->{rxFile}{size},
932                                            $fio->{xfer}{compress});
933         $fio->log("$fio->{rxFile}{name}: opening output file $rxOutFile")
934                         if ( $fio->{logLevel} >= 9 );
935         $fio->{rxOutFile} = $rxOutFile;
936         $fio->{rxOutFileRel} = $rxOutFileRel;
937         $fio->{rxDigest} = File::RsyncP::Digest->new();
938         $fio->{rxDigest}->protocol($fio->{protocol_version});
939         $fio->{rxDigest}->add(pack("V", $fio->{checksumSeed}));
940     }
941     if ( defined($fio->{rxMatchBlk})
942                 && $fio->{rxMatchBlk} != $fio->{rxMatchNext} ) {
943         #
944         # Need to copy the sequence of blocks that matched.  If the file
945         # is compressed we need to make a copy of the uncompressed file,
946         # since the compressed file is not seekable.  Future optimizations
947         # could include only creating an uncompressed copy if the matching
948         # blocks were not monotonic, and to only do this if there are
949         # matching blocks (eg, maybe the entire file is new).
950         #
951         my $attr = $fio->{rxLocalAttr};
952         my $fh;
953         if ( !defined($fio->{rxInFd}) && !defined($fio->{rxInData}) ) {
954             my $inPath = $attr->{fullPath};
955             $inPath = $fio->{rxHLinkAttr}{fullPath}
956                             if ( defined($fio->{rxHLinkAttr}) );
957             if ( $attr->{compress} ) {
958                 if ( !defined($fh = BackupPC::FileZIO->open(
959                                                    $inPath,
960                                                    0,
961                                                    $attr->{compress})) ) {
962                     $fio->log("Can't open $inPath");
963                     $fio->{stats}{errorCnt}++;
964                     return -1;
965                 }
966                 if ( $attr->{size} < 16 * 1024 * 1024 ) {
967                     #
968                     # Cache the entire old file if it is less than 16MB
969                     #
970                     my $data;
971                     $fio->{rxInData} = "";
972                     while ( $fh->read(\$data, 16 * 1024 * 1024) > 0 ) {
973                         $fio->{rxInData} .= $data;
974                     }
975                     $fio->log("$attr->{fullPath}: cached all $attr->{size}"
976                             . " bytes")
977                                     if ( $fio->{logLevel} >= 9 );
978                 } else {
979                     #
980                     # Create and write a temporary output file
981                     #
982                     unlink("$fio->{outDirSh}RStmp")
983                                     if  ( -f "$fio->{outDirSh}RStmp" );
984                     if ( open(F, "+>", "$fio->{outDirSh}RStmp") ) {
985                         my $data;
986                         my $byteCnt = 0;
987                         binmode(F);
988                         while ( $fh->read(\$data, 1024 * 1024) > 0 ) {
989                             if ( syswrite(F, $data) != length($data) ) {
990                                 $fio->log(sprintf("Can't write len=%d to %s",
991                                       length($data) , "$fio->{outDirSh}RStmp"));
992                                 $fh->close;
993                                 $fio->{stats}{errorCnt}++;
994                                 return -1;
995                             }
996                             $byteCnt += length($data);
997                         }
998                         $fio->{rxInFd} = *F;
999                         $fio->{rxInName} = "$fio->{outDirSh}RStmp";
1000                         sysseek($fio->{rxInFd}, 0, 0);
1001                         $fio->log("$attr->{fullPath}: copied $byteCnt,"
1002                                 . "$attr->{size} bytes to $fio->{rxInName}")
1003                                         if ( $fio->{logLevel} >= 9 );
1004                     } else {
1005                         $fio->log("Unable to open $fio->{outDirSh}RStmp");
1006                         $fh->close;
1007                         $fio->{stats}{errorCnt}++;
1008                         return -1;
1009                     }
1010                 }
1011                 $fh->close;
1012             } else {
1013                 if ( open(F, "<", $inPath) ) {
1014                     binmode(F);
1015                     $fio->{rxInFd} = *F;
1016                     $fio->{rxInName} = $attr->{fullPath};
1017                 } else {
1018                     $fio->log("Unable to open $inPath");
1019                     $fio->{stats}{errorCnt}++;
1020                     return -1;
1021                 }
1022             }
1023         }
1024         my $lastBlk = $fio->{rxMatchNext} - 1;
1025         $fio->log("$fio->{rxFile}{name}: writing blocks $fio->{rxMatchBlk}.."
1026                   . "$lastBlk")
1027                         if ( $fio->{logLevel} >= 9 );
1028         my $seekPosn = $fio->{rxMatchBlk} * $fio->{rxBlkSize};
1029         if ( defined($fio->{rxInFd})
1030                         && !sysseek($fio->{rxInFd}, $seekPosn, 0) ) {
1031             $fio->log("Unable to seek $fio->{rxInName} to $seekPosn");
1032             $fio->{stats}{errorCnt}++;
1033             return -1;
1034         }
1035         my $cnt = $fio->{rxMatchNext} - $fio->{rxMatchBlk};
1036         my($thisCnt, $len, $data);
1037         for ( my $i = 0 ; $i < $cnt ; $i += $thisCnt ) {
1038             $thisCnt = $cnt - $i;
1039             $thisCnt = 512 if ( $thisCnt > 512 );
1040             if ( $fio->{rxMatchBlk} + $i + $thisCnt == $fio->{rxBlkCnt} ) {
1041                 $len = ($thisCnt - 1) * $fio->{rxBlkSize} + $fio->{rxRemainder};
1042             } else {
1043                 $len = $thisCnt * $fio->{rxBlkSize};
1044             }
1045             if ( defined($fio->{rxInData}) ) {
1046                 $data = substr($fio->{rxInData}, $seekPosn, $len);
1047                 $seekPosn += $len;
1048             } else {
1049                 my $got = sysread($fio->{rxInFd}, $data, $len);
1050                 if ( $got != $len ) {
1051                     my $inFileSize = -s $fio->{rxInName};
1052                     $fio->log("Unable to read $len bytes from $fio->{rxInName}"
1053                             . " got=$got, seekPosn=$seekPosn"
1054                             . " ($i,$thisCnt,$fio->{rxBlkCnt},$inFileSize"
1055                             . ",$attr->{size})");
1056                     $fio->{stats}{errorCnt}++;
1057                     return -1;
1058                 }
1059                 $seekPosn += $len;
1060             }
1061             $fio->{rxOutFd}->write(\$data);
1062             $fio->{rxDigest}->add($data);
1063             $fio->{rxSize} += length($data);
1064         }
1065         $fio->{rxMatchBlk} = undef;
1066     }
1067     if ( defined($blk) ) {
1068         #
1069         # Remember the new block number
1070         #
1071         $fio->{rxMatchBlk}  = $blk;
1072         $fio->{rxMatchNext} = $blk + 1;
1073     }
1074     if ( defined($newData) ) {
1075         #
1076         # Write the new chunk
1077         #
1078         my $len = length($newData);
1079         $fio->log("$fio->{rxFile}{name}: writing $len bytes new data")
1080                         if ( $fio->{logLevel} >= 9 );
1081         $fio->{rxOutFd}->write(\$newData);
1082         $fio->{rxDigest}->add($newData);
1083         $fio->{rxSize} += length($newData);
1084     }
1085 }
1086
1087 #
1088 # Finish up the current receive file.  Returns undef if ok, -1 if not.
1089 # Returns 1 if the md4 digest doesn't match.
1090 #
1091 sub fileDeltaRxDone
1092 {
1093     my($fio, $md4, $phase) = @_;
1094     my $name = $1 if ( $fio->{rxFile}{name} =~ /(.*)/s );
1095     my $ret;
1096
1097     close($fio->{rxInFd})  if ( defined($fio->{rxInFd}) );
1098     unlink("$fio->{outDirSh}RStmp") if  ( -f "$fio->{outDirSh}RStmp" );
1099     $fio->{phase} = $phase;
1100
1101     #
1102     # Check the final md4 digest
1103     #
1104     if ( defined($md4) ) {
1105         my $newDigest;
1106         if ( !defined($fio->{rxDigest}) ) {
1107             #
1108             # File was exact match, but we still need to verify the
1109             # MD4 checksum.  Compute the md4 digest (or fetch the
1110             # cached one.)
1111             #
1112             if ( defined(my $attr = $fio->{rxLocalAttr}) ) {
1113                 #
1114                 # block size doesn't matter: we're only going to
1115                 # fetch the md4 file digest, not the block digests.
1116                 #
1117                 my($err, $csum, $blkSize)
1118                          = BackupPC::Xfer::RsyncDigest->digestStart(
1119                                  $attr->{fullPath}, $attr->{size},
1120                                  0, 2048, $fio->{checksumSeed}, 1,
1121                                  $attr->{compress}, 1,
1122                                  $fio->{protocol_version});
1123                 if ( $err ) {
1124                     $fio->log("Can't open $attr->{fullPath} for MD4"
1125                             . " check (err=$err, $name)");
1126                     $fio->{stats}{errorCnt}++;
1127                 } else {
1128                     if ( $fio->{logLevel} >= 5 ) {
1129                         my($isCached, $invalid) = $csum->isCached;
1130                         $fio->log("MD4 $attr->{fullPath} cache = $isCached,"
1131                                 . " invalid = $invalid");
1132                     }
1133                     $newDigest = $csum->digestEnd;
1134                 }
1135                 $fio->{rxSize} = $attr->{size};
1136             } else {
1137                 #
1138                 # Empty file; just create an empty file digest
1139                 #
1140                 $fio->{rxDigest} = File::RsyncP::Digest->new();
1141                 $fio->{rxDigest}->protocol($fio->{protocol_version});
1142                 $fio->{rxDigest}->add(pack("V", $fio->{checksumSeed}));
1143                 $newDigest = $fio->{rxDigest}->digest;
1144             }
1145             $fio->log("$name got exact match") if ( $fio->{logLevel} >= 5 );
1146         } else {
1147             $newDigest = $fio->{rxDigest}->digest;
1148         }
1149         if ( $fio->{logLevel} >= 3 ) {
1150             my $md4Str = unpack("H*", $md4);
1151             my $newStr = unpack("H*", $newDigest);
1152             $fio->log("$name got digests $md4Str vs $newStr")
1153         }
1154         if ( $md4 ne $newDigest ) {
1155             if ( $phase > 0 ) {
1156                 $fio->log("$name: fatal error: md4 doesn't match on retry;"
1157                         . " file removed");
1158             } else {
1159                 $fio->log("$name: md4 doesn't match: will retry in phase 1;"
1160                         . " file removed");
1161             }
1162             $fio->{stats}{errorCnt}++;
1163             if ( defined($fio->{rxOutFd}) ) {
1164                 $fio->{rxOutFd}->close;
1165                 unlink($fio->{rxOutFile});
1166             }
1167             delete($fio->{rxFile});
1168             delete($fio->{rxOutFile});
1169             return 1;
1170         }
1171     }
1172
1173     #
1174     # One special case is an empty file: if the file size is
1175     # zero we need to open the output file to create it.
1176     #
1177     if ( $fio->{rxSize} == 0 ) {
1178         my $rxOutFileRel = "$fio->{shareM}/"
1179                          . $fio->{bpc}->fileNameMangle($name);
1180         my $rxOutFile    = $fio->{outDir} . $rxOutFileRel;
1181         $fio->{rxOutFd}  = BackupPC::PoolWrite->new($fio->{bpc},
1182                                            $rxOutFile, $fio->{rxSize},
1183                                            $fio->{xfer}{compress});
1184     }
1185     if ( !defined($fio->{rxOutFd}) ) {
1186         #
1187         # No output file, meaning original was an exact match.
1188         #
1189         $fio->log("$name: nothing to do")
1190                         if ( $fio->{logLevel} >= 5 );
1191         my $attr = $fio->{rxLocalAttr};
1192         my $f = $fio->{rxFile};
1193         $fio->logFileAction("same", $f) if ( $fio->{logLevel} >= 1 );
1194         if ( $fio->{full}
1195                 || $attr->{type}       != $f->{type}
1196                 || $attr->{mtime}      != $f->{mtime}
1197                 || $attr->{size}       != $f->{size}
1198                 || $attr->{uid}        != $f->{uid}
1199                 || $attr->{gid}        != $f->{gid}
1200                 || $attr->{mode}       != $f->{mode}
1201                 || $attr->{hlink_self} != $f->{hlink_self} ) {
1202             #
1203             # In the full case, or if the attributes are different,
1204             # we need to make a link from the previous file and
1205             # set the attributes.
1206             #
1207             my $rxOutFile = $fio->{outDirSh}
1208                             . $fio->{bpc}->fileNameMangle($name);
1209             my($exists, $digest, $origSize, $outSize, $errs)
1210                                 = BackupPC::PoolWrite::LinkOrCopy(
1211                                       $fio->{bpc},
1212                                       $attr->{fullPath},
1213                                       $attr->{compress},
1214                                       $rxOutFile,
1215                                       $fio->{xfer}{compress});
1216             #
1217             # Cumulate the stats
1218             #
1219             $fio->{stats}{TotalFileCnt}++;
1220             $fio->{stats}{TotalFileSize} += $fio->{rxSize};
1221             $fio->{stats}{ExistFileCnt}++;
1222             $fio->{stats}{ExistFileSize} += $fio->{rxSize};
1223             $fio->{stats}{ExistFileCompSize} += -s $rxOutFile;
1224             $fio->{rxFile}{size} = $fio->{rxSize};
1225             $ret = $fio->attribSet($fio->{rxFile});
1226             $fio->log(@$errs) if ( defined($errs) && @$errs );
1227
1228             if ( !$exists && $outSize > 0 ) {
1229                 #
1230                 # the hard link failed, most likely because the target
1231                 # file has too many links.  We have copied the file
1232                 # instead, so add this to the new file list.
1233                 #
1234                 my $rxOutFileRel = "$fio->{shareM}/"
1235                                  . $fio->{bpc}->fileNameMangle($name);
1236                 $rxOutFileRel =~ s{^/+}{};
1237                 my $fh = $fio->{newFilesFH};
1238                 print($fh "$digest $origSize $rxOutFileRel\n")
1239                                                 if ( defined($fh) );
1240             }
1241         }
1242     } else {
1243         my $exist = $fio->processClose($fio->{rxOutFd},
1244                                        $fio->{rxOutFileRel},
1245                                        $fio->{rxSize}, 1);
1246         $fio->logFileAction($exist ? "pool" : "create", $fio->{rxFile})
1247                             if ( $fio->{logLevel} >= 1 );
1248         $fio->{rxFile}{size} = $fio->{rxSize};
1249         $ret = $fio->attribSet($fio->{rxFile});
1250     }
1251     delete($fio->{rxDigest});
1252     delete($fio->{rxInData});
1253     delete($fio->{rxFile});
1254     delete($fio->{rxOutFile});
1255     return $ret;
1256 }
1257
1258 #
1259 # Callback function for BackupPC::View->find.  Note the order of the
1260 # first two arguments.
1261 #
1262 sub fileListEltSend
1263 {
1264     my($a, $fio, $fList, $outputFunc) = @_;
1265     my $name = $a->{relPath};
1266     my $n = $name;
1267     my $type = $a->{type};
1268     my $extraAttribs = {};
1269
1270     if ( $a->{mode} & S_HLINK_TARGET ) {
1271         $a->{hlink_self} = 1;
1272         $a->{mode} &= ~S_HLINK_TARGET;
1273     }
1274     $n =~ s/^\Q$fio->{xfer}{pathHdrSrc}//;
1275     $fio->log("Sending $name (remote=$n) type = $type") if ( $fio->{logLevel} >= 1 );
1276     if ( $type == BPC_FTYPE_CHARDEV
1277             || $type == BPC_FTYPE_BLOCKDEV
1278             || $type == BPC_FTYPE_SYMLINK ) {
1279         my $fh = BackupPC::FileZIO->open($a->{fullPath}, 0, $a->{compress});
1280         my($str, $rdSize);
1281         if ( defined($fh) ) {
1282             $rdSize = $fh->read(\$str, $a->{size} + 1024);
1283             if ( $type == BPC_FTYPE_SYMLINK ) {
1284                 #
1285                 # Reconstruct symbolic link
1286                 #
1287                 $extraAttribs = { link => $str };
1288                 if ( $rdSize != $a->{size} ) {
1289                     # ERROR
1290                     $fio->log("$name: can't read exactly $a->{size} bytes");
1291                     $fio->{stats}{errorCnt}++;
1292                 }
1293             } elsif ( $str =~ /(\d*),(\d*)/ ) {
1294                 #
1295                 # Reconstruct char or block special major/minor device num
1296                 #
1297                 # Note: char/block devices have $a->{size} = 0, so we
1298                 # can't do an error check on $rdSize.
1299                 #
1300                 $extraAttribs = {
1301                     rdev       => $1 * 256 + $2,
1302                     rdev_major => $1,
1303                     rdev_minor => $2,
1304                 };
1305             } else {
1306                 $fio->log("$name: unexpected special file contents $str");
1307                 $fio->{stats}{errorCnt}++;
1308             }
1309             $fh->close;
1310         } else {
1311             # ERROR
1312             $fio->log("$name: can't open");
1313             $fio->{stats}{errorCnt}++;
1314         }
1315     } elsif ( $fio->{preserve_hard_links}
1316             && ($type == BPC_FTYPE_HARDLINK || $type == BPC_FTYPE_FILE)
1317             && ($type == BPC_FTYPE_HARDLINK
1318                     || $fio->{protocol_version} < 27
1319                     || $a->{hlink_self}) ) {
1320         #
1321         # Fill in fake inode information so that the remote rsync
1322         # can correctly create hardlinks.
1323         #
1324         $name =~ s/^\.?\/+//;
1325         my($target, $inode);
1326
1327         if ( $type == BPC_FTYPE_HARDLINK ) {
1328             my $fh = BackupPC::FileZIO->open($a->{fullPath}, 0,
1329                                              $a->{compress});
1330             if ( defined($fh) ) {
1331                 $fh->read(\$target,  65536);
1332                 $fh->close;
1333                 $target =~ s/^\.?\/+//;
1334                 if ( defined($fio->{hlinkFile2Num}{$target}) ) {
1335                     $inode = $fio->{hlinkFile2Num}{$target};
1336                 } else {
1337                     $inode = $fio->{fileListCnt};
1338                     $fio->{hlinkFile2Num}{$target} = $inode;
1339                 }
1340             } else {
1341                 $fio->log("$a->{fullPath}: can't open for hardlink");
1342                 $fio->{stats}{errorCnt}++;
1343             }
1344         } elsif ( $a->{hlink_self} ) {
1345             if ( defined($fio->{hlinkFile2Num}{$name}) ) {
1346                 $inode = $fio->{hlinkFile2Num}{$name};
1347             } else {
1348                 $inode = $fio->{fileListCnt};
1349                 $fio->{hlinkFile2Num}{$name} = $inode;
1350             }
1351         }
1352         $inode = $fio->{fileListCnt} if ( !defined($inode) );
1353         $fio->log("$name: setting inode to $inode");
1354         $extraAttribs = {
1355             %$extraAttribs,
1356             dev   => 0,
1357             inode => $inode,
1358         };
1359     }
1360     my $f = {
1361         name  => $n,
1362         mode  => $a->{mode} & ~S_HLINK_TARGET,
1363         uid   => $a->{uid},
1364         gid   => $a->{gid},
1365         mtime => $a->{mtime},
1366         size  => $a->{size},
1367         %$extraAttribs,
1368     };
1369     my $logName = $f->{name};
1370     from_to($f->{name}, "utf8", $fio->{clientCharset})
1371                             if ( $fio->{clientCharset} ne "" );
1372     $fList->encode($f);
1373
1374     $logName = "$fio->{xfer}{pathHdrDest}/$logName";
1375     $logName =~ s{//+}{/}g;
1376     $f->{name} = $logName;
1377     $fio->logFileAction("restore", $f) if ( $fio->{logLevel} >= 1 );
1378
1379     &$outputFunc($fList->encodeData);
1380     #
1381     # Cumulate stats
1382     #
1383     $fio->{fileListCnt}++;
1384     if ( $type != BPC_FTYPE_DIR ) {
1385         $fio->{stats}{TotalFileCnt}++;
1386         $fio->{stats}{TotalFileSize} += $a->{size};
1387     }
1388 }
1389
1390 sub fileListSend
1391 {
1392     my($fio, $flist, $outputFunc) = @_;
1393
1394     #
1395     # Populate the file list with the files requested by the user.
1396     # Since some might be directories so we call BackupPC::View::find.
1397     #
1398     $fio->log("fileListSend: sending file list: "
1399              . join(" ", @{$fio->{fileList}})) if ( $fio->{logLevel} >= 4 );
1400     $fio->{fileListCnt} = 0;
1401     $fio->{hlinkFile2Num} = {};
1402     foreach my $name ( @{$fio->{fileList}} ) {
1403         $fio->{view}->find($fio->{xfer}{bkupSrcNum},
1404                            $fio->{xfer}{bkupSrcShare},
1405                            $name, 1,
1406                            \&fileListEltSend, $fio, $flist, $outputFunc);
1407     }
1408 }
1409
1410 sub finish
1411 {
1412     my($fio, $isChild) = @_;
1413
1414     #
1415     # If we are aborting early, remove the last file since
1416     # it was not complete
1417     #
1418     if ( $isChild && defined($fio->{rxFile}) ) {
1419         unlink("$fio->{outDirSh}RStmp") if  ( -f "$fio->{outDirSh}RStmp" );
1420         if ( defined($fio->{rxFile}) ) {
1421             unlink($fio->{rxOutFile});
1422             $fio->log("finish: removing in-process file $fio->{rxFile}{name}");
1423         }
1424     }
1425
1426     #
1427     # Flush the attributes if this is the child
1428     #
1429     $fio->attribWrite(undef) if ( $isChild );
1430 }
1431
1432 #sub is_tainted
1433 #{
1434 #    return ! eval {
1435 #        join('',@_), kill 0;
1436 #        1;
1437 #    };
1438 #}
1439
1440 1;