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