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