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