mrb-0.3/0002755000000000000000000000000012754317720007034 5ustar mrb-0.3/debian/0002755000000000000000000000000012754317720010256 5ustar mrb-0.3/debian/copyright0000644000000000000000000000164712754317720012217 0ustar This package was Created by Ron on Tue, 9 May 2006 14:10:19 +0930. Copyright (C) 2004, 2006, 2008, 2014, 2016, Ron This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA On Debian systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL-2'. mrb-0.3/debian/rules0000755000000000000000000000157512754317720011344 0ustar #!/usr/bin/make -f # # Copyright 2003 - 2016 Ron Lee. SHELL = /bin/bash export DH_OPTIONS clean: dh_testdir $(RM) gitkeeper.8 dh_clean build: build-indep build-arch: build-indep: dh_testdir pod2man -s 8 gitkeeper gitkeeper.8 install: install-indep install-arch: install-indep: DH_OPTIONS = -i install-indep: dh_testdir dh_testroot dh_prep dh_install mrb gitkeeper usr/bin dh_installman mrb.8 gitkeeper.8 dh_link usr/bin/gitkeeper usr/bin/gk \ usr/share/man/man8/gitkeeper.8 usr/share/man/man8/gk.8 binary: binary-indep binary-arch: binary-indep: DH_OPTIONS = -i binary-indep: install-indep dh_testdir dh_testroot dh_installchangelogs dh_installdocs dh_compress dh_fixperms dh_installdeb dh_gencontrol dh_md5sums dh_builddeb .PHONY: clean build build-arch build-indep \ install install-arch install-indep \ binary binary-arch binary-indep mrb-0.3/debian/control0000644000000000000000000000270012754317720011656 0ustar Source: mrb Section: admin Priority: optional Maintainer: Ron Lee Build-Depends: debhelper (>= 5), perl Standards-Version: 3.9.8.0 Package: mrb Architecture: all Depends: make, rsync Suggests: git, perl-modules, libjson-xs-perl Description: Manage incremental data snapshots with make/rsync mrb is a single, self-documenting, executable makefile, which aims to make trivial the task of maintaining a set of compact, incremental, rsync mirrors of your important (and sometimes rapidly changing) data. . It relies only on the time-hardened industry tools GNU make and rsync. Snapshots may be taken at any opportune interval. Multiple snapshot targets can be configured in a modular fashion, so fast changing data can be separated from static bulk data, with snapshots of each scheduled or triggered on demand, as may be appropriate for each. . This package also includes gitkeeper, which is a tool for easy bidirectional mirroring of 'installed' data files from a remote system to a local git repository. It doesn't actually require you to keep the local files in git, but if you do it supports restoring the remote system to any snapshot from the repository. Likewise it allows you to explicitly configure the owner and access permission that should be applied when pushing to the remote system since they are not preserved by the versions stored in git. . To use gitkeeper you will need to install the suggested perl packages too. mrb-0.3/debian/compat0000644000000000000000000000000212754317720011452 0ustar 5 mrb-0.3/debian/changelog0000644000000000000000000000404012754317720012124 0ustar mrb (0.3) unstable; urgency=medium * Completely rewrite gitkeeper. Since the version in 0.2 was never released, there is no attempt to remain compatible with it. It was an interesting experiment but its shortcomings were too serious to persist with that approach. This one is a much more generic bidirectional mirroring helper, and is not only more powerful, but easier to configure and use too. But the price of this is that it is now written in perl instead of bash, and the configuration is structured JSON instead of simple shell variables. As simple as possible, but no simpler and all that. It is a better suited tool for this job. * 10 year anniversary upload, to close the only bug reported since 0.1 was released, that it didn't build with `dpkg-buildpackage -A`. That's been fixed in git since 2008 though, so we just need to release it. Closes: #831959 -- Ron Lee Mon, 15 Aug 2016 20:18:16 +0930 mrb (0.2) unstable; urgency=low * Added the gitkeeper script. Remote administration and local backups have more in common than might be first expected. * Tweak mrb to permit pulling files from remote sources too. * Sanity check new module names, limiting them to alphanumeric characters and underscore. GNU make is getting more picky about what is allowed in variable names, and it makes sense for us to as well. * Add (more) vim swapfile extensions to the list of example excludes. -- Ron Lee Fri, 4 Jul 2014 04:38:35 +0930 mrb (0.1) unstable; urgency=low * Yes, the world is full of 'simple' backup systems already. And the bts is simply full of old unfixed bugs that point out the errors in their purported simplicity. So here is yet another one, hopefully so simple that it can't possibly ever have (m)any bugs of its own... Hats off especially to Mike Rubel and jw for wanting the right things and stirring this particular broth until my job was simple. -- Ron Lee Tue, 9 May 2006 14:10:19 +0930 mrb-0.3/gitkeeper0000755000000000000000000011012612754317720010740 0ustar #!/usr/bin/perl -w # # Mirror files between a git repository and their 'installed' location. # # This is my latest attempt at having something I don't hate, to manage and # track the history of system configuration files. I don't want much, just # something that is simple enough to be near to foolproof to remember how to # use correctly, even when you haven't used it for months, yet complete and # powerful enough to not handwave away the usual problems with git and rsync # mirroring where ownership and/or file permission is complex and critical. # # It isn't strictly limited to mirroring "configuration files", this attempt # should be generic enough to handle just about anything it makes sense to # pull and push from git, but that was the initial motivation for having it. # # Copyright 2004 - 2016, Ron # This file is distributed under the terms of the GNU GPL version 2. use strict; use utf8; use feature 'unicode_strings'; use Getopt::Long qw( :config gnu_getopt ); use Pod::Usage; use File::Path qw( remove_tree ); use JSON::XS; $ENV{PATH} = '/usr/bin:/bin'; my $conf_file = './gk.conf'; my $conf_set; my $list_hosts; my $create_conf; my $working_dir; my $destdir = ''; my $remote_user; my $dry_run; my $verbose = 0; my $help; GetOptions( 'config|c=s' => sub { (undef,$conf_file) = @_; $conf_set = 1; }, 'list|l' => \$list_hosts, 'init' => \$create_conf, 'chdir|C=s' => \$working_dir, 'destdir=s' => \$destdir, 'user|u=s' => \$remote_user, 'dry-run|n' => \$dry_run, 'verbose|v+' => \$verbose, 'help|?' => \$help ) or pod2usage(-exitval => 2, -message => "Use -? for option details"); # Using perldoc requires perl-doc to be installed, if it isn't then it will just dump # the entire source code out, along with a message telling you to install perl-doc. pod2usage(-exitval => 0, -verbose => 2, -noperldoc => 1) if $help && $verbose; pod2usage(-exitval => 0, -verbose => 1) if $help; sub read_config($) { #{{{ my $file = shift; die "$0: No '$file' configuration file found.\n" unless -e $file; open(my $h, '<', $file) or die "$0: Failed to read '$file': $!\n"; my $data = do { local $/; <$h> }; close $h; my $conf = eval { JSON::XS->new->utf8->relaxed->decode($data) }; die "$0: Failed to parse '$file': $@\n" if $@; return $conf; } #}}} sub get_host_opts($$) { #{{{ my ($c, $h) = @_; my %o; $o{host} = $h; $o{address} = $c->{hosts}{$h}{address}; $o{pull_pre_command} = $c->{hosts}{$h}{pull_pre_command} // $c->{pull_pre_command}; $o{pull_post_command} = $c->{hosts}{$h}{pull_post_command} // $c->{pull_post_command}; $o{push_pre_command} = $c->{hosts}{$h}{push_pre_command} // $c->{push_pre_command}; $o{push_post_command} = $c->{hosts}{$h}{push_post_command} // $c->{push_post_command}; $o{local_root} = $c->{hosts}{$h}{local_root} // $c->{local_root}; $o{remote_root} = $c->{hosts}{$h}{remote_root} // $c->{remote_root}; $o{remote_user} = $c->{hosts}{$h}{remote_user} // $c->{remote_user}; $o{rsync_opts} = $c->{hosts}{$h}{rsync_opts} // $c->{rsync_opts}; $o{rsync_pull_opts} = $c->{hosts}{$h}{rsync_pull_opts} // $c->{rsync_pull_opts}; $o{rsync_push_opts} = $c->{hosts}{$h}{rsync_push_opts} // $c->{rsync_push_opts}; $o{rsync_include} = $c->{hosts}{$h}{rsync_include} // $c->{rsync_include}; $o{rsync_exclude} = $c->{hosts}{$h}{rsync_exclude} // $c->{rsync_exclude}; $o{rsync_filter} = $c->{hosts}{$h}{rsync_filter} // $c->{rsync_filter}; $o{rsync_pull_filter} = $c->{hosts}{$h}{rsync_pull_filter} // $c->{rsync_pull_filter}; $o{rsync_push_filter} = $c->{hosts}{$h}{rsync_push_filter} // $c->{rsync_push_filter}; $o{chown} = $c->{hosts}{$h}{chown} // $c->{chown}; $o{chmod} = $c->{hosts}{$h}{chmod} // $c->{chmod}; die "$0: No address configured for host '$h'\n" unless $o{address}; die "$0: No sync sets configured for host '$h'\n" unless $c->{hosts}{$h}{sync} && @{$c->{hosts}{$h}{sync}}; return \%o; } #}}} sub get_sync_opts($$$) { #{{{ my ($h, $s, $n) = @_; my %o = %{$h}; $o{local_root} = $s->{local_root} if defined $s->{local_root}; $o{remote_root} = $s->{remote_root} if defined $s->{remote_root}; $o{remote_user} = $s->{remote_user} if defined $s->{remote_user}; $o{remote_user} = $remote_user if $remote_user; $o{rsync_opts} = $s->{rsync_opts} if defined $s->{rsync_opts}; $o{rsync_pull_opts} = $s->{rsync_pull_opts} if defined $s->{rsync_pull_opts}; $o{rsync_push_opts} = $s->{rsync_push_opts} if defined $s->{rsync_push_opts}; $o{rsync_include} = $s->{rsync_include} if defined $s->{rsync_include}; $o{rsync_exclude} = $s->{rsync_exclude} if defined $s->{rsync_exclude}; $o{rsync_filter} = $s->{rsync_filter} if defined $s->{rsync_filter}; $o{rsync_pull_filter} = $s->{rsync_pull_filter} if defined $s->{rsync_pull_filter}; $o{rsync_push_filter} = $s->{rsync_push_filter} if defined $s->{rsync_push_filter}; $o{chown} = $s->{chown} if defined $s->{chown}; $o{chmod} = $s->{chmod} if defined $s->{chmod}; $o{paths} = $s->{paths}; # local_root must not be empty, a local directory must be specified. # remote_root may be empty, to use the home dir of the remote user, # but it must be set to the empty string for that, to avoid accidents. die "$0: No local_root configured for '$o{host}' sync set $n\n" unless $o{local_root}; die "$0: local_root is not a relative path ($o{host}: $o{local_root})\n" unless substr($o{local_root},0,1) ne '/'; die "$0: No remote_root configured for '$o{host}' sync set $n\n" unless defined $o{remote_root}; die "$0: No paths configured for '$o{host}' sync set $n\n" unless $o{paths} && @{$o{paths}}; for my $p (@{$o{paths}}) { die "$0: sync path for '$o{host}' is not relative ($p)\n" unless substr($p,0,1) ne '/'; } die "$0: local_root path '$o{local_root}' does not exist.\n" unless -e $o{local_root}; die "$0: local_root path '$o{local_root}' is not a directory.\n" unless -d $o{local_root}; $o{local_root} .= '/' unless substr($o{local_root},-1) eq '/'; $o{remote_root} .= '/' if $o{remote_root} && substr($o{remote_root},-1) ne '/'; $o{remote_root} = "$destdir/$o{remote_root}" if $destdir; $o{remote_host} = $o{remote_user} ? "$o{remote_user}\@$o{address}" : $o{address}; return \%o; } #}}} sub maybe_exec($$$) { #{{{ my ($host, $label, $cmd) = @_; return unless $cmd && @$cmd; if ($dry_run) { print " (not executing) $label: `" . join(' ', map { "'$_'" } @$cmd) . "`\n"; } else { print " $label: `" . join(' ', map { "'$_'" } @$cmd) . "`\n" if $verbose; system( @$cmd ) == 0 or die "$0: $label for '$host' failed\n"; } } #}}} sub do_pull($$) { #{{{ my ($conf, $host) = @_; my $host_opt = get_host_opts($conf, $host); my $n = 0; print "Pulling '$host' from $host_opt->{address} ...\n"; maybe_exec($host, 'pre-command', $host_opt->{pull_pre_command}); for my $s (@{$conf->{hosts}{$host}{sync}}) { my $so = get_sync_opts($host_opt, $s, ++$n); my @rsync_cmd = ( 'rsync', '--relative' ); push @rsync_cmd, @{$so->{rsync_opts}} if $so->{rsync_opts}; push @rsync_cmd, @{$so->{rsync_pull_opts}} if $so->{rsync_pull_opts}; push @rsync_cmd, map { '--include=' . $_ } @{$so->{rsync_include}} if $so->{rsync_include}; push @rsync_cmd, map { '--exclude=' . $_ } @{$so->{rsync_exclude}} if $so->{rsync_exclude}; push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_filter}} if $so->{rsync_filter}; push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_pull_filter}} if $so->{rsync_pull_filter}; push @rsync_cmd, '--dry-run' if $dry_run; push @rsync_cmd, '--'; push @rsync_cmd, map { "$so->{remote_host}:$so->{remote_root}./$_" } @{$so->{paths}}; push @rsync_cmd, $so->{local_root}; print " " . join(' ', map { "'$_'" } @rsync_cmd) . "\n" if $verbose > 1; system( @rsync_cmd ) == 0 or die "$0: rsync pull for '$host' failed\n"; } maybe_exec($host, 'post-command', $host_opt->{pull_post_command}); print "Pulling '$host' done.\n\n"; } #}}} sub do_push($$) { #{{{ my ($conf, $host) = @_; my $host_opt = get_host_opts($conf, $host); my $n = 0; print "Pushing '$host' to $host_opt->{address} ...\n"; maybe_exec($host, 'pre-command', $host_opt->{push_pre_command}); for my $s (@{$conf->{hosts}{$host}{sync}}) { my $so = get_sync_opts($host_opt, $s, ++$n); my @rsync_cmd = ( 'rsync', '--relative' ); push @rsync_cmd, @{$so->{rsync_opts}} if $so->{rsync_opts}; push @rsync_cmd, @{$so->{rsync_push_opts}} if $so->{rsync_push_opts}; push @rsync_cmd, map { '--include=' . $_ } @{$so->{rsync_include}} if $so->{rsync_include}; push @rsync_cmd, map { '--exclude=' . $_ } @{$so->{rsync_exclude}} if $so->{rsync_exclude}; push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_filter}} if $so->{rsync_filter}; push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_push_filter}} if $so->{rsync_push_filter}; push @rsync_cmd, '-og', '--chown=' . $so->{chown} if $so->{chown}; push @rsync_cmd, '-p', '--chmod=' . $so->{chmod}, if $so->{chmod}; push @rsync_cmd, '--dry-run' if $dry_run; push @rsync_cmd, '--'; # rsync expects local wildcards to be expanded "by the shell", but since # we aren't passing through the shell here, we need to glob them ourself. # The nested quoting avoids glob splitting the pattern on whitespace. push @rsync_cmd, map { glob qq("$so->{local_root}./$_") } @{$so->{paths}}; push @rsync_cmd, "$so->{remote_host}:$so->{remote_root}"; print " " . join(' ', map { "'$_'" } @rsync_cmd) . "\n" if $verbose > 1; system( @rsync_cmd ) == 0 or die "$0: rsync push for '$host' failed\n"; } maybe_exec($host, 'post-command', $host_opt->{push_post_command}); print "Pushing '$host' done.\n\n"; } #}}} sub git_check_ref_format($) { #{{{ # We could check this ourselves with a regex, but just punt to using the # git check-ref-format test. The cost of shelling out to that here is # minimal, and the rules have changed at least once before in the past, # so just let the version of git we have to use decide. # # We place one extra restriction on what we will accept, disallowing a # single quote anywhere in the refname, which lets us be lazy with what # is needed to quote it for the export. We can fix that if needed, but # there aren't many sane reasons to use them in a refname anyway. my $ref = shift; system( 'git', 'check-ref-format', '--allow-onelevel', $ref ); return 0 if $?; return 0 if $ref =~ /'/; return 1; } #}}} if ($working_dir) { #{{{ chdir($working_dir) or die "$0: Failed to change to directory '$working_dir': $!\n"; } #}}} if ($create_conf) { #{{{ die "$0: Configuration file '$conf_file' already exists.\n" . " *** Refusing to overwrite it.\n\n" if -e $conf_file; my $new_conf = <', $conf_file) or die "$0: Failed to create '$conf_file': $!\n"; print $h $new_conf; close $h; print "\n Created skeleton configuration in '$conf_file'.\n\n"; exit 0; } #}}} my $command = $ARGV[0]; if (($command // '') eq "export") { #{{{ my $ref = $ARGV[1] // ''; my $host = $ARGV[2] // ''; my $export_dir = './z-export'; pod2usage(-exitval => 1, -verbose => 0, -message => "Error: export command requires a git ref.\n") unless $ref; git_check_ref_format($ref) or die "$0: Invalid git ref '$ref'\n"; # mkdir will fail if this is true, but explain why we bail out in this case. die "$0: git export dir '$export_dir' already exists.\n" . " *** Refusing to overwrite it.\n\n" if -e $export_dir; mkdir( $export_dir, 0700 ) or die "$0: Failed to create git export dir '$export_dir': $!\n"; print " Exporting '$ref' to '$export_dir' ...\n" if $verbose; if (system( "git archive --format=tar '$ref' | tar -C '$export_dir' -xf -" )) { remove_tree( $export_dir ); die "$0: Failed to export git ref '$ref'\n"; } my @gk_cmd = ( $0, '--chdir', $export_dir ); push @gk_cmd, '--config', $conf_file if $conf_set; push @gk_cmd, '--list' if $list_hosts; push @gk_cmd, '--destdir', $destdir if $destdir; push @gk_cmd, '--user', $remote_user if $remote_user; push @gk_cmd, '--dry-run' if $dry_run; push @gk_cmd, '-' . ('v' x $verbose) if $verbose; push @gk_cmd, 'push'; push @gk_cmd, $host if $host; print " " . join(' ', map { "'$_'" } @gk_cmd) . "\n" if $verbose; if (system( @gk_cmd )) { remove_tree( $export_dir ); die "$0: Failed to push export of git ref '$ref'\n"; } remove_tree( $export_dir ); exit 0; } #}}} if ($list_hosts) { #{{{ my $c = read_config($conf_file); if ($c->{hosts} && %{$c->{hosts}}) { print "\n Available hosts:\n"; for my $h (sort keys %{$c->{hosts}}) { print " $h:\t" . ($c->{hosts}{$h}{address} // '(No address set)') . "\n"; } print "\n"; } else { print "\n No hosts configured in '$conf_file'\n\n"; } exit 0; } #}}} pod2usage(-exitval => 1, -verbose => 0, -message => "Error: A command (push/pull/export) is required.\n") unless $command; my $conf = read_config($conf_file); my @hosts; if ($ARGV[1]) { die "$0: Error, no host '$ARGV[1]' defined in '$conf_file'.\n" unless exists $conf->{hosts}{$ARGV[1]}; push @hosts, $ARGV[1]; } else { die "$0: Error, no hosts section defined in '$conf_file'.\n" unless exists $conf->{hosts}; @hosts = sort keys %{$conf->{hosts}}; } if ($command eq "pull") { for my $h (@hosts) { do_pull($conf, $h); } exit 0; } if ($command eq "push") { for my $h (@hosts) { do_push($conf, $h); } exit 0; } pod2usage(-exitval => 1, -verbose => 0, -message => "Error: Unknown command '$command'.\n"); __END__ =head1 NAME gitkeeper - Mirror files between git and an installed location. =head1 SYNOPSIS B I<[options]> B I<[host]> B I<[options]> B I<[host]> B I<[options]> B I I<[host]> =head1 DESCRIPTION B is a remote administration aid. It enables configuration files to be maintained locally, with a full history of changes, and synchronised on demand with a remote target system. This allows files to still be altered directly on the running system, if and/or when that is needed, with a simple method to get that all back in sync with the archived copy again later. It uses B(1) and B(1) for all operations on the remote hosts. No special configuration beyond permission to use them is required. The use of an B for managing remote logins may be an advantage though. At its core, B is really just a tool for managing bidirectional mirrors, of potentially sparse segments of the remote filesystem, so it's not strictly limited to being used for configuration files, nor is it strictly dependent upon B(1) aside from the B option, but those are the primary uses that it was initially designed for. =head1 COMMANDS =over 8 =item B Pull files from the remote system into the local mirror. This will update the local directory content to match the live system, but it will not commit any files to git or change the local index state in any way. If you wish to commit changes imported in this way, you can just do that with normal git operations. If the I parameter is not explicitly specified, then all defined hosts will be pulled. =item B Push files from the local mirror to the remote system. This will push the state of the current working directory, regardless of whether the repository tree is currently clean or dirty. If the I parameter is not explicitly specified, then all defined hosts will be pushed. =item B Push files from a historical snapshot of the local mirror to the remote system. This will do a B(1) export of the given I to a temporary directory, and then perform a B operation on that tree. It is equivalent to doing a B of the desired ref, and then doing a B on that tree state, except it will respect any B you have set for what will be exported, and it will not change the current working directory state. If the I parameter is not explicitly specified, then all hosts defined in the exported configuration will be pushed. =back =head1 OPTIONS =over 8 =item B<-c, --config> I The file describing what should be mirrored and how. If not specified then F will be looked for in the directory that B is invoked in. Usually this should be a relative path under that location, but an absolute path is permitted and may be useful in some circumstances. If the B command is used, then this file is not read until after the export from git, and relative paths are resolved to files in the exported directory. This is usually what you want since the configuration from the exported snapshot will then be used. If you need to override that for some reason then you can use an absolute path to an alternate configuration file. =item B<-l, --list> Show the list of host aliases defined in the configuration file. If this is used with the B command, then the configuration of the exported snapshot will be shown. =item B<--init> Create a skeleton configuration file. This is a convenience to get an initial configuration when bootstrapping a new mirror. =item B<-C, --chdir> I Change to the given directory before running B. Normally you would just run it from the top level directory of the mirror, but this permits use from elsewhere in a similar way to what C allows. =item B<--destdir> I Prefix the remote paths to an alternate file system root. This always changes only the remote path, regardless of whether a B or B operation is being performed. It acts like the C option for C and allows mirroring files to and from an alternative filesystem location but with the same subdirectory structure as what they would normally have. You can use this to export files to a chroot, or to a temporary directory somewhere, so that they can be examined without replacing the real files on the remote system. =item B<-u, --user> Override the B option from the configuration file for access to the remote host. There probably aren't many good reasons to ever use this option, it's a pretty blunt hammer which will override it everywhere, but it may be useful for exporting a mirror to some machine or location where it isn't usually expected to go. =item B<-n, --dry-run> Don't actually copy any files, just show what would be done if this was a live run. If this is used with the B command, then the dry run will be performed on the requested snapshot. =item B<-v, --verbose> Show more detail about what is being done. This option may be passed multiple times to increase the level of verbosity even further. If passed along with B<--help> then more verbose documentation will be shown. =item B<-?, --help> Show this help, again. =back =head1 CONFIGURATION The B configuration file is expected to contain a single JSON Object, which will be parsed by perl's JSON::XS in its relaxed mode (which allows trailing commas after the final element of an array or object, and '#'-comments anywhere that whitespace would be permitted). =head2 Global options There is only one required member of the top level object, though other options may also be specified there to be inherited as defaults if not overridden for a host or one of its sync sets. =over 8 =item B The B member defines a JSON object in which each member is a host name alias that may be passed as the I parameter to B. The alias names are not used for any other purpose than as the I identifier, and may be any JSON string value. No other options may be included directly in the B section. =back { "hosts": { "host1": { ... }, "host2": { ... } } } =head2 Per-host options There are two required members which must be specified directly for each host alias object. Other options may also be specified there which will override a global default for that host and be inherited as defaults for its sync sets, if not also overridden there. =over 8 =item B
The B
member is a JSON string value, that defines the hostname or IP address used when connecting to the remote system. It must be a valid string that can be passed as the host part of a remote B(1) path. It should not contain a I part (that should instead be set with the B option), but may contain a port specification. =item B The B member is a JSON array of objects. It must contain at least one object, but there is no upper bound to the number which may be included. Each of the objects in the B array define a mapping from a B to a B, the paths which will be mirrored under those roots, and the B options which will be applied when transferring them. =back "host1": { "address": "myhost.mydomain.org", "sync": [ { ... }, { ... } ] } =head2 Sync set options Each object in the B array has one required member that must be specified directly in it. Other options may also be specified there which will override the global and host defaults, and some of those options must also be defined in at least one of those places for each sync set. =over 8 =item B The B member is an array of JSON string values which specify the files and/or directories under the B which will be mirrored with their full directory structure. They may contain shell wildcards, but cannot contain brace expansions if the B option is used. They must all be relative paths (ie. they must not begin with a '/'). =back "sync": [ { "paths": [ "file1", "dir1", "dir2/subdir", "dir3/*.conf" ] } ] =head2 Required options The following options must be defined for every sync set, though they may be configured in either the top level object as global defaults, in the host alias object for per-host defaults, or in the sync object itself. =over 8 =item B A JSON string value that defines the local directory which remote B will be mirrored under. This must be a relative path, which itself is rooted to the directory under which B is invoked. As a sanity check against accidents, this directory must already exist. =item B A JSON string value which defines the location on the remote system that the specified B are relative to. This may be an absolute or relative path. A relative path will be rooted to the home directory of the B. A value of "" may be used to specify the home directory of the B. =back =head2 Additional options The following options may be defined as global or per-host defaults, or set explicitly in each sync set. It is not an error for them not to be set, and a higher level default may be 'unset' by overriding it with an empty value. =over 8 =item B A JSON string which defines the username to use for access to the remote host. If not set, then the ssh default for the remote system will be used (as configured by .ssh/config or similar). =item B A JSON array of string values containing options to be passed to all invocations of B, for both B and B operations. No word splitting or shell quote stripping is done on the values used here, so each option must be its own array element. Note that the B<--relative> option is passed to B(1) by default for all invocations and does not need to be included in this set. If you really don't want that option for some reason, and understand the consequences of not passing it for this use, you can disable it with B<--no-relative>, but there's probably no good reason to ever do that here. "rsync_opts": [ "--prune-empty-dirs", "--delete-excluded", "--filter=protect .s[a-w][a-z]" ] =item B Similar to B above, but options specified in this array are appended to those only for B operations. =item B Similar to B above, but options specified in this array are appended to those only for B operations. =item B A JSON array of string values which will be passed to B(1) as B<--include> options. This is a convenience which is eqivalent to adding those to B ie. the following configurations would be identical in their operation if no other ordering constraints for the filter rules applied. "rsync_opts": [ "--include=.s[a-w][a-z]/" ] "rsync_include": [ ".s[a-w][a-z]/" ] =item B A JSON array of string values which will be passed to B(1) as B<--exclude> options. This is the same as B above, except for excludes. =item B A JSON array of string values which will be passed to B(1) as B<--filter> options. This is similar to the include and exclude options above, except it allows the full range of B filter rules to be used. =item B A JSON array of string values which will be passed to B(1) as B<--filter> options (in addition to the include, exclude, and filter options above) only for B operations. =item B A JSON array of string values which will be passed to B(1) as B<--filter> options (in addition to the include, exclude, and filter options above) only for B operations. =item B A JSON string value which will be passed to B as the B<--chown> option for B operations to set file and directory ownership on the remote host. If this option is used, the B<--owner> and B<--group> options will automatically added too, otherwise it would have no effect. You must have superuser privilege on the remote host for this to work. "chown": "root:bind" =item B A JSON string value which will be passed to B as the B<--chmod> option for B operations to set file and directory permissions on the remote host. If this option is used, the B<--perms> option will automatically added too, otherwise it would have no effect. Valid values here are anything that the B option would accept. "chmod": "D2755,F664" =back =head2 Pre- and Post- command hooks The following options may be used to execute arbitrary commands before and/or after a B or B operation. The commands are executed on the local host, in the directory that B was invoked in, as the user which B was invoked as. They can be used to perform operations on the remote host by simply invoking B(1) or similar themselves. =over 8 =item B =item B =item B =item B A JSON array of string values containing the command to execute and the options to pass to it. This will be passed as an array to the perl B command, so if the array contains multiple elements, then no word splitting or other shell interpretation will be performed. If it is a single string, then it will instead be passed to the local shell, with all the caveats that accompany doing that. If the pre-command fails, then no transfer will take place. If the transfer fails for some reason then the post-command will not be executed. That might change later if we let this get more complex and begin passing status and other variables to the commands that are invoked, but at this stage, that isn't really needed for any current use we have, so I'm not going to complicate things now in anticipation of what later uses might require. =back =head1 FILES =over 8 =item B<./gk.conf> The default configuration file. =back =head1 SEE ALSO B(1), B(1), B(1), B(1). =head1 AUTHOR B was written by Ron . =cut # vi:sts=4:sw=4:et:foldmethod=marker mrb-0.3/mrb.80000644000000000000000000001055412754317720007710 0ustar .TH MRB 8 "May 9, 2006" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME mrb \- manage incremental snapshots with rsync/make. .SH SYNOPSIS .B mrb .I command .SH DESCRIPTION .B mrb is a simple aid to creating efficient incremental snapshots of a set, or sets, of directories whenever that may be required. It may be used as part of a regular automated backup regime, or for manually checkpointing changes at convenient points in time. .SH COMMANDS The following commands are recognised (where '\fIMODULE\fP' is the name of one of your snapshot definitions): .TP .BI new\- MODULE Create a skeleton definition for a new snapshot '\fIMODULE\fP'. .TP .BI dest\- MODULE Create the destination dir for '\fIMODULE\fP'. This directory must exist to create a snapshot. .TP .BI snap\- MODULE Create a snapshot of '\fIMODULE\fP'. .TP .B sync Create snapshots of all defined modules. If run as root this may be configured to include the modules of other users too (see MRB_SYNC_USERS in ~/.mrb/defaults). .TP .B help Show mrb's own help text. .SH CONFIGURATION FILES .SS Per-user configuration The following files may be used to specify global and local configuration options. .TP .I /etc/default/mrb system default configuration. .TP .I ~/.mrb/defaults per-user configuration. .SS Per-user options The following options control behaviour for all of a user's modules. .TP .B MRB_SNAPSHOT_LOG An optional file path where transfer details will be recorded. If unset these details will not be logged. .TP .B MRB_SYNC_USERS A space separated list of users whose modules should be included in a \fBsync\fP. This is mostly only useful for root, as mrb will assume the identity of each user before creating snapshots of their modules. If unset, only the invoking user's modules will be sync'ed. .TP .B MRB_CONFDIR An space separated list of the directories to search for module definition (*.mrc) files. They will be searched in the order given, with new modules added by default to the last one listed. There should be few reasons to change the default value. .SS Per-module configuration The default \fBMRB_CONFDIR\fP value will search for module definitions in: .TP .I /etc/mrb/*.mrc .TP .I ~/.mrb/*.mrc Those created by .BI new\- MODULE will be placed in this latter location by default. .SS Per-module options In each case \fImodule\fP below is the name of the particular module that the value set should apply to. These options should be defined in a file named \fImodule\fP.\fBmrc\fP. .TP .B module_SRC A space separated list of the files and (top level) directories to include in the snapshots for this module. These may be remote urls. .TP .B module_DEST The (local) directory root where snapshots of \fImodule\fP should be stored. .TP .B module_INCLUDE An optional list of .BR rsync (1) include patterns. .TP .B module_EXCLUDE An optional list of .BR rsync (1) exclude patterns. .TP .B module_FILTER An optional list of .BR rsync (1) filter patterns. .TP .B module_FILTER_FILE An optional filename for .BR rsync (1) dir-merge filtering support. .TP .B module_RSYNC_OPTIONS Optional additional .BR rsync (1) options to pass verbatim when it is invoked. .TP .B module_PRECOMMAND An optional shell command to invoke just prior to creating a new snapshot. If the command does not return a successful exit status, then the snapshot creation will be aborted before it begins. It may be used to mount removable media or similar. .TP .B module_POSTCOMMAND An optional shell command to execute after making the snapshot. It will not be called if the snaphot creation failed at an earlier stage, and its return status may halt a sync operation if it fails with more modules still to process. It may be used, for example, to unmount removable media again. .TP .B module_USER An optional user name to check that mrb is running as before performing a snapshot. This can be used to ensure you have the correct permisson to access the files being mirrored before you get too far. .SH SEE ALSO .BR rsync (1), .BR make (1). .SH AUTHOR mrb was written by Ron . mrb-0.3/mrb0000755000000000000000000003675212754317720007555 0ustar #!/usr/bin/make -f # # mrb make/rsync backup system # # Copyright 2006 - 2014, Ron Lee. # This file is distributed under the terms of the GNU GPL version 2. # # WARNING: Misconfigured and/or buggy backup systems can easily # DESTROY YOUR PRECIOUS DATA # No warranty is, or can be, made against this happening # to you. Your only protection is your own care and # vigilance. This script is very simple, and should be # reasonably easy to audit for correctness according to # your particular needs. You are strongly encouraged to # carefully test any new configuration, or release of it, # before deploying it on your only copy of critical data. # # I may be a nice guy, and you may not even feel the need # to lock up your daughters or other valuables, but don't # trust me with your data. Please. It's too flimsy to # leave in the hands of someone whose attention is busy # enough worrying about their own. Your help to make sure # nobody will ever have to read this warning 'in anger' # will be greatly appreciated. So, on to the good bits ... # # ------------------------------------------------------------------- # Configurable fundamentals. # A couple of simple axioms to kick things off. override THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST)) SHELL = /bin/bash GLOBAL_CONFDIR = /etc USER_CONFDIR = ~/.mrb USER_DEFAULTS = $(USER_CONFDIR)/defaults # Permit an expert user to reconfigure MRB_CONFDIR from an external file. # There probably isn't much else you'd want to tweak there, but read on # (carefully) if you really feel the need... -include $(GLOBAL_CONFDIR)/default/mrb -include $(USER_DEFAULTS) # The directories to search for module definitions. # A space separated list if multiple. They are included in the order # listed, but new modules will be added to only in the last one provided # by default. MRB_CONFDIR ?= $(GLOBAL_CONFDIR)/mrb $(USER_CONFDIR) -include $(addsuffix /*.mrc,$(MRB_CONFDIR)) # Some basic environment for the operations to come. USER ?= $(shell id -un) # No need for --delete if we are linking into a barren dir. RSYNC = rsync RSYNC_OPTIONS = --super -ahivS DATESTAMP := $(shell date +%Y%m%d-%H.%M) # ------------------------------------------------------------------- # The rules. # Default target explains how to press this button again properly. default: help # Convenience target for creating the skeleton of a new module. new-%: NEW_FILE = $(lastword $(MRB_CONFDIR))/$*.mrc new-%: @if [ -z "$(MRB_CONFDIR)" ]; then \ echo; echo " Error: MRB_CONFDIR unset? Aborting."; echo; \ exit 1; \ fi @if [[ "$*" =~ [^[:alnum:]_] ]]; then \ echo; \ echo " Error: Invalid Module name. Names must consist of";\ echo " only alphanumeric characters or underscore.";\ echo; \ exit 1; \ fi @mkdir -p $(dir $(NEW_FILE)) @if [ -e $(NEW_FILE) ]; then \ echo; echo " Module '$*' already exists. Stopping."; echo;\ exit 1; \ fi @if [ ! -e $(USER_DEFAULTS) ]; then \ echo; \ echo " Creating $(USER_DEFAULTS)"; \ echo; \ echo "Edit this file to configure logging of transfers, or";\ echo "enable root 'sync' operations to create snapshots of";\ echo "other users' modules also."; \ \ echo "# This file configures mrb default settings for '$(USER)'" > $(USER_DEFAULTS);\ echo "# It was created on $(shell date) by $(THIS_MAKEFILE)" >> $(USER_DEFAULTS);\ echo >> $(USER_DEFAULTS); \ echo "# An optional file path to log transfer details to." >> $(USER_DEFAULTS);\ echo "MRB_SNAPSHOT_LOG = ~/.mrb/snapshot.log" >> $(USER_DEFAULTS);\ echo >> $(USER_DEFAULTS); \ echo "# A space separated list of users to include in a 'sync'." >> $(USER_DEFAULTS);\ echo "# This is typically only useful for the root user, as" >> $(USER_DEFAULTS);\ echo "# the identity of each user listed here is assumed when" >> $(USER_DEFAULTS);\ echo "# performing the sync of their modules." >> $(USER_DEFAULTS);\ echo "#MRB_SYNC_USERS = " >> $(USER_DEFAULTS); \ echo >> $(USER_DEFAULTS); \ fi @echo @echo " Creating skeleton $(NEW_FILE) ..." @echo "# This is the configuration file for the mrb snapshot module '$*'." > $(NEW_FILE) @echo "# It was created on $(shell date) for $(USER)" >> $(NEW_FILE) @echo "# by $(THIS_MAKEFILE)" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# A space separated list of the source files and directories to be" >> $(NEW_FILE) @echo "# included in snapshots of this module. These may be remote urls." >> $(NEW_FILE) @echo "# Specifying this is mandatory." >> $(NEW_FILE) @echo "#$*_SRC = " >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# The (local) directory root where the snapshots should be stored." >> $(NEW_FILE) @echo "# Specifying this is mandatory." >> $(NEW_FILE) @echo "#$*_DEST = " >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# A pattern list for files under '$*_SRC' to explicitly include" >> $(NEW_FILE) @echo "# in the snapshot. Specifying this is optional." >> $(NEW_FILE) @echo "# See rsync(1) for syntax details." >> $(NEW_FILE) @echo "#$*_INCLUDE = *.o/ *.d/ core/ *.a/ *.dll/ *.mo/ *.lo/ *.la/ *.so/ \\" >> $(NEW_FILE) @echo "# *.init.d .*.s[a-w][a-z]/ .s[a-w][a-z]/" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# A pattern list for files under '$*_SRC' to explicitly exclude" >> $(NEW_FILE) @echo "# from the snapshot. Specifying this is optional." >> $(NEW_FILE) @echo "# See rsync(1) for syntax details." >> $(NEW_FILE) @echo "#$*_EXCLUDE = *.o *.d core *.a *.dll *.gch *.mo *.lo *.la .libs/ \\" >> $(NEW_FILE) @echo "# *.so *.so.[0-9] *.so.[0-9].[0-9] *.so.[0-9].[0-9].[0-9] \\" >> $(NEW_FILE) @echo "# .*.s[a-w][a-z] .s[a-w][a-z]" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# An rsync filter rule to apply for the snapshot." >> $(NEW_FILE) @echo "# Specifying this is optional. See rsync(1) for syntax details." >> $(NEW_FILE) @echo "#$*_FILTER = " >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# The file-name for an rsync per-directory filter to use if found." >> $(NEW_FILE) @echo "# Specifying this is optional. See rsync(1) for syntax details" >> $(NEW_FILE) @echo "# of 'dir-merge' filters. This is the name the file must match." >> $(NEW_FILE) @echo "# The default given here will scan all dirs from $*_SRC down." >> $(NEW_FILE) @echo "# Remove the leading '/' to search only the actual directory" >> $(NEW_FILE) @echo "# that is being transferred." >> $(NEW_FILE) @echo "#$*_FILTER_FILE = /.mrb-rsync-filter" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# Optional additional rsync options to pass verbatim." >> $(NEW_FILE) @echo "#$*_RSYNC_OPTIONS = --relative --prune-empty-dirs" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# An optional shell command to execute before making the snapshot." >> $(NEW_FILE) @echo "# If the command does not return a successful exit status, then the" >> $(NEW_FILE) @echo "# snapshot creation will be aborted before it begins." >> $(NEW_FILE) @echo "# It may be used to mount $*_DEST on removable media or similar." >> $(NEW_FILE) @echo "#$*_PRECOMMAND = mount /mnt/backup" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# An optional shell command to execute after making the snapshot." >> $(NEW_FILE) @echo "# It will not be called if the snaphot creation failed at an earlier" >> $(NEW_FILE) @echo "# stage. It's exit status will be passed to the user, but nothing" >> $(NEW_FILE) @echo "# remains to be done with it at this level." >> $(NEW_FILE) @echo "# It may be used, for example, to unmount removable media again." >> $(NEW_FILE) @echo "#$*_POSTCOMMAND = umount /mnt/backup" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# An optional user name to check before performing a snapshot." >> $(NEW_FILE) @echo "# This can be used to ensure you have the correct permisson to" >> $(NEW_FILE) @echo "# access the files being mirrored before you get too far." >> $(NEW_FILE) @echo "$*_USER = $(USER)" >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo >> $(NEW_FILE) @echo "# Do not remove or edit this entry, it is used internally by mrb." >> $(NEW_FILE) @echo "$*_CONFIG = \$$(lastword \$$(MAKEFILE_LIST))" >> $(NEW_FILE) @echo @echo "Please edit this file now to define the details of the" @echo "snapshot which you would like it to create." @echo # Some basic checks shared by multiple operations. define BASIC_SANITY if [ -z "$($*_CONFIG)" ]; then \ echo; \ echo " No module '$*'. Aborting backup run."; \ echo " You may create it with: \`$(THIS_MAKEFILE) new-$*\`";\ echo; \ exit 1; \ fi; \ if [ -z "$($*_SRC)" ]; then \ echo; \ echo " Module $*_SRC not defined. Aborting backup run."; \ echo " Edit $($*_CONFIG) first, then try again."; \ echo; \ exit 1; \ fi; \ if [ -z "$($*_DEST)" ]; then \ echo; \ echo " Module $*_DEST not defined. Aborting backup run."; \ echo " Edit $($*_CONFIG) first, then try again."; \ echo; \ exit 1; \ fi if [ -n "$($*_USER)" ] && [ "$($*_USER)" != "$(USER)" ]; then \ echo; \ echo " Module requires user '$($*_USER)'. Aborting backup run.";\ echo " Edit $($*_CONFIG), or run as the required user."; \ echo; \ exit 1; \ fi endef # Convenience target for initialising a new module destination dir. # The snapshot target cowardly refuses to create new destinations # as a basic sanity trap. # Note: The pre- and post-commands are not run for this target. dest-%: @$(BASIC_SANITY) @echo; @echo " Creating '$($*_DEST)' dir" @echo; @mkdir -p $($*_DEST) # Syntax highlighting hack for the quote removal operation. DOUBLE_QUOTE := " #" # Helper target for issuing (more or less) arbitrary post-commands. do_postcommand-%: @[ x"$(subst $(DOUBLE_QUOTE),_,$($*_POSTCOMMAND))" = x ] || \ echo "Executing post-commands:" $($*_POSTCOMMAND) # The real worker target, creates a snapshot of some module. do_snap-%: LAST_SNAP = $(notdir $(lastword $(sort $(wildcard $($*_DEST)/$*-*.mrb)))) do_snap-%: THIS_SNAP = $($*_DEST)/$*-$(DATESTAMP).mrb do_snap-%: RSYNC_OPTIONS += $(addprefix --link-dest=,$(addprefix ../,$(LAST_SNAP))) do_snap-%: RSYNC_OPTIONS += $(addprefix --include=',$(addsuffix ',$($*_INCLUDE))) do_snap-%: RSYNC_OPTIONS += $(addprefix --exclude=',$(addsuffix ',$($*_EXCLUDE))) do_snap-%: RSYNC_OPTIONS += $(addprefix --filter=',$(addsuffix ',$($*_FILTER))) do_snap-%: RSYNC_OPTIONS += $(addprefix --filter=': ,$(addsuffix ',$($*_FILTER_FILE))) do_snap-%: RSYNC_OPTIONS += $($*_RSYNC_OPTIONS) do_snap-%: LOG_OUTPUT = $(addprefix 2>&1 | tee -a ,$(MRB_SNAPSHOT_LOG)) do_snap-%: # Begin with a little more sanity checking @for s in $($*_SRC); do \ if [ ! -r "$$s" ]; then \ if [[ $$s == *:* ]]; then continue; fi; \ echo; \ echo " Source '$$s' does not exist or is not readable.";\ echo " Aborting backup run."; \ echo; \ $(THIS_MAKEFILE) --no-print-directory do_postcommand-$*;\ exit 1; \ fi; \ done @if [ ! -d $($*_DEST) ]; then \ echo; \ echo " Destination dir '$($*_DEST)' does not exist."; \ echo " Aborting backup run."; \ echo " If this destination is correct, but new, you may create it now";\ echo " with the command: \`$(THIS_MAKEFILE) dest-$*\`"; \ echo; \ $(THIS_MAKEFILE) --no-print-directory do_postcommand-$*; \ exit 1; \ fi # Tell 'em what we're going to do. @echo @echo " Making $*-$(DATESTAMP) snapshot" @echo " from: $($*_SRC)" @echo " to : $($*_DEST)" @[ -z "$($*_INCLUDE)" ] || echo " include : $($*_INCLUDE)" @[ -z "$($*_EXCLUDE)" ] || echo " exclude : $($*_EXCLUDE)" @[ -z "$($*_FILTER)" ] || echo " filter : $($*_FILTER)" @if [ -z "$(LAST_SNAP)" ]; then \ echo " Performing full copy."; \ else \ echo " Incremental update based on '$(LAST_SNAP)'"; \ fi @echo # Do it. @mkdir $(THIS_SNAP) @(echo; echo " ==== $(THIS_SNAP) ===="; echo) >> $(MRB_SNAPSHOT_LOG) @echo "==> $(RSYNC) $(RSYNC_OPTIONS) $($*_SRC) $(THIS_SNAP)" $(LOG_OUTPUT) @$(RSYNC) $(RSYNC_OPTIONS) $($*_SRC) $(THIS_SNAP) $(LOG_OUTPUT) @echo $(LOG_OUTPUT) @[ x"$(MRB_SNAPSHOT_LOG)" = x ] || echo "Logged transfer to '$(MRB_SNAPSHOT_LOG)'" # Main user entry-point for creating new snapshots. # We do any PRECOMMAND's here, before forwarding to the main worker target, # as they may be needed to prepare the source and/or dest mount points # before the target specific variables are assigned their final values. snap-%: @$(BASIC_SANITY) @[ -z "$(subst $(DOUBLE_QUOTE),_,$($*_PRECOMMAND))" ] || echo "Executing pre-commands:" $($*_PRECOMMAND) @$(THIS_MAKEFILE) --no-print-directory do_$@ @$(THIS_MAKEFILE) --no-print-directory do_postcommand-$* # Tell 'em its done. @echo; echo "All done. You may resume normal breathing patterns now." # Helper target for multiple user sync. user_sync-%: @echo; echo "Begin sync for '$*'" @su - $* -c '$(THIS_MAKEFILE) --no-print-directory sync' # Convenience target to create a snapshot of all defined modules. # If called by an ordinary user, it will find all the modules # available to that user. If MRB_SYNC_USERS contains a list of # users, it will try to sync for them as well. Typically this is # only useful for root, as we must assume their identity to # reliably perform that task. sync: $(addprefix snap-,$(notdir $(basename $(foreach d, \ $(MRB_CONFDIR),$(wildcard $(d)/*.mrc))))) \ $(addprefix user_sync-,$(MRB_SYNC_USERS)) @echo "Full sync for '$(USER)' done" # Convenience target to check the required config files are available. info: @echo @echo "$(THIS_MAKEFILE) is reading the following configuration files:" @for f in $(filter-out $(THIS_MAKEFILE),$(MAKEFILE_LIST)); do \ echo " $$f"; \ done @echo "as user '$(USER)'." @echo # If I have to explain this one, then I guess you are just reading this # 'for the articles' -- but I hope you'll have enjoyed it anyway... help: @echo @echo "Usage: $(THIS_MAKEFILE) " @echo @echo "mrb is a simple, on demand, backup system for taking a snapshot" @echo "of a set of directories. The following commands are recognised" @echo "(where 'MODULE' is the name of one of your snapshot definitions):" @echo @echo " new-MODULE - Create a skeleton definition for a new snapshot 'MODULE'." @echo " dest-MODULE - Create the destination dir for 'MODULE'." @echo " This directory must exist to create a snapshot." @echo @echo " snap-MODULE - Create a snapshot of 'MODULE'." @echo " sync - Create snapshots of all defined modules." @echo " If run as root this may be configured to include" @echo " the modules of other users too (see MRB_SYNC_USERS" @echo " in ~/.mrb/defaults)." @echo @echo " info - Report on the available config and module files." @echo " help - Display this text again." @echo .PHONY: default info help # TODO: # How to link back to the last N snapshots if useful.. ? # (note you can presently override the value of LAST_SNAP, or # add additional --link-dest commands via module_RSYNC_OPTIONS) # Support for per-user precommand and postcommands instead # of executing the same ones repeatedly for each snapshot. # Report on file additions/removals since the last incremental.