Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

executable file 303 lines (246 sloc) 9.288 kb
#!/usr/bin/perl
use strict;
use warnings;
my $CONFBASE = "$ENV{HOME}/.config/simplicity";
my $CONF = "$CONFBASE/.conf";
my $SIMPLE_PATT = qr(^[-.\w/]+$);
my $URL_PATT = qr(^[-.\w/:/@]+$);
@ARGV or usage();
my ($a1, $a2, $a3) = (@ARGV, '', '', '');
# create confbase if needed
-d $CONFBASE or run("mkdir -p $CONFBASE");
# edit the conf file
if ("$a1" eq "edit" and $a2 eq "") {
run("perl -ne 'print if m(^/)...m(^/) and not m(^/)' < $0 > $CONF") unless (-f "$CONF");
run( ($ENV{VISUAL} || $ENV{EDITOR} || "vi") . " $CONF");
exit;
}
# the rest of the code needs a parsed config file
# parse the conf file and store NAMES, FILES, etc...
my %conf = parse_conf();
# hash -> hash -> list
# list the names and lists available
if ("$a1" eq "list") {
printf "backupset names in conf:\n";
printf "\t$_\n" for (sort keys %{ $conf{NAMES} });
printf "file lists in conf:\n";
printf "\t$_\n" for (sort &get_conf_lists());
printf "actual listfiles on disk:\n";
printf "\t$_\n" for (sort &get_phy_lists());
exit;
}
# edit list (part of name is sufficient)
if ($a1 eq "edit" and $a2 =~ $SIMPLE_PATT) {
# combine lists in conf and lists on disk
my @lists = map { s/\t.*//; $_ } &get_conf_lists();
push @lists, &get_phy_lists();
# de-dup
my %lists = map { $_ => 1 } @lists;
@lists = grep /$a2/, sort keys %lists;
# if empty, use argument itself as new list and invoke editor
@lists = ($a2) unless @lists;
run( ($ENV{VISUAL} || $ENV{EDITOR} || "vi") . " " . join(" ", @lists));
exit;
}
# finally... we get to *do* something!
{
my ($name, $op) = ($a1, $a2);
$op ||= 'b'; # default is "backup"
die "don't know the name $name\n" unless ref($conf{NAMES}{$name}) eq 'ARRAY';
die "don't know the operation $op\n" unless $op =~ /^(b|r|status|list|verify|full=\d)$/;
my ($files, $disk, $key, @options) = @{ $conf{NAMES}{$name} };
die "unknown $files in $name\n" unless ref($conf{FILES}{$files}) eq 'ARRAY';
die "unknown $disk in $name\n" unless ref($conf{DISKS}{$disk}) eq 'ARRAY';
die "unknown $key in $name\n" unless ref($conf{KEYS}{$key}) eq 'ARRAY' or
$key =~ /^(ask|env|none)$/;
my ($DIR, $LIST, $URL, $KEY);
$DIR = $conf{FILES}{$files}[0];
$LIST = $conf{FILES}{$files}[1] || '';
$URL = $conf{DISKS}{$disk}[0] . "/$name";
$KEY = $conf{KEYS}{$key}[0] || $key;
my ($OPTIONS);
for my $o (@options) {
die "unknown $o in $name\n" unless ref($conf{OPTIONS}{$o}) eq 'ARRAY';
$OPTIONS .= join (" ", @ { $conf{OPTIONS}{$o} } );
}
# adjust options
$KEY = adjust_key($KEY, $op);
$LIST = "$CONFBASE/$LIST" if $LIST;
die "include/exclude list '$LIST' not found\n" if ($LIST and not -f $LIST);
unless ($LIST) {
print STDERR "simplicity: warning: using empty include/exclude list...\n";
$LIST = '/dev/null';
}
$OPTIONS .= " --name='$name'";
# run the requested operation
my $rc = 0;
if ($op eq 'b') {
run("duplicity $KEY --include-globbing-filelist $LIST $OPTIONS $DIR $URL");
} elsif ($op eq 'r') {
print STDERR "duplicity $KEY $OPTIONS" . ($ENV{FTR} ? " --file-to-restore $ENV{FTR}" : "") . " $URL\n";
} elsif ($op eq 'status') {
run("duplicity collection-status $KEY $OPTIONS $URL");
} elsif ($op eq 'list') {
run("duplicity list-current-files $KEY $OPTIONS $URL");
} elsif ($op eq 'verify') {
run("duplicity verify $KEY --include-globbing-filelist $LIST $OPTIONS $URL $DIR");
} elsif ($op =~ /^full=(\d)$/) {
my $n = $1;
run("duplicity remove-all-but-n-full $n $KEY --force $OPTIONS $URL");
run("duplicity full $KEY --include-globbing-filelist $LIST $OPTIONS $DIR $URL");
run("duplicity remove-all-but-n-full $n $KEY --force $OPTIONS $URL");
}
}
# ----------------------------------------------------------------------
# subroutines
sub parse_conf {
my %conf;
local(@ARGV) = ($CONF);
my $section = '';
while (<>) {
# kill tabs and comments; trim; skip empty lines
s/\t/ /g; s/^ +//; s/ +$//; s/#.*//; next unless /\S/;
# % FOO
if (/^% ([A-Z]+)$/) {
$section = $1;
next;
}
# foo = bar baz ...
if (/^(\S+) *= *(.*)/) {
my ($key, $value) = ($1, $2);
die "I dont like $key\n" unless $key =~ $SIMPLE_PATT;
my @values = split(' ', $value);
die "extra fields in $_" if
($section eq 'FILES' and @values > 2 or
$section eq 'DISKS' and @values > 1 or
$section eq 'KEYS' and @values > 1);
die "too few fields in $_" if
($section eq 'NAMES' and @values < 3 or
$section eq 'FILES' and @values < 1 or
$section eq 'DISKS' and @values < 1 or
$section eq 'KEYS' and @values < 1);
my $patt = ($section =~ /^(DISKS|KEYS)$/ ? $URL_PATT : $SIMPLE_PATT);
for my $v (@values) {
die "I don't recognise $v in $_" unless $v =~ $patt;
}
$conf{$section}{$key} = \@values;
}
}
return %conf;
}
sub adjust_key {
my ($k, $op) = @_;
if ($k eq 'ask') {
delete $ENV{PASSPHRASE};
$k = '';
} elsif ($k eq 'env') {
die "PASSPHRASE env var not set\n"
unless $ENV{PASSPHRASE} or ($op eq 'status' or $op eq 'list');
$k = '';
} elsif ($k eq 'none') {
$k = '--no-encryption';
} else {
$k = "--encrypt-key $k";
}
}
sub run {
my $rc = 0;
my $cmd = shift;
print STDERR "$cmd\n" if $ENV{D};
$rc = system($cmd);
return unless $rc;
print "simplicity: $cmd\n";
if ($rc == -1) {
print "simplicity: failed to execute: $!\n";
} elsif ($rc & 127) {
printf "simplicity: child died with signal %d, %s coredump\n", ($rc & 127), ($rc & 128) ? 'with' : 'without';
} else {
printf "simplicity: child exited with value %d\n", $rc >> 8;
}
exit 1;
}
sub get_phy_lists {
chdir($CONFBASE) or die "chdir $CONFBASE failed: $!\n";
return glob("*");
}
sub get_conf_lists {
my @l;
my $pat = $_[0] || '.';
for my $f (sort keys %{ $conf{FILES} }) {
my $l = $conf{FILES}{$f}[1] or next;
push @l, "$l\t($f)";
}
return @l;
}
sub usage {
print STDERR "
duplicity made simple
---------------------
simplicity edit
create it if it doesn't exist, then edit the main config file
( $CONF )
simplicity list
list backupset names and include/exclude lists
simplicity edit foo
edit the include/exclude lists whose name contains 'foo', or create and
edit a new list called 'foo'
(yes those commands are plain English. You can't have a program called
'simplicity' and have the commands be 'ec', 'll', etc!)
simplicity <backupset name> [ <operation> ]
performs the requested operation on the backupset named. The
following operations are defined:
<empty> default if no operation given: take a backup
status collection status
list list current files in the backup
verify verify
full=N (where N = 1 to 9) take a full backup, then remove all but the
last N full backups
r show the restore command you would need to use. If the
environment variable FTR is defined, that will be used as the
argument for the --file-to-restore option so the resulting command
is easier to copy/paste
See http://github.com/sitaramc/simplicity for more.
";
exit 1;
}
__END__
/begin conf template
# simplicity -- conf file
#
# please modify as needed
# see http://github.com/sitaramc/simplicity for more
% FILES
# the first word after the '=' is the directory to be backed up
# (if it does not start with a "/", it is relative to the pwd)
# the second word is an include/exclude list. SEE THE DOCUMENTATION!
mail = thundermail mail-list
nonmail = . home-list
pix = Pictures
% DISKS
# about a dozen more such 'scheme's (including, I kid you not, IMAP!) can be
# found in 'man duplicity')
sg500 = file:///media/500gb-seagate/backups
local = file:///bigdisks/backups
# I have an account on a friend's machine
raj = ssh://sitaram@raj.example.com/backups
% KEYS
# please, Please, PLEASE, read the docs!
sita = sitaram@atc.tcs.com
# other types of keys (ask, env, and none) don't have to be declared here
% OPTIONS
vs10mb = --volsize 10
info = --verbosity info
debug = --verbosity debug
# the NAMES section must come last
% NAMES
m-raj = mail raj sita
# running 'simplicity m-raj' will take the files in 'thundermail' (see
# definition of 'mail' above), with an include/exclude list in 'mail-list',
# and back them up to raj's machine, using the gpg key sitaram@atc.tcs.com
m-raj-debug = mail raj sita debug
# same thing, except it shows you how to add pretty much any option that
# duplicity allows you to use
m-l = mail local none
nm-sg500 = nonmail sg500 ask
pix = pix local none
/end conf template
Jump to Line
Something went wrong with that request. Please try again.