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