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