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