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