b028b65682fcda43ee4db2ae8aee96f542748749
[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.0beta1, released 9 Apr 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::Xfer::RsyncDigest;
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 }
101
102 #
103 # Setup rsync checksum computation for the given file.
104 #
105 sub csumStart
106 {
107     my($fio, $f, $needMD4, $defBlkSize) = @_;
108
109     $defBlkSize ||= $fio->{blockSize};
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::Xfer::RsyncDigest->digestStart($attr->{fullPath},
116                          $attr->{size}, 0, $defBlkSize, $fio->{checksumSeed},
117                          $needMD4, $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     my $ret;
882
883     close($fio->{rxInFd})  if ( defined($fio->{rxInFd}) );
884     unlink("$fio->{outDirSh}RStmp") if  ( -f "$fio->{outDirSh}RStmp" );
885
886     #
887     # Check the final md4 digest
888     #
889     if ( defined($md4) ) {
890         my $newDigest;
891         if ( !defined($fio->{rxDigest}) ) {
892             #
893             # File was exact match, but we still need to verify the
894             # MD4 checksum.  Compute the md4 digest (or fetch the
895             # cached one.)
896             #
897             if ( defined(my $attr = $fio->{rxLocalAttr}) ) {
898                 #
899                 # block size doesn't matter: we're only going to
900                 # fetch the md4 file digest, not the block digests.
901                 #
902                 my($err, $csum, $blkSize)
903                          = BackupPC::Xfer::RsyncDigest->digestStart(
904                                  $attr->{fullPath}, $attr->{size},
905                                  0, 2048, $fio->{checksumSeed}, 1,
906                                  $attr->{compress});
907                 if ( $err ) {
908                     $fio->log("Can't open $attr->{fullPath} for MD4"
909                             . " check (err=$err, $name)");
910                     $fio->{stats}{errorCnt}++;
911                 } else {
912                     $newDigest = $csum->digestEnd;
913                 }
914                 $fio->{rxSize} = $attr->{size};
915             } else {
916                 #
917                 # Empty file; just create an empty file digest
918                 #
919                 $fio->{rxDigest} = File::RsyncP::Digest->new;
920                 $fio->{rxDigest}->add(pack("V", $fio->{checksumSeed}));
921                 $newDigest = $fio->{rxDigest}->digest;
922             }
923             $fio->log("$name got exact match") if ( $fio->{logLevel} >= 5 );
924         } else {
925             $newDigest = $fio->{rxDigest}->digest;
926         }
927         if ( $fio->{logLevel} >= 3 ) {
928             my $md4Str = unpack("H*", $md4);
929             my $newStr = unpack("H*", $newDigest);
930             $fio->log("$name got digests $md4Str vs $newStr")
931         }
932         if ( $md4 ne $newDigest ) {
933             $fio->log("$name: fatal error: md4 doesn't match");
934             $fio->{stats}{errorCnt}++;
935             if ( defined($fio->{rxOutFd}) ) {
936                 $fio->{rxOutFd}->close;
937                 unlink($fio->{rxOutFile});
938             }
939             delete($fio->{rxFile});
940             delete($fio->{rxOutFile});
941             return 1;
942         }
943     }
944
945     #
946     # One special case is an empty file: if the file size is
947     # zero we need to open the output file to create it.
948     #
949     if ( $fio->{rxSize} == 0 ) {
950         my $rxOutFileRel = "$fio->{shareM}/"
951                          . $fio->{bpc}->fileNameMangle($name);
952         my $rxOutFile    = $fio->{outDir} . $rxOutFileRel;
953         $fio->{rxOutFd}  = BackupPC::PoolWrite->new($fio->{bpc},
954                                            $rxOutFile, $fio->{rxSize},
955                                            $fio->{xfer}{compress});
956     }
957     if ( !defined($fio->{rxOutFd}) ) {
958         #
959         # No output file, meaning original was an exact match.
960         #
961         $fio->log("$name: nothing to do")
962                         if ( $fio->{logLevel} >= 5 );
963         my $attr = $fio->{rxLocalAttr};
964         my $f = $fio->{rxFile};
965         $fio->logFileAction("same", $f) if ( $fio->{logLevel} >= 1 );
966         if ( $fio->{full}
967                 || $attr->{type}  != $f->{type}
968                 || $attr->{mtime} != $f->{mtime}
969                 || $attr->{size}  != $f->{size}
970                 || $attr->{gid}   != $f->{gid}
971                 || $attr->{mode}  != $f->{mode} ) {
972             #
973             # In the full case, or if the attributes are different,
974             # we need to make a link from the previous file and
975             # set the attributes.
976             #
977             my $rxOutFile = $fio->{outDirSh}
978                             . $fio->{bpc}->fileNameMangle($name);
979             if ( !link($attr->{fullPath}, $rxOutFile) ) {
980                 $fio->log("Unable to link $attr->{fullPath} to $rxOutFile");
981                 $fio->{stats}{errorCnt}++;
982                 $ret = -1;
983             } else {
984                 #
985                 # Cumulate the stats
986                 #
987                 $fio->{stats}{TotalFileCnt}++;
988                 $fio->{stats}{TotalFileSize} += $fio->{rxSize};
989                 $fio->{stats}{ExistFileCnt}++;
990                 $fio->{stats}{ExistFileSize} += $fio->{rxSize};
991                 $fio->{stats}{ExistFileCompSize} += -s $rxOutFile;
992                 $fio->{rxFile}{size} = $fio->{rxSize};
993                 $ret = $fio->attribSet($fio->{rxFile});
994             }
995         }
996     } else {
997         my $exist = $fio->processClose($fio->{rxOutFd},
998                                        $fio->{rxOutFileRel},
999                                        $fio->{rxSize}, 1);
1000         $fio->logFileAction($exist ? "pool" : "create", $fio->{rxFile})
1001                             if ( $fio->{logLevel} >= 1 );
1002         $fio->{rxFile}{size} = $fio->{rxSize};
1003         $ret = $fio->attribSet($fio->{rxFile});
1004     }
1005     delete($fio->{rxDigest});
1006     delete($fio->{rxInData});
1007     delete($fio->{rxFile});
1008     delete($fio->{rxOutFile});
1009     return $ret;
1010 }
1011
1012 #
1013 # Callback function for BackupPC::View->find.  Note the order of the
1014 # first two arguments.
1015 #
1016 sub fileListEltSend
1017 {
1018     my($a, $fio, $fList, $outputFunc) = @_;
1019     my $name = $a->{relPath};
1020     my $n = $name;
1021     my $type = $fio->mode2type($a->{mode});
1022     my $extraAttribs = {};
1023
1024     $n =~ s/^\Q$fio->{xfer}{pathHdrSrc}//;
1025     $fio->log("Sending $name (remote=$n)") if ( $fio->{logLevel} >= 4 );
1026     if ( $type == BPC_FTYPE_CHARDEV
1027             || $type == BPC_FTYPE_BLOCKDEV
1028             || $type == BPC_FTYPE_SYMLINK ) {
1029         my $fh = BackupPC::FileZIO->open($a->{fullPath}, 0, $a->{compress});
1030         my($str, $rdSize);
1031         if ( defined($fh) ) {
1032             $rdSize = $fh->read(\$str, $a->{size} + 1024);
1033             if ( $type == BPC_FTYPE_SYMLINK ) {
1034                 #
1035                 # Reconstruct symbolic link
1036                 #
1037                 $extraAttribs = { link => $str };
1038                 if ( $rdSize != $a->{size} ) {
1039                     # ERROR
1040                     $fio->log("$name: can't read exactly $a->{size} bytes");
1041                     $fio->{stats}{errorCnt}++;
1042                 }
1043             } elsif ( $str =~ /(\d*),(\d*)/ ) {
1044                 #
1045                 # Reconstruct char or block special major/minor device num
1046                 #
1047                 # Note: char/block devices have $a->{size} = 0, so we
1048                 # can't do an error check on $rdSize.
1049                 #
1050                 $extraAttribs = { rdev => $1 * 256 + $2 };
1051             } else {
1052                 $fio->log("$name: unexpected special file contents $str");
1053                 $fio->{stats}{errorCnt}++;
1054             }
1055             $fh->close;
1056         } else {
1057             # ERROR
1058             $fio->log("$name: can't open");
1059             $fio->{stats}{errorCnt}++;
1060         }
1061     }
1062     my $f = {
1063             name  => $n,
1064             #dev   => 0,                # later, when we support hardlinks
1065             #inode => 0,                # later, when we support hardlinks
1066             mode  => $a->{mode},
1067             uid   => $a->{uid},
1068             gid   => $a->{gid},
1069             mtime => $a->{mtime},
1070             size  => $a->{size},
1071             %$extraAttribs,
1072     };
1073     $fList->encode($f);
1074     $f->{name} = "$fio->{xfer}{pathHdrDest}/$f->{name}";
1075     $f->{name} =~ s{//+}{/}g;
1076     $fio->logFileAction("restore", $f) if ( $fio->{logLevel} >= 1 );
1077     &$outputFunc($fList->encodeData);
1078     #
1079     # Cumulate stats
1080     #
1081     if ( $type != BPC_FTYPE_DIR ) {
1082         $fio->{stats}{TotalFileCnt}++;
1083         $fio->{stats}{TotalFileSize} += $a->{size};
1084     }
1085 }
1086
1087 sub fileListSend
1088 {
1089     my($fio, $flist, $outputFunc) = @_;
1090
1091     #
1092     # Populate the file list with the files requested by the user.
1093     # Since some might be directories so we call BackupPC::View::find.
1094     #
1095     $fio->log("fileListSend: sending file list: "
1096              . join(" ", @{$fio->{fileList}})) if ( $fio->{logLevel} >= 4 );
1097     foreach my $name ( @{$fio->{fileList}} ) {
1098         $fio->{view}->find($fio->{xfer}{bkupSrcNum},
1099                            $fio->{xfer}{bkupSrcShare},
1100                            $name, 1,
1101                            \&fileListEltSend, $fio, $flist, $outputFunc);
1102     }
1103 }
1104
1105 sub finish
1106 {
1107     my($fio, $isChild) = @_;
1108
1109     #
1110     # If we are aborting early, remove the last file since
1111     # it was not complete
1112     #
1113     if ( $isChild && defined($fio->{rxFile}) ) {
1114         unlink("$fio->{outDirSh}RStmp") if  ( -f "$fio->{outDirSh}RStmp" );
1115         if ( defined($fio->{rxFile}) ) {
1116             unlink($fio->{rxOutFile});
1117             $fio->log("finish: removing in-process file $fio->{rxFile}{name}");
1118         }
1119     }
1120
1121     #
1122     # Flush the attributes if this is the child
1123     #
1124     $fio->attribWrite(undef) if ( $isChild );
1125 }
1126
1127 #sub is_tainted
1128 #{
1129 #    return ! eval {
1130 #        join('',@_), kill 0;
1131 #        1;
1132 #    };
1133 #}
1134
1135 1;