1#============================================================= -*-perl-*-
2#
3# BackupPC::Xfer::Rsync package
4#
5# DESCRIPTION
6#
7#   This library defines a BackupPC::Xfer::Rsync class for managing
8#   the rsync-based transport of backup data from/to the client.
9#   After generating the rsync arguments, it calls BackupPC_rsyncBackup
10#   or BackupPC_rsyncRestore to actually do the backup or restore.
11#
12# AUTHOR
13#   Craig Barratt  <cbarratt@users.sourceforge.net>
14#
15# COPYRIGHT
16#   Copyright (C) 2002-2020  Craig Barratt
17#
18#   This program is free software: you can redistribute it and/or modify
19#   it under the terms of the GNU General Public License as published by
20#   the Free Software Foundation, either version 3 of the License, or
21#   (at your option) any later version.
22#
23#   This program is distributed in the hope that it will be useful,
24#   but WITHOUT ANY WARRANTY; without even the implied warranty of
25#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26#   GNU General Public License for more details.
27#
28#   You should have received a copy of the GNU General Public License
29#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
30#
31#========================================================================
32#
33# Version 4.3.3, released 5 Apr 2020.
34#
35# See http://backuppc.sourceforge.net.
36#
37#========================================================================
38
39package BackupPC::Xfer::Rsync;
40
41use strict;
42use BackupPC::View;
43use Encode qw/from_to encode/;
44use base qw(BackupPC::Xfer::Protocol);
45use Errno qw(EINTR);
46
47sub new
48{
49    my($class, $bpc, $args) = @_;
50
51    my $t = BackupPC::Xfer::Protocol->new($bpc, $args);
52
53    $t->{logSave} = [];
54    $t->{logInfo} = {};
55    return bless($t, $class);
56}
57
58sub start
59{
60    my($t) = @_;
61    my $bpc = $t->{bpc};
62    my $conf = $t->{conf};
63    my(@fileList, $rsyncArgs, $logMsg, $rsyncCmd);
64    my $binDir = $t->{bpc}->BinDir();
65    my $shareNamePath = $t->shareName2Path($t->{shareName});
66
67    alarm(0);
68    #
69    # We add a slash to the share name we pass to rsync
70    #
71    ($t->{shareNameSlash} = "$shareNamePath/") =~ s{//+$}{/};
72
73    if ( $t->{type} eq "restore" ) {
74	my $remoteDir = "$shareNamePath/$t->{pathHdrDest}";
75	$remoteDir    =~ s{//+}{/}g;
76        my $filesFd;
77        my $srcList;
78        my $srcDir = "/";
79
80        #from_to($remoteDir, "utf8", $conf->{ClientCharset})
81        #                            if ( $conf->{ClientCharset} ne "" );
82        $rsyncArgs = [@{$conf->{RsyncRestoreArgs}}];
83
84        #
85        # Each name in the fileList starts with $t->{pathHdrSrc}.  The
86        # default $t->{pathHdrDest} also ends in $t->{pathHdrSrc}, although
87        # the user might have changed that.  So we have $t->{pathHdrSrc}
88        # appearing twice: in the fileList and in the target directory.
89        # We have to remove one or the other.
90        #
91        # Since the client rsync only tries to create the last directory
92        # in $t->{pathHdrDest} (rather than the full path), it will fail
93        # if the parent directory doesn't exist.  So, if the last part of
94        # $t->{pathHdrDest} matches $t->{pathHdrSrc}, we remove it.
95        # Otherwise, we remove $t->{pathHdrSrc} from each of fileList,
96        # and it's the user's responsibility to make sure the target
97        # directory exists.
98        #
99        if ( $remoteDir =~ m{(.*)\Q$t->{pathHdrSrc}\E(/*)$} ) {
100            $remoteDir = "$1$2";
101            $remoteDir = "/" if ( $remoteDir eq "" );
102            $t->{XferLOG}->write(\"Trimming $t->{pathHdrSrc} from remoteDir -> $remoteDir\n");
103        } else {
104            for ( my $i = 0 ; $i < @{$t->{fileList}} ; $i++ ) {
105                $t->{fileList}[$i] = substr($t->{fileList}[$i], length($t->{pathHdrSrc}));
106                $t->{fileList}[$i] = "." if ( $t->{fileList}[$i] eq "" );
107            }
108            $srcDir = $t->{pathHdrSrc} if ($t->{pathHdrSrc});
109            $t->{XferLOG}->write(\"Trimming $t->{pathHdrSrc} from filesList\n");
110        }
111
112        $t->{filesFrom} = "$conf->{TopDir}/pc/$t->{client}/.rsyncFilesFrom$$";
113        if ( open($filesFd, ">", $t->{filesFrom}) ) {
114            syswrite($filesFd, join("\n", @{$t->{fileList}}));
115            close($filesFd);
116            $t->{XferLOG}->write(\"Wrote source file list to $t->{filesFrom}: @{$t->{fileList}}\n");
117            $srcList = ["--files-from=$t->{filesFrom}", $srcDir];
118        } else {
119            $t->{XferLOG}->write(\"Failed to open/create file list $t->{filesFrom}\n");
120            $t->{_errStr} = "Failed to open/create file list $t->{filesFrom}";
121            return;
122        }
123
124        if ( $t->{XferMethod} eq "rsync" ) {
125            unshift(@$rsyncArgs, "--rsync-path=$conf->{RsyncClientPath}")
126                            if ( $conf->{RsyncClientPath} ne "" );
127            unshift(@$rsyncArgs, @{$conf->{RsyncSshArgs}})
128                            if ( ref($conf->{RsyncSshArgs}) eq 'ARRAY' );
129            push(@$rsyncArgs, @$srcList, "$t->{hostIP}:$remoteDir");
130        } else {
131            if ( length($conf->{RsyncdPasswd}) ) {
132                my($pwFd, $ok);
133                $t->{pwFile} = "$conf->{TopDir}/pc/$t->{client}/.rsyncdpw$$";
134                if ( open($pwFd, ">", $t->{pwFile}) ) {
135                    $ok = 1;
136                    $ok = 0 if ( $ok && chmod(0400, $t->{pwFile}) != 1 );
137                    $ok = 0 if ( $ok && !binmode($pwFd) );
138                    $ok = 0 if ( $ok && syswrite($pwFd, $conf->{RsyncdPasswd}) != length($conf->{RsyncdPasswd}) );
139                    $ok = 0 if ( $ok && !close($pwFd) );
140                    push(@$rsyncArgs, "--password-file=$t->{pwFile}");
141                }
142                if ( !$ok ) {
143                    $t->{XferLOG}->write(\"Failed to open/create rsynd pw file $t->{pwFile} ($!)\n");
144                    $t->{_errStr} = "Failed to open/create rsynd pw file $t->{pwFile} ($!)";
145                    return;
146                }
147            }
148            #my $shareName = $t->{shareName};
149            #from_to($shareName, "utf8", $conf->{ClientCharset})
150            #                    if ( $conf->{ClientCharset} ne "" );
151            if ( $conf->{RsyncdClientPort} != 873 ) {
152                push(@$rsyncArgs, "--port=$conf->{RsyncdClientPort}");
153            }
154            if ( $conf->{ClientCharset} ne "" ) {
155                push(@$rsyncArgs, "--iconv=utf8,$conf->{ClientCharset}");
156            }
157            push(@$rsyncArgs,
158                    @$srcList,
159                    "$conf->{RsyncdUserName}\@$t->{hostIP}::$remoteDir");
160        }
161
162        #
163        # Merge variables into $rsyncArgs
164        #
165        $rsyncArgs = $bpc->cmdVarSubstitute($rsyncArgs, {
166                            host          => $t->{host},
167                            hostIP        => $t->{hostIP},
168                            client        => $t->{client},
169                            shareNameOrig => $t->{shareName},
170                            shareName     => $shareNamePath,
171                            confDir       => $conf->{ConfDir},
172                            sshPath       => $conf->{SshPath},
173                        });
174        #
175        # create --bpc-bkup-merge list.  This is the list of backups that have to
176        # be merged to create the correct "view" of the backup being restore.
177        #
178        my($srcIdx, $i, $mergeInfo);
179        my $mergeIdxList = [];
180
181        for ( $i = 0 ; $i < @{$t->{backups}} ; $i++ ) {
182            if ( $t->{backups}[$i]{num} == $t->{bkupSrcNum} ) {
183                $srcIdx = $i;
184                last;
185            }
186        }
187        if ( !defined($srcIdx) ) {
188            $t->{_errStr} = "Can't find backup number $t->{bkupSrcNum} in backups file";
189            return;
190        }
191        if ( $t->{backups}[$srcIdx]{version} < 4 ) {
192            #
193            # For per-V4 backups, we merge forward from the prior full.
194            #
195            my $level = $t->{backups}[$srcIdx]{level} + 1;
196            for ( $i = $srcIdx ; $level > 0 && $i >= 0 ; $i-- ) {
197                next if ( $t->{backups}[$i]{level} >= $level );
198                $level = $t->{backups}[$i]{level};
199                unshift(@$mergeIdxList, $i);
200            }
201        } else {
202            #
203            # For V4+ backups, we merge backward from the following filled backup.
204            #
205            for ( $i = $srcIdx ; $i < @{$t->{backups}} ; $i++ ) {
206                unshift(@$mergeIdxList, $i);
207                last if ( !$t->{backups}[$i]{noFill} );
208            }
209        }
210        foreach my $i ( @$mergeIdxList ) {
211            $mergeInfo .= "," if ( length($mergeInfo) );
212            $mergeInfo .= sprintf("%d/%d/%d", $t->{backups}[$i]{num}, $t->{backups}[$i]{compress}, int($t->{backups}[$i]{version}));
213        }
214
215        unshift(@$rsyncArgs,
216            '--bpc-top-dir',        $conf->{TopDir},
217            '--bpc-host-name',      $t->{bkupSrcHost},
218            '--bpc-share-name',     $t->{bkupSrcShare},
219            '--bpc-bkup-num',       $t->{backups}[$srcIdx]{num},
220            '--bpc-bkup-comp',      $t->{backups}[$srcIdx]{compress},
221            '--bpc-bkup-merge',     $mergeInfo,
222            '--bpc-attrib-new',
223            '--bpc-log-level',      $conf->{XferLogLevel},
224        );
225
226        $logMsg = "restore started below directory $t->{shareName}"
227		. " to host $t->{host}";
228    } else {
229	#
230	# Turn $conf->{BackupFilesOnly} and $conf->{BackupFilesExclude}
231	# into a hash of arrays of files, and $conf->{RsyncShareName}
232	# to an array
233	#
234	$bpc->backupFileConfFix($conf, "RsyncShareName");
235
236        if ( defined($conf->{BackupFilesOnly}{$t->{shareName}}) ) {
237            my(@inc, @exc, %incDone, %excDone);
238            foreach my $file2 ( @{$conf->{BackupFilesOnly}{$t->{shareName}}} ) {
239                #
240                # If the user wants to just include /home/craig, then
241                # we need to do create include/exclude pairs at
242                # each level:
243                #     --include /home --exclude /*
244                #     --include /home/craig --exclude /home/*
245                #
246                # It's more complex if the user wants to include multiple
247                # deep paths.  For example, if they want /home/craig and
248                # /var/log, then we need this mouthfull:
249                #     --include /home --include /var --exclude /*
250                #     --include /home/craig --exclude /home/*
251                #     --include /var/log --exclude /var/*
252                #
253                # To make this easier we do all the includes first and all
254                # of the excludes at the end (hopefully they commute).
255                #
256                my $file = $file2;
257                $file =~ s{/$}{};
258                $file = "/$file";
259                $file =~ s{//+}{/}g;
260		if ( $file eq "/" ) {
261		    #
262		    # This is a special case: if the user specifies
263		    # "/" then just include it and don't exclude "/*".
264		    #
265                    push(@inc, $file) if ( !$incDone{$file} );
266		    next;
267		}
268                my $f = "";
269                while ( $file =~ m{^/([^/]*)(.*)} ) {
270                    my $elt = $1;
271                    $file = $2;
272                    if ( $file eq "/" ) {
273                        #
274                        # preserve a tailing slash
275                        #
276                        $file = "";
277                        $elt = "$elt/";
278                    }
279                    push(@exc, "$f/*") if ( !$excDone{"$f/*"} );
280                    $excDone{"$f/*"} = 1;
281                    $f = "$f/$elt";
282                    push(@inc, $f) if ( !$incDone{$f} );
283                    $incDone{$f} = 1;
284                }
285            }
286            foreach my $file ( @inc ) {
287                $file = encode($conf->{ClientCharset}, $file)
288                            if ( $conf->{ClientCharset} ne "" );
289                push(@fileList, "--include=$file");
290            }
291            foreach my $file ( @exc ) {
292                $file = encode($conf->{ClientCharset}, $file)
293                            if ( $conf->{ClientCharset} ne "" );
294                push(@fileList, "--exclude=$file");
295            }
296        }
297        if ( defined($conf->{BackupFilesExclude}{$t->{shareName}}) ) {
298            foreach my $file2 ( @{$conf->{BackupFilesExclude}{$t->{shareName}}} ) {
299                #
300                # just append additional exclude lists onto the end
301                #
302                my $file = $file2;
303                $file = encode($conf->{ClientCharset}, $file)
304                            if ( $conf->{ClientCharset} ne "" );
305                push(@fileList, "--exclude=$file");
306            }
307        }
308        #
309        # A full dump is implemented with $Conf{RsyncFullArgsExtra},
310        # which is normally --checksum.  This causes the client to
311        # generate and send a full-file checksum for each file with
312        # the file list.  That can be directly compared with the
313        # V4 full-file digest.
314        #
315        # In V3 --ignore-times was used, that causes block checksum
316        # to be generated and checked for every file.  That's more
317        # conservative, but a lot more effort.
318        #
319        $rsyncArgs = [@{$conf->{RsyncArgs}}];
320
321        if ( $t->{type} eq "full" ) {
322            $logMsg = "full backup started for directory $t->{shareName}";
323            if ( ref($conf->{RsyncFullArgsExtra}) eq 'ARRAY' ) {
324                push(@$rsyncArgs, @{$conf->{RsyncFullArgsExtra}});
325            } elsif ( ref($conf->{RsyncFullArgsExtra}) eq '' && $conf->{RsyncFullArgsExtra} ne "" ) {
326                push(@$rsyncArgs, $conf->{RsyncFullArgsExtra});
327            }
328        } else {
329            $logMsg = "incr backup started for directory $t->{shareName}";
330            if ( ref($conf->{RsyncIncrArgsExtra}) eq 'ARRAY' ) {
331                push(@$rsyncArgs, @{$conf->{RsyncIncrArgsExtra}});
332            } elsif ( ref($conf->{RsyncIncrArgsExtra}) eq '' && $conf->{RsyncIncrArgsExtra} ne "" ) {
333                push(@$rsyncArgs, $conf->{RsyncIncrArgsExtra});
334            }
335        }
336
337        #
338        # Add any additional rsync args
339        #
340        push(@$rsyncArgs, @{$conf->{RsyncArgsExtra}})
341                    if ( ref($conf->{RsyncArgsExtra}) eq 'ARRAY' );
342        if ( $conf->{ClientCharset} ne "" ) {
343            push(@$rsyncArgs, "--iconv=utf8,$conf->{ClientCharset}");
344        }
345        if ( $conf->{ClientTimeout} > 0 && $conf->{ClientTimeout} =~ /^\d+$/ ) {
346            push(@$rsyncArgs, "--timeout=$conf->{ClientTimeout}");
347        }
348
349        if ( $t->{XferMethod} eq "rsync" ) {
350            unshift(@$rsyncArgs, "--rsync-path=$conf->{RsyncClientPath}")
351                            if ( $conf->{RsyncClientPath} ne "" );
352            unshift(@$rsyncArgs, @{$conf->{RsyncSshArgs}})
353                            if ( ref($conf->{RsyncSshArgs}) eq 'ARRAY' );
354        } else {
355            if ( $conf->{RsyncdClientPort} != 873 ) {
356                push(@$rsyncArgs, "--port=$conf->{RsyncdClientPort}");
357            }
358        }
359
360        #
361        # Merge variables into $rsyncArgs
362        #
363        $rsyncArgs = $bpc->cmdVarSubstitute($rsyncArgs, {
364                            host          => $t->{host},
365                            hostIP        => $t->{hostIP},
366                            client        => $t->{client},
367                            shareNameOrig => $t->{shareName},
368                            shareName     => $shareNamePath,
369                            confDir       => $conf->{ConfDir},
370                            sshPath       => $conf->{SshPath},
371                        });
372
373        if ( $t->{XferMethod} eq "rsync" ) {
374            my $shareNameSlash = $t->{shareNameSlash};
375            #from_to($shareNameSlash, "utf8", $conf->{ClientCharset})
376            #                    if ( $conf->{ClientCharset} ne "" );
377
378            push(@$rsyncArgs, @fileList) if ( @fileList );
379            push(@$rsyncArgs, "$t->{hostIP}:$shareNameSlash", "/");
380        } else {
381            my $pwFd;
382            $t->{pwFile} = "$conf->{TopDir}/pc/$t->{client}/.rsyncdpw$$";
383            if ( !length($conf->{RsyncdPasswd}) ) {
384                $t->{XferLOG}->write(\"\$Conf{RsyncdPasswd} is empty; host's rsyncd auth will fail\n");
385                $t->{_errStr} = "\$Conf{RsyncdPasswd} is empty; host's rsyncd auth will fail";
386                return;
387            }
388            if ( open($pwFd, ">", $t->{pwFile}) ) {
389                chmod(0400, $t->{pwFile});
390                binmode($pwFd);
391                syswrite($pwFd, $conf->{RsyncdPasswd});
392                close($pwFd);
393                push(@$rsyncArgs, "--password-file=$t->{pwFile}");
394            } else {
395                $t->{XferLOG}->write(\"Failed to open/create rsynd pw file $t->{pwFile}\n");
396                $t->{_errStr} = "Failed to open/create rsynd pw file $t->{pwFile}";
397                return;
398            }
399            my $shareName = $shareNamePath;
400            #from_to($shareName, "utf8", $conf->{ClientCharset})
401            #                    if ( $conf->{ClientCharset} ne "" );
402            push(@$rsyncArgs, @fileList) if ( @fileList );
403            push(@$rsyncArgs,
404                    "$conf->{RsyncdUserName}\@$t->{hostIP}::$shareName",
405                    "/");
406        }
407        if ( $bpc->{PoolV3} ) {
408            unshift(@$rsyncArgs,
409                    '--bpc-hardlink-max',  $conf->{HardLinkMax} || 31999,
410                    '--bpc-v3pool-used',   $conf->{PoolV3Enabled},
411            );
412        }
413
414	my $inode0 = 1;
415        for ( my $i = 0 ; $i < @{$t->{backups}} ; $i++ ) {
416            $inode0 = $t->{backups}[$i]{inodeLast} + 1 if ( $inode0 <= $t->{backups}[$i]{inodeLast} );
417        }
418
419        unshift(@$rsyncArgs,
420            '--bpc-top-dir',        $conf->{TopDir},
421            '--bpc-host-name',      $t->{client},
422            '--bpc-share-name',     $t->{shareName},
423            '--bpc-bkup-num',       $t->{backups}[$t->{newBkupIdx}]{num},
424            '--bpc-bkup-comp',      $t->{backups}[$t->{newBkupIdx}]{compress},
425            '--bpc-bkup-prevnum',   defined($t->{lastBkupIdx}) ? $t->{backups}[$t->{lastBkupIdx}]{num} : -1,
426            '--bpc-bkup-prevcomp',  defined($t->{lastBkupIdx}) ? $t->{backups}[$t->{lastBkupIdx}]{compress} : -1,
427            '--bpc-bkup-inode0',    $inode0,
428            '--bpc-attrib-new',
429            '--bpc-log-level',      $conf->{XferLogLevel},
430        );
431    }
432    $logMsg .= " (client path $shareNamePath)" if ( $t->{shareName} ne $shareNamePath );
433
434    #from_to($args->{shareName}, "utf8", $conf->{ClientCharset})
435    #                        if ( $conf->{ClientCharset} ne "" );
436    if ( $conf->{RsyncBackupPCPath} eq "" || !-x $conf->{RsyncBackupPCPath} ) {
437        $t->{_errStr} = "\$Conf{RsyncBackupPCPath} is set to $conf->{RsyncBackupPCPath}, which isn't a valid executable";
438        return;
439    }
440    $rsyncCmd = [$conf->{RsyncBackupPCPath}, @$rsyncArgs];
441
442    my $rsyncFd;
443    if ( !defined($t->{xferPid} = open($rsyncFd, "-|")) ) {
444        $t->{_errStr} = "Can't fork to run $conf->{RsyncBackupPCPath}";
445        return;
446    }
447    $t->{rsyncFd} = $rsyncFd;
448    if ( !$t->{xferPid} ) {
449        #
450        # This is the rsync child.  We capture both stdout
451        # and stderr to put into the XferLOG file.
452        #
453        setpgrp 0,0;
454        close(STDERR);
455        open(STDERR, ">&STDOUT");
456        #
457        # Run the $conf->{RsyncBackupPCPath} command
458        #
459        print("This is the rsync child about to exec $conf->{RsyncBackupPCPath}\n");
460	$bpc->cmdExecOrEval($rsyncCmd);
461        print("cmdExecOrEval failed $?\n");
462        # should not be reached, but just in case...
463        $t->{_errStr} = "Can't exec @$rsyncCmd)";
464        return;
465    }
466    my $str = $bpc->execCmd2ShellCmd(@$rsyncCmd);
467    #from_to($str, $conf->{ClientCharset}, "utf8")
468    #                        if ( $conf->{ClientCharset} ne "" );
469    $t->{XferLOG}->write(\"Running: $str\n");
470    $t->{_errStr} = undef;
471    return $logMsg;
472}
473
474sub run
475{
476    my($t) = @_;
477    my $conf = $t->{conf};
478    my $bpc  = $t->{bpc};
479
480    alarm(0);
481    while ( 1 ) {
482        my($mesg, $done);
483        if ( sysread($t->{rsyncFd}, $mesg, 32768) <= 0 ) {
484            next if ( $!{EINTR} );
485            if ( !close($t->{rsyncFd}) ) {
486                #
487                # rsync exits with the RERR_* codes in errcode.h.  Exit codes 23, 24, 25 are minor (ie: some
488                # error in transfer, but not fatal).  Other non-zero exit codes are considered failures.
489                #
490                $t->{lastOutputLine} = $t->{lastErrorLine} if ( defined($t->{lastErrorLine}) );
491                my $exitCode = $? >> 8;
492                if ( $exitCode == 23 || $exitCode == 24 || $exitCode == 25 ) {
493                    $t->{rsyncOut} .= "rsync_bpc exited with benign status $exitCode ($?)\n";
494                    $t->{xferOK} = 1;
495                    $t->{stats}{xferErrs}++;
496                } else {
497                    $t->{rsyncOut} .= "rsync_bpc exited with fatal status $exitCode ($?) ($t->{lastOutputLine})\n";
498                    $t->{xferOK} = 0;
499                    $t->{stats}{xferErrs}++;
500                }
501	    } else {
502		$t->{xferOK} = 1;
503            }
504            $done = 1;
505        } else {
506            $t->{rsyncOut} .= $mesg;
507        }
508        while ( $t->{rsyncOut} =~ /(.*?)[\n\r]+(.*)/s ) {
509            $_ = $1;
510            $t->{rsyncOut} = $2;
511            #
512            # refresh our inactivity alarm
513            #
514            if ( /^log:\s(recv|del\.|send)\s(.{11})\s.{9}\s*\d+,\s*\d+\s*(\d+)\s(.*)/ ) {
515                my $type     = $1;
516                my $changes  = $2;
517                my $size     = $3;
518                my $fileName = $4;
519                if ( $changes =~ /^\./ ) {
520                    $t->{logInfo}{$fileName}{seqNum} = ++$t->{logInfoSeq};
521                    push(@{$t->{logInfo}{$fileName}{status}}, "same");
522                }
523                if ( $type eq "del." ) {
524                    $t->{logInfo}{$fileName}{seqNum} = ++$t->{logInfoSeq};
525                    push(@{$t->{logInfo}{$fileName}{status}}, "del");
526                }
527                s/^log: //;
528                push(@{$t->{logSave}},
529                        {
530                            mesg     => $_,
531                            type     => $type,
532                            fileName => $fileName,
533                        });
534                $t->logSaveFlush();
535                next;
536            }
537            if ( /^IOdone:\s(\S*)\s(.*)/ ) {
538                my $status   = $1;
539                my $fileName = $2;
540                $t->{logInfo}{$fileName}{seqNum} = ++$t->{logInfoSeq};
541                push(@{$t->{logInfo}{$fileName}{status}}, $status);
542                $t->{XferLOG}->write(\"$_\n") if ( $conf->{XferLogLevel} >= 6 );
543                $t->logSaveFlush();
544                next;
545            }
546            if ( /^__bpc_progress_fileCnt__ \d+/ ) {
547                print("$_\n") if ( !$t->{noProgressPrint} );
548                $t->{XferLOG}->write(\"$_\n") if ( $conf->{XferLogLevel} >= 6 );
549                next;
550            }
551            if ( /^ERROR: / ) {
552                if ( /failed verification -- update discarded./ ) {
553                    $t->{xferBadFileCnt}++;
554                }
555                $t->{stats}{xferErrs}++;
556            }
557            if ( /^rsync error: / || /^rsync warning: / ) {
558                $t->{stats}{xferErrs}++;
559            }
560            if ( /^rsync: send_files failed to open / || /^file has vanished: / ) {
561                $t->{stats}{xferErrs}++;
562            }
563            if ( /^IOrename:\s(\d+)\s(.*)/ ) {
564                my $oldName = substr($2, 0, $1);
565                my $newName = substr($2, $1);
566                $t->{logInfo}{$newName} = $t->{logInfo}{$oldName};
567                delete($t->{logInfo}{$oldName});
568                $t->{XferLOG}->write(\"$_\n") if ( $conf->{XferLogLevel} >= 6 );
569                $t->logSaveFlush();
570                next;
571            }
572            if ( /^xferPids (\d+),(\d+)/ ) {
573                my $pidHandler = $t->{pidHandler};
574                if ( ref($pidHandler) eq 'CODE' ) {
575                    &$pidHandler($1, $2);
576                } else {
577                    $t->{XferLOG}->write(\"$_\n") if ( $conf->{XferLogLevel} >= 4 );
578                }
579            }
580            if ( /^Done(Gen)?: (\d+) errors, (\d+) filesExist, (\d+) sizeExist, (\d+) sizeExistComp, (\d+) filesTotal, (\d+) sizeTotal, (\d+) filesNew, (\d+) sizeNew, (\d+) sizeNewComp, (\d+) inode/ ) {
581                $t->{stats}{xferErrs}      += $2;
582                $t->{stats}{nFilesExist}   += $3;
583                $t->{stats}{sizeExist}     += $4;
584                $t->{stats}{sizeExistComp} += $5;
585                $t->{stats}{nFilesTotal}   += $6;
586                $t->{stats}{sizeTotal}     += $7;
587                $t->{stats}{nFilesNew}     += $8;
588                $t->{stats}{sizeNew}       += $9;
589                $t->{stats}{sizeNewComp}   += $10;
590                $t->{stats}{inode}          = $11 if ( $t->{stats}{inode} < $11 );
591                $t->{XferLOG}->write(\"$_\n");
592                $t->{XferLOG}->write(\"Parsing done: nFilesTotal = $t->{stats}{nFilesTotal}\n")
593                                        if ( $conf->{XferLogLevel} >= 3 );
594                $t->{fileCnt} = $t->{stats}{nFilesTotal};
595                $t->{byteCnt} = $t->{stats}{sizeTotal};
596                next;
597            }
598
599#           if ( /: \.\/(.*): Read error at byte / ) {
600#                my $badFile = $1;
601#                push(@{$t->{badFiles}}, {
602#                        share => $t->{shareName},
603#                        file  => $badFile
604#                    });
605#           }
606#            from_to($_, $conf->{ClientCharset}, "utf8")
607#                                if ( $conf->{ClientCharset} ne "" );
608            $t->{lastOutputLine} = $_ if ( !/^\s+$/ && length($_) );
609            $t->{lastErrorLine}  = $_ if ( /^rsync_bpc: / || /^rsync error: / );
610            $t->{XferLOG}->write(\"$_\n");
611        }
612        last if ( $done );
613    }
614    unlink($t->{pwFile})    if ( length($t->{pwFile})    && -f $t->{pwFile} );
615    unlink($t->{filesFrom}) if ( length($t->{filesFrom}) && -f $t->{filesFrom} );
616    $t->logSaveFlush(1);
617    $t->{lastOutputLine} = $t->{lastErrorLine} if ( defined($t->{lastErrorLine}) );
618
619    #
620    # Remove any rsyncTmp files in the backup directory
621    #
622    my $bkupDir = $t->{type} eq "restore" ? "$conf->{TopDir}/pc/$t->{bkupSrcHost}/$t->{bkupSrcNum}"
623					  : "$conf->{TopDir}/pc/$t->{client}/$t->{backups}[$t->{newBkupIdx}]{num}";
624    my $bkupDirEntries = BackupPC::DirOps::dirRead($bpc, $bkupDir);
625    my $pidRunning = {};
626    if ( ref($bkupDirEntries) eq 'ARRAY' ) {
627        foreach my $e ( @$bkupDirEntries ) {
628            next if ( $e->{name} !~ /^rsyncTmp\.(\d+)\.\d+\.\d+$/ );
629            my $pid = $1;
630            $pidRunning->{$pid} = kill(0, $pid) ? 1 : 0 if ( !defined($pidRunning->{$pid} ) );
631            next if ( $pidRunning->{$pid} );
632	    $t->{XferLOG}->write(\"Removing rsync temporary file $bkupDir/$e->{name}\n");
633	    unlink("$bkupDir/$e->{name}");
634        }
635    }
636
637    if ( $t->{type} eq "restore" ) {
638        if ( $t->{xferOK} ) {
639            return (
640                $t->{fileCnt},
641                $t->{byteCnt},
642                0,
643                undef,
644            );
645        } else {
646            return (
647                $t->{fileCnt},
648                $t->{byteCnt},
649                $t->{stats}{xferErrs},
650                $t->{lastOutputLine},
651            );
652        }
653    } else {
654        $t->{xferErrCnt} = $t->{stats}{xferErrs};
655	return (
656            $t->{stats}{xferErrs},
657            $t->{stats}{nFilesExist},
658            $t->{stats}{sizeExist},
659            $t->{stats}{sizeExistComp},
660            $t->{stats}{nFilesTotal},
661            $t->{stats}{sizeTotal},
662            $t->{stats}{nFilesNew},
663            $t->{stats}{sizeNew},
664            $t->{stats}{sizeNewComp},
665            $t->{stats}{inode},
666	);
667    }
668}
669
670sub errStr
671{
672    my($t) = @_;
673
674    return $t->{_errStr};
675}
676
677sub xferPid
678{
679    my($t) = @_;
680
681    return $t->{xferPid};
682}
683
684#
685#  usage:
686#    $t->abort($reason);
687#
688# Aborts the current job, by sending an INT signal to the first rsync process
689# (which in turn kills the receiver child).
690#
691sub abort
692{
693    my($t, $reason) = @_;
694    my @xferPid = $t->xferPid;
695
696    $t->{abort}       = 1;
697    $t->{abortReason} = $reason;
698    if ( @xferPid ) {
699        kill($t->{bpc}->sigName2num("INT"), $xferPid[0]);
700    }
701}
702
703sub logSaveFlush
704{
705    my($t, $all) = @_;
706    my $change = 1;
707    my $conf = $t->{conf};
708
709    $all = 1 if ( $t->{type} eq "restore" );
710
711    while ( $change && @{$t->{logSave}} ) {
712        $change = 0;
713        my $fileName = $t->{logSave}[0]{fileName};
714        if ( defined($t->{logInfo}{$fileName}) || $all || @{$t->{logSave}} > 200 ) {
715            my $mesg = sprintf("    %-6s %s",
716                                shift(@{$t->{logInfo}{$fileName}{status}}),
717                                $t->{logSave}[0]{mesg});
718            delete($t->{logInfo}{$fileName}) if ( !@{$t->{logInfo}{$fileName}{status}} );
719            shift(@{$t->{logSave}});
720            #from_to($mesg, $conf->{ClientCharset}, "utf8")
721            #                    if ( $conf->{ClientCharset} ne "" );
722            $t->{lastOutputLine} = $mesg if ( !/^\s+$/ && length($mesg) );
723            $t->{XferLOG}->write(\"$mesg\n");
724            $change = 1;
725        }
726    }
727    if ( %{$t->{logInfo}} > 2000 ) {
728        #
729        # prune the fileName logInfo array if it gets too big
730        #
731        my @info = sort { $t->{logInfo}{$a}{seqNum} <=> $t->{logInfo}{$b}{seqNum} }
732                        keys(%{$t->{logInfo}});
733        while ( @info > 500 ) {
734            my $fileName = shift(@info);
735            delete($t->{logInfo}{$fileName});
736        }
737    }
738}
739
7401;
741