1#!/usr/bin/env perl 2 3# rsyncbackup <http://BSDforge.com/projects/sysutils/rsync-backup/> 4# A backup utility for automating rsync backup from multiple sources 5# to multiple destinations. 6# 7# Copyright 2005-2017 (C) Chris Hutchinson :: BSDforge.com 8# Copyright 2004 (C) Andreas Aakre Solberg <mac@solweb.no> 9# 10 11#use warnings; 12use strict; 13use Getopt::Long; 14use Data::Dumper; 15use Digest::MD5 qw(md5_base64); 16 17my $Notes = ["StartBackup", "FinnishedBackup", "BackupFailed"]; 18my $AppName = "rsyncbackup"; 19my $GROWLINSTALLED = eval q{ 20 use Mac::Growl; 21 Mac::Growl::RegisterNotifications($AppName,$Notes,$Notes); 22 1; 23}; 24 25 26sub growlnotify() { 27 my ($serv, $type, $header, $msg, $stick) = @_; 28 my ($ip, $port, $passwd) = split(/:/, $serv); 29 my $application = 'rsyncbackup'; 30 my $types = 'StartBackup,FinnishedBackup,BackupFailed'; 31 32 eval { 33 use IO::Socket::INET; 34 my $client = new IO::Socket::INET->new( 35 PeerPort => $port, 36 Proto => 'udp', 37 PeerAddr => $ip 38 ); 39 my $regmsg = 'register|' . $application . '|' . $types; 40 $client->send($regmsg . '$' . md5_base64($regmsg . $passwd) ); 41 my $datagram; 42 if ($stick == 1) { 43 $datagram = "stick|$application|$type|$header|$msg"; 44 } else { 45 $datagram = "notify|$application|$type|$header|$msg"; 46 } 47 my $checksum .= '$' . md5_base64($datagram . $passwd); 48 49 $client->send($datagram . $checksum); 50 1; 51 } 52} 53 54# print "Support: " . $GROWLINSTALLED . "\n"; 55 56Getopt::Long::Configure ("bundling"); 57 58my %options = ( 59 'help' => 1, 60 'verbosity' => 1 61); 62 63GetOptions(\%options, 64 'help|h', 65 'backupset|s=s', 66 'do_backup|b', 67 'cronlist|l', 68 'debugconf|d', 69 'email|e=s', 70 'stats|w', 71 'add_remote|r', 72 'no_file_log|n', 73 'version', 74 'growl', 75 'growlnotify=s', 76 'verbose|v+', 77 'quiet|q', 78 'backupconfigdir|x=s', 79 'status', 80 'dry-run', 81 'rsync-dry-run' 82); 83 84# print Dumper(\%options); 85 86 87my $PATH_DIR = $ENV{'HOME'} . "/backup/"; 88if (defined $options{'backupconfigdir'}) { 89 $PATH_DIR = $options{'backupconfigdir'} . '/'; 90} 91 92my $CONFIG_FILE = $PATH_DIR . "config.conf"; 93my $BACKUPSET_FILE = $PATH_DIR . "backupset.conf"; 94my $DESTS_FILE = $PATH_DIR . "destinations.conf"; 95my $SOURCE_FILE = $PATH_DIR . "sources.conf"; 96my $STATUS_FILE = $PATH_DIR . ".rsyncbackup.status"; 97 98my $LOG_FOLDER = $PATH_DIR . "logs"; 99 100if (defined $options{'verbose'}) { 101 $options{'verbosity'} += $options{'verbose'}; 102} 103if (defined $options{'quiet'}) { 104 $options{'verbosity'} = 0; 105} 106 107my $BACKUPSET = 'default'; 108if (defined $options{'backupset'}) { 109 $BACKUPSET = $options{'backupset'}; 110} 111 112my $EMAIL = undef; 113if (defined $options{'email'}) { 114 $EMAIL = $options{'email'}; 115} 116 117 118 119OPTSWITCH : { 120 121 # Print version 122 exists($options{'version'}) && do { 123 $options{'help'} = 0; 124 &print_version($options{'verbosity'}); 125 last; 126 }; 127 128 # Add remote destination 129 exists($options{'add_remote'}) && do { 130 $options{'help'} = 0; 131 &add_remote($options{'verbosity'}); 132 last; 133 }; 134 135 # Debug configuration 136 exists($options{'debugconf'}) && do { 137 $options{'help'} = 0; 138 &debug_conf($options{'verbosity'}); 139 last; 140 }; 141 142 # Print statistics 143 exists($options{'stats'}) && do { 144 $options{'help'} = 0; 145 &statistics($options{'verbosity'}); 146 last; 147 }; 148 149 # Do backup 150 exists($options{'do_backup'}) && do { 151 $options{'help'} = 0; 152 &do_backup($options{'verbosity'}); 153 last; 154 }; 155 156 # Show status 157 exists($options{'status'}) && do { 158 $options{'help'} = 0; 159 &read_status($STATUS_FILE); 160 last; 161 }; 162 163 # Add remote destination 164 exists($options{'remote'}) && do { 165 $options{'help'} = 0; 166 &add_remote($options{'verbosity'}); 167 last; 168 }; 169 170 exists($options{'cronlist'}) && do { 171 $options{'help'} = 0; 172 &list_cron; 173 last; 174 }; 175 176 # Print help screen 177 ( $options{'help'} == 1) && do { 178 print <<END 179Usage: rsyncbackup [options] 180 181[options] : 182 -b Run backup 183 --do_backup 184 185 -s file Specify which backup source file to use 186 --source file 187 188 -h Print this help screen 189 --help 190 191 -l Print a list of all cronjobs for rsyncbackup 192 --cronlist 193 194 -d Debug configurations 195 --debugconf 196 197 --status Check wether rsyncbackup is currently running, and for how long. 198 199 -e Specify e-mail address to send errors, when errors occur 200 --email Example: --email rsyncerror\@hotmail.com 201 202 -w Print statistics about disk usage for source and destination folders 203 --stats 204 205 -r Add remote destination. This is a wizard for creating ssh-keys and distribute them. 206 --add_remote 207 208 --growl Send notifications to Growl when starting and stopping backup. Will send a sticky 209 notification, if an error occur. 210 211 --growlnotify Send notifications to Remote Growl. http://erlang.no/remotegrowl 212 To send Growl notifications from a cronjob, you have to use --growlnotify and 213 remotegrowl, rather than --growl. 214 215 --version Prints version information 216 217 -n Do not log to files, instead print to stdout 218 --no_file_log 219 220 -x Specify another backup config directory than ~/backup 221 --backupconfigdir Example: --backupconfigdir /etc/rsyncbackup 222 223 --dry-run Do not execute rsync command. 224 --rsync-dry-run Do execute rsync command with --dry-run parameter. 225 226 -q Do not print output 227 --quiet 228 -v Print more output 229 --verbose 230 -vv Print even more 231 232Read online documentation on: http://BSDforge.com/projects/sysutils/rsync-backup/ 233END 234 } 235} 236 237#### Functions ### 238 239sub print_verb { 240 my ($verbosity, $verblevel, $string) = @_; 241 print $string if ($verbosity >= $verblevel); 242} 243 244sub trim { 245 my $temp = shift @_; 246 $temp =~ s/(^\s+|\s+$)//g; 247 return $temp; 248} 249 250sub datediff() { 251 my $diff = shift; 252 253 my $secs = $diff % 60; 254 $diff = ($diff - $secs) / 60; 255 256 my $mins = $diff % 60; 257 $diff = ($diff - $mins) / 60; 258 259 my $hrs = $diff % 60; 260 $diff = ($diff - $hrs) / 60; 261 262 my $days = $diff; 263 264 my $ds = ""; 265 266 if ($days > 0) { 267 $ds = $days . " days and " . $hrs . " hours"; 268 } elsif ($hrs > 0) { 269 $ds = $hrs . " hours and " . $mins . " minutes"; 270 } else { 271 $ds = $mins . " minutes and " . $secs . " seconds"; 272 } 273 return $ds; 274} 275 276sub list_cron { 277 system("crontab -l|grep rsyncbackup"); 278} 279 280sub print_version { 281 my $verbosity = shift @_; 282 &print_verb($verbosity, 1, "rsyncbackup version 1.1, 2017-03-22\n"); 283 &print_verb($verbosity, 1, "(c) 2005-2017, Chris Hutchinson\n"); 284 &print_verb($verbosity, 1, "(c) 2004, Andreas Aakre Solberg\n"); 285 &print_verb($verbosity, 1, "<http://BSDforge.com/projects/sysutils/rsync-backup/>\n"); 286} 287 288sub add_remote { 289 my $verbosity = shift @_; 290 291 292 KEYGEN : { 293 print "Enter the hostname of the remote computer (or IP-address): "; 294 my $rhost = <>; chomp($rhost); 295 296 print "Enter the backup directory on the remote computer [Backup]: "; 297 my $rdir = <>; chomp($rdir); 298 if (not $rdir) { $rdir = 'Backup'; } 299 300 print "Enter your username on the remote computer [" . $ENV{"USER"} . "]:"; 301 my $ruser = <>; chomp($ruser); 302 if (not $ruser) { $ruser = $ENV{"USER"}; } 303 304 print "Enter an unique tag for the destination [$rhost]: "; 305 my $rtag = <>; chomp($rtag); 306 if (not $rtag) { $rtag = $rhost; } 307 308# print "$rhost - $ruser - $rdir - $rtag\n"; 309# exit(1); 310 311 if (-f $ENV{'HOME'} . '/.ssh/rsyncbackup') { 312 print "You already have a ssh key for use with rsyncbackup, so we skip \n" . 313 "the key creation process.\n\n"; 314 } else { 315 316 print <<END 317********************************************************************* 318* SECURITY Warning * 319* * 320* You are about to create a ssh-keyset without password for logging * 321* in to a remote host. That means hackers who attack your computer * 322* also can log in to the remote computer. Do clearify if this is * 323* OK with the local IT-administration of the remote host. * 324********************************************************************* 325 326END 327 ; 328 print "Press enter to create the keys, or Control+C to quit\n"; <>; 329 system('ssh-keygen -t dsa -f ~/.ssh/rsyncbackup -N ""'); 330 331 } 332 333 print "The public key will now be distributed to the remote computer. You will have to enter\n" . 334 "your ssh account password on the remote computer.\n"; 335 336 my $dcom = 'cat ~/.ssh/rsyncbackup.pub | ssh ' . 337 $ruser . '@' . $rhost . " 'mkdir -p ~/.ssh " . $rdir . ' && ' . 338 "cat - >> ~/.ssh/authorized_keys'"; 339 340 #print "dcom: $dcom\n\n"; 341 system($dcom); 342 343 print "Public key is now distributed to the remote computer successfully\n\n"; 344 345 my $dline = $rtag . '|ssh[key=rsyncbackup,incremental=0]:' . $ruser . '@' . $rhost . ':' . $rdir . '|true|'; 346 347 print "Writing $rtag to " . $DESTS_FILE . " :\n" . $dline . "\n\n"; 348 349 open DFILE, '>>', $DESTS_FILE; 350 print DFILE "\n#Destination added by remote host wizard\n" . $dline . "\n"; 351 close DFILE; 352 353 print "Remotehost added successfully\n\n"; 354 } 355 356} 357 358sub statistics { 359 360 my $verbosity = shift @_; 361 362 my %dests = read_dest($DESTS_FILE); 363 my %sources = read_source($SOURCE_FILE); 364 365 &print_verb($verbosity, 1, "--- Statistics ---\n"); 366 &print_verb($verbosity, 1, scalar localtime); 367 &print_verb($verbosity, 1, "\n"); 368 369 my $command = ''; my $r; 370 foreach my $source (sort keys %sources) { 371 372 if (defined $sources{$source}->{'condition'}) { 373 $r = &condition($verbosity, $sources{$source}->{'condition'}); 374 unless ($r == 0) { next; } 375 } 376 377 print 'Source [' . $source . "]\n"; 378 $command = 'du -sh "' . $sources{$source}->{'src'} . '"'; 379 system($command); 380 } 381 382 foreach my $dest (sort keys %dests) { 383 384 my $desten = $dests{$dest}->{'src'}; 385 386 387 if (defined $dests{$dest}->{'condition'}) { 388 $r = condition($verbosity, $dests{$dest}->{'condition'}); 389 unless ($r == 0) { next; } 390 } 391 392 if ($desten->{'type'} eq 'ssh') { 393 $command = 'ssh -p ' . $desten->{'options'}->{'sshport'} . ' -i ~/.ssh/' . 394 $desten->{'options'}->{'key'} . ' ' . $desten->{'user'} . '@' . $desten->{'host'} . 395 ' du -sh "' . $desten->{'path'} . '"'; 396 } elsif($desten->{'type'} eq 'local') { 397 $command = 'du -sh "' . $desten->{'localpath'} . '"'; 398 } else { 399 next; 400 } 401 402 print 'Destination [' . $dest . "]\n"; 403# print "command: $command \n"; 404 system($command); 405 406 } 407 408} 409 410sub do_backup { 411 412 my $verbosity = shift @_; 413 414 415 die "Cannot find directory $PATH_DIR" 416 unless (-d $PATH_DIR); 417 418 die "Cannot find directory $LOG_FOLDER" 419 unless (-d $LOG_FOLDER); 420 421 die "Cannot find file $CONFIG_FILE" 422 unless (-f $CONFIG_FILE); 423 424 die "Cannot find file $SOURCE_FILE" 425 unless (-f $SOURCE_FILE); 426 427 die "Cannot find file $DESTS_FILE" 428 unless (-f $DESTS_FILE); 429 430 die "Cannot find file $BACKUPSET_FILE" 431 unless (-f $BACKUPSET_FILE); 432 433 my @copt = read_config($CONFIG_FILE); 434 my %dests = read_dest($DESTS_FILE); 435 my %sources = read_source($SOURCE_FILE); 436 my %backupsets = read_backupsets($BACKUPSET_FILE); 437 438# print "Sources\n"; 439# print Dumper(\%sources); 440 441 die "Cannot find backupset $BACKUPSET" 442 unless (defined $backupsets{$BACKUPSET} ); 443 444 445 if (defined $options{'growl'} && $GROWLINSTALLED) { 446 Mac::Growl::PostNotification($AppName,"StartBackup","Starting rsyncbackup","Running backupset\n$BACKUPSET", 0); 447 } 448 if (defined $options{'growlnotify'}) { 449 &growlnotify($options{'growlnotify'}, "StartBackup", "rsyncbackup STARTING", "Running backupset\n$BACKUPSET", 0); 450 } 451 452 &write_status($STATUS_FILE,1, $BACKUPSET); 453 454 &print_verb($verbosity, 1, "--- BACKUP START ---\n"); 455 &print_verb($verbosity, 1, scalar localtime); 456 &print_verb($verbosity, 1, "\nBackupset: $BACKUPSET\n\n"); 457 458 459 my $tc = time(); 460 461 foreach my $backupsetentry (@{$backupsets{$BACKUPSET}} ) { 462 463 foreach my $source (@{$backupsetentry->{'sources'}}) { 464 465 foreach my $dest (@{$backupsetentry->{'dests'}}) { 466 my @mergedopts = (@copt, $dests{$dest}->{'opts'}, $sources{$source}->{'opts'}, $backupsetentry->{'opts'}); 467 my @mergedconditions = ($dests{$dest}->{'condition'}, $sources{$source}->{'condition'}, $backupsetentry->{'condition'}); 468# print "DESTTING [$dest] : "; 469# print Dumper($dests{$dest}); 470 atom_backup($verbosity, $sources{$source}->{'src'}, $dests{$dest}->{'src'}, 471 \@mergedopts, \@mergedconditions, $LOG_FOLDER, $source . '_to_' . $dest); 472 473 474 } 475 } 476 } 477 478 $tc = time() - $tc; 479 &print_verb($verbosity, 1, "All backups in this set took " . &datediff($tc) . " seconds\n\n"); 480 481 &write_status($STATUS_FILE, 0, 'None'); 482 if (defined $options{'growl'} && $GROWLINSTALLED) { 483 Mac::Growl::PostNotification($AppName,"FinnishedBackup","rsyncbackup is finnished","Finnished backupset\n$BACKUPSET", 0); 484 } 485 if (defined $options{'growlnotify'}) { 486 &growlnotify($options{'growlnotify'}, "FinnishedBackup", "rsyncbackup FINNISHED", "Finnished backupset\n$BACKUPSET", 0); 487 } 488 489 490} 491 492sub debug_conf { 493 494 my $verbosity = shift @_; 495 496 die "Cannot find directory $PATH_DIR" 497 unless (-d $PATH_DIR); 498 print_verb($verbosity, 2, "PATH DIR:" . $PATH_DIR . "\n"); 499 500 die "Cannot find directory $LOG_FOLDER" 501 unless (-d $LOG_FOLDER); 502 print_verb($verbosity, 2, "LOG DIR:" . $LOG_FOLDER . "\n"); 503 504 die "Cannot find file $CONFIG_FILE" 505 unless (-f $CONFIG_FILE); 506 print_verb($verbosity, 2, "CONFIG_FILE:" . $CONFIG_FILE . "\n"); 507 508 die "Cannot find file $SOURCE_FILE" 509 unless (-f $SOURCE_FILE); 510 print_verb($verbosity, 2, "SOURCE FILE:" . $SOURCE_FILE . "\n"); 511 512 die "Cannot find file $DESTS_FILE" 513 unless (-f $DESTS_FILE); 514 print_verb($verbosity, 2, "DESTS_FILE:" . $DESTS_FILE . "\n"); 515 516 die "Cannot find file $BACKUPSET_FILE" 517 unless (-f $BACKUPSET_FILE); 518 print_verb($verbosity, 2, "BACKUPSET_FILE:" . $BACKUPSET_FILE . "\n"); 519 520 my @copt = read_config($CONFIG_FILE); 521 my %dests = read_dest($DESTS_FILE); 522 my %sources = read_source($SOURCE_FILE); 523 my %backupsets = read_backupsets($BACKUPSET_FILE); 524 525# print "Sources\n"; 526# print Dumper(\%sources); 527 528 die "Cannot find backupset $BACKUPSET" 529 unless (defined $backupsets{$BACKUPSET} ); 530 print_verb($verbosity, 1, "BACKUPSET:" . $BACKUPSET . "\n"); 531 532 print_verb($verbosity, 2, "\n"); 533 534 535 my $counter = 0; 536 537 538 foreach my $backupsetentry (@{$backupsets{$BACKUPSET}} ) { 539 540 foreach my $source (@{$backupsetentry->{'sources'}}) { 541 542 foreach my $dest (@{$backupsetentry->{'dests'}}) { 543 544 my @mergedopts = (@copt, $dests{$dest}->{'opts'}, $sources{$source}->{'opts'}, $backupsetentry->{'opts'}); 545 my @mergedconditions = ($dests{$dest}->{'condition'}, $sources{$source}->{'condition'}, $backupsetentry->{'condition'}); 546 547 &print_verb($verbosity, 1, "Backup set " . ++$counter . " $source to $dest\n"); 548 549 &print_verb($verbosity, 3, "Source : " . $source . "\n"); 550 &print_verb($verbosity, 2, "Source dir : " . prettyprintdest($sources{$source}->{'src'}) . "\n"); 551 &print_verb($verbosity, 3, "Source opts : " . $sources{$source}->{'opts'} . "\n"); 552 &print_verb($verbosity, 3, "Source cond : " . $sources{$source}->{'condition'} . "\n"); 553 554 &print_verb($verbosity, 3, "Destination : " . $dest . "\n"); 555 &print_verb($verbosity, 2, "Destination dir : " . prettyprintdest($dests{$dest}->{'src'}) . "\n"); 556 &print_verb($verbosity, 3, "Destination opts: " . $dests{$dest}->{'opts'} . "\n"); 557 &print_verb($verbosity, 3, "Destination cond: " . $dests{$dest}->{'condition'} . "\n"); 558 559 &print_verb($verbosity, 3, "Config options : " . join(' ', @copt) . "\n"); 560 &print_verb($verbosity, 3, "Backupset opts : " . $backupsetentry->{'condition'} . "\n"); 561 562 &print_verb($verbosity, 2, "All options : " . join(' ', @mergedopts) . "\n"); 563 &print_verb($verbosity, 2, "All conditions : " . join(' ', @mergedconditions) . "\n"); 564 565 &print_verb($verbosity, 2, "\n"); 566 } # end dest iteration 567 568 } # end source iteration 569 570 } # end backupsetentry iteration 571 572} 573 574 575 576 577sub write_status { 578 my ($file, $status, $tag) = @_; 579 open (FILE, '>', $file) || return "Error"; 580 581 print FILE $status . ":" . time() . ":" . $tag . "\n"; 582 close (FILE); 583} 584 585sub read_status { 586 my ($file) = @_; 587 open (FILE, '<', $file) || return "Error"; 588 my $confline = <FILE>; 589 my @carray; 590 unless (@carray = split(/:/, $confline) ) { 591 return "Error"; 592 } 593 close (FILE); 594 my $status = { 595 'status' => $carray[0], 596 'epoch' => $carray[1], 597 'tag' => $carray[2], 598 'secondstime' => time() - $carray[1], 599 'prettytime' => &datediff(time() - $carray[1]) 600 }; 601 if ($status->{'status'} == 1) { 602 print 'rsyncbackup is currently running backup set [' . trim($status->{'tag'}) . ']' . "\n" . 603 " and have been running for " . $status->{'prettytime'} . ".\n"; 604 } else { 605 print "rsyncbackup is currently not running. Last run " . 606 $status->{'prettytime'} . " ago.\n"; 607 } 608} 609 610 611 612 613## READ CONFIGURATION FILES ### 614 615sub read_dest { 616 my ($file) = @_; 617 my %dests; 618 open (FILE, '<', $file) || die "Error [$file]: $!\n"; 619 while (my $confline = <FILE>) { 620 chop($confline); 621 next unless ($confline =~ m/^[^#][^\|]*\|[^\|]+\|[^\|]+\|[^\|]*$/); 622 my @carray = split(/\|/, $confline); 623 next unless ($carray[1] =~ m/^(ssh|local)(\[(.*?)\])?:((.*?)@(.*?):(.*)|(.*))$/); 624 my $src = { 'type' => $1, 'host' => $6, 'user' => $5, 'path' => $7 , 'localpath' => $8 }; 625 if (defined $src->{'path'}) { 626 $src->{'path'} .= ''; #/'; 627 } else { 628 $src->{'path'} = $src->{'localpath'}; # . '/'; 629 } 630 my $src_params = { 631 'incremental' => 0, 632 'key' => 'rsyncbackup', 633 'tag' => 'backup', 634 'sshport' => 22 635 }; 636 if (defined $3) { 637 foreach my $p (split(',', $3)) { 638 next unless ($p =~ m/^(.*)=(.*)$/); 639 $src_params->{$1} = $2; 640 } 641 } 642 $src->{'options'} = $src_params; 643 $dests{$carray[0]} = { 644 'key', $carray[0], 645 'src', $src, 646 'condition', $carray[2], 647 'opts', $carray[3] || '' 648 }; 649 } 650 close (FILE); 651 #print Dumper(\%dests); 652 return %dests; 653} 654 655sub read_source { 656 my ($file) = @_; 657 my %sources; 658 open (FILE, '<', $file) || die "Error [$file]: $!\n"; 659 while (my $confline = <FILE>) { 660 chop($confline); 661 next unless ($confline =~ m/^[^#][^\|]*\|[^\|]+\|[^\|]+\|[^\|]*$/); 662 my @carray = split(/\|/, $confline); 663 my $src; my $src_params; 664 if ($carray[1] =~ m/^(ssh|local)(\[(.*?)\])?:((.*?)@(.*?):(.*)|(.*))$/) { 665 $src = { 'type' => $1, 'host' => $6, 'user' => $5, 'path' => $7 , 'localpath' => $8 }; 666 if (defined $src->{'path'}) { 667 $src->{'path'} .= ''; #'/'; 668 } else { 669 $src->{'path'} = $src->{'localpath'} ; # . '/'; 670 } 671 $src_params = { 672 'key' => 'rsyncbackup', 673 'tag' => 'backup', 674 'sshport' => 22 675 }; 676 if (defined $3) { 677 foreach my $p (split(',', $3)) { 678 next unless ($p =~ m/^(.*)=(.*)$/); 679 $src_params->{$1} = $2; 680 } 681 } 682 $src->{'options'} = $src_params; 683 } else { 684 # We add this section for backward compatibility for config files from before 685 # rsyncbackup 1.0. 686 $src = { 687 'type' => 'local', 688 'path' => $carray[1], 689 'localpath' => $carray[1] 690 } 691 } 692 $sources{$carray[0]} = { 693 'key' => $carray[0], 694 'src' => $src, 695 'condition' => $carray[2], 696 'opts' => $carray[3] || '' 697 }; 698 } 699 close (FILE); 700# print Dumper(\%sources); exit; 701 return %sources; 702} 703 704sub read_backupsets { 705 my ($file) = @_; 706 my %backupsets; 707 open (FILE, '<', $file) || die "Error [$file]: $!\n"; 708 709 my $current_set = 'default'; 710 $backupsets{$current_set} = []; 711 while (my $confline = <FILE>) { 712 chop($confline); 713 if ($confline =~ m/^\s*\[(.*?)\]\s*$/) { 714 $current_set = $1; 715 $backupsets{$current_set} = []; 716 } elsif ($confline =~ m/^[^#][^\|]*\|[^\|]+\|[^\|]+\|[^\|]*$/ ) { 717 my @carray = split(/\|/, $confline); 718 push @{$backupsets{$current_set}}, { 719 'sources', [split(/,/, $carray[0])], 720 'dests', [split(/,/, $carray[1])], 721 'condition', $carray[2], 722 'opts', $carray[3] || '' 723 }; 724 } 725 } 726 close (FILE); 727# print Dumper(\%backupsets); 728 return %backupsets; 729} 730 731sub read_config { 732 my ($file) = @_; 733 my @opts; 734 open (FILE, '<', $file) || print "Error: $!\n" && return undef; 735 while (my $confline = <FILE>) { 736 chop($confline); 737 next unless ($confline =~ m/^[^#].+$/); 738 push @opts, $confline; 739 } 740 close (FILE); 741 return @opts; 742} 743 744sub condition { 745 my ($verbosity, $condition) = @_; 746 #print "Condition: $condition \n"; 747 system($condition); # . ' &2>&1 > /dev/null'); 748 my $retval = $? >> 8; 749 750 #print "Run: $condition \nRetval = $retval \n\n"; 751 752 return $retval; 753} 754 755sub destcommand { 756 my ($verbosity, $dest, $rawcommand) = @_; 757 my $command ; 758 # Check destination type, and add propriate options for ssh handling. 759 if ($dest->{'type'} eq 'ssh') { 760 $command = 'ssh -p ' . $dest->{'options'}->{'sshport'} . 761 ' -i ~/.ssh/' . $dest->{'options'}->{'key'} . ' ' . 762 $dest->{'user'} . '@' . $dest->{'host'} . 763 " '" . $rawcommand . "'"; 764 } elsif($dest->{'type'} eq 'local') { 765 $command = $rawcommand; 766 } 767 print_verb($verbosity, 2, "Command: \n$command \n"); 768 system ($command); 769} 770 771sub prettyprintdest { 772 my $d = shift @_; 773 774 775 if ($d->{'type'} eq 'ssh') { 776 return '[ssh] ' . $d->{'user'} . '@' . $d->{'host'} . ':' . 777 $d->{'path'} . ' [key=' . $d->{'options'}->{'key'} . ',sshport=' . $d->{'options'}->{'sshport'} . ']'; 778 } elsif ($d->{'type'} eq 'local') { 779 return '[local] ' . $d->{'localpath'}; 780 } else { 781 return "[ERROR]: Unknown destination type, should be file or local!"; 782 } 783} 784 785sub atom_backup { 786 my ($verbosity, $source, $dest, $opts, $conds, $log_folder, $tag) = @_; 787 my $logfile = $log_folder . '/last.' . $tag . '.log'; 788 my $errfile = $log_folder . '/last.' . $tag . '.err.log'; 789 790 my $tc = time(); 791 my $deststr = ''; my $sourcestr = ''; 792 793 # Check destination type, and add propriate options for ssh handling. 794 if ($dest->{'type'} eq 'ssh') { 795 $deststr = $dest->{'user'} . '@' . $dest->{'host'} . ':' . $dest->{'path'}; 796 push @$opts, '--rsh="ssh -p ' . $dest->{'options'}->{'sshport'} . ' -l ' . $dest->{'user'} . ' -i '. $ENV{"HOME"} . '/.ssh/' . $dest->{'options'}->{'key'} . '"'; 797 } elsif ($dest->{'type'} eq 'local') { 798 $deststr = $dest->{'localpath'}; 799 } else { 800 warn ("Unknown sourcetype for destination " . $dest->{'type'} . ".\n"); return 1; 801 } 802 803 # Check source type, and add propriate options for ssh handling. 804 if ($source->{'type'} eq 'ssh') { 805 $sourcestr = $source->{'user'} . '@' . $source->{'host'} . ':' . $source->{'path'}; 806 push @$opts, '--rsh="ssh -p ' . $source->{'options'}->{'sshport'} . ' -l ' . $source->{'user'} . ' -i '. $ENV{"HOME"} . '/.ssh/' . $source->{'options'}->{'key'} . '"'; 807 } elsif ($source->{'type'} eq 'local') { 808 $sourcestr = $source->{'localpath'}; 809 } else { 810 warn ("Unknown sourcetype for source " . $source->{'type'} . ".\n"); return 1; 811 } 812 #print "Source string: $sourcestr\n"; 813 #print "Destination string: $deststr\n"; exit; 814 815 if ($source->{'type'} eq 'ssh' and $dest->{'type'} eq 'ssh') { 816 warn ("rsyncbackup cannot backup from a remote source to a remote destination.\n"); return 2; 817 } 818 819 820 &print_verb($verbosity, 1, "Doing backup <" . $tag . ">\n"); 821 822 $0 = "Backup [$tag] testing conditions"; 823 # Checking if all conditions are met.. 824 foreach my $c (@$conds) { 825 my $r = &condition($verbosity, $c); 826 if ($r == 0) { 827 print_verb($verbosity, 2, "Condition met\n"); 828 } else { 829 print_verb($verbosity, 2, "Condition failed: $c\n"); 830 print_verb($verbosity, 1, "Condition failed...\n"); 831 return 1; 832 } 833 } 834 print_verb($verbosity, 1, "All conditions met...\n"); 835 836 # Check if backup is incremental 837 if ($dest->{'options'}->{'incremental'} > 0) { 838 $0 = "Backup [$tag] incrementing"; 839 # Removing the oldest backup increment 840 my $rc = 'test -d "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.' . $dest->{'options'}->{'incremental'} . 841 '" && rm -rf "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.' . $dest->{'options'}->{'incremental'} . '"'; 842 destcommand($verbosity, $dest, $rc); 843 844 # Shift increment register 845 for (my $i = $dest->{'options'}->{'incremental'}; $i > 0; $i--) { 846 $rc = 'test -d "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.' . ($i - 1) . 847 '" && mv "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.' . ($i - 1) . '" ' . 848 '"' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.' . $i . '"'; 849 destcommand($verbosity, $dest, $rc); 850 } 851 $rc = 'mkdir -p "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.0"'; 852 destcommand($verbosity, $dest, $rc); 853 $rc = 'mkdir -p "' . $dest->{'path'} . $dest->{'options'}->{'tag'} . '.1"'; 854 destcommand($verbosity, $dest, $rc); 855 856 $deststr .= $dest->{'options'}->{'tag'} . '.0/'; 857 push @$opts, '--link-dest="../' . $dest->{'options'}->{'tag'} . '.1"'; 858 859 } 860 861 862 # Preparing backup... 863 my $command = 'rsync ' . join(' ', @$opts) . ' "' . $sourcestr . '" "' . $deststr . '" '; 864 unless (defined $options{'no_file_log'}) { 865 $command .= ' > ' . $logfile . ' 2> ' . $errfile; 866 } 867 868 $0 = "Backup [$tag] running"; 869 870 ## EXECUTING BACKUP 871 &print_verb($verbosity, 2, "Command: $command\n"); 872 system($command); 873 874 ## CHECK FOR ERRORS, and send email 875 if (-s $errfile > 0 ) { 876 if (defined $EMAIL) { 877 print_verb($verbosity, 1, "Errors occured. Sending e-mail to " . $EMAIL . "\n"); 878 my $ecommand = 'cat ' . $errfile . ' | mail -s "rsyncbackup [error] ' . $tag . '" ' . $EMAIL; 879 system($ecommand); 880 } else { 881 print_verb($verbosity, 1, "Errors occured. E-mail is not configured, and will not be sent.\n"); 882 } 883 if (defined $options{'growl'} && $GROWLINSTALLED) { 884 Mac::Growl::PostNotification($AppName,"BackupFailed","rsyncbackup failed","Backupset: $BACKUPSET\nSrc-Dst: $tag", 1); 885 } 886 if (defined $options{'growlnotify'}) { 887 &growlnotify($options{'growlnotify'}, "BackupFailed", "rsyncbackup FAILED", "Backupset: $BACKUPSET\nSrc-Dst: $tag", 1); 888 } 889 } else { 890 print_verb($verbosity, 2, "No errors occured.\n"); 891 } 892 893 894 $tc = time() - $tc; 895 &print_verb($verbosity, 1, "Backup of this source took " . &datediff($tc) . " seconds\n\n"); 896 return 0; 897 898} 899