[Bps-public-commit] brackup branch, master, updated. 38ee669ac3b9bd77cc2b58c8acedcf892ea1bb37

Alex Vandiver alexmv at bestpractical.com
Thu Dec 23 17:42:24 EST 2010


The branch, master has been updated
       via  38ee669ac3b9bd77cc2b58c8acedcf892ea1bb37 (commit)
       via  b20908dfc1494918e4b7acb2f43dbe830d88dc7b (commit)
       via  3e984db819301a2508db37f77c89e88f3f845ec2 (commit)
       via  580a48898e00ef7a30bef3d84e12aec6802fe030 (commit)
       via  b4eb24444a3234d270bd0af3360df4236b7f5801 (commit)
       via  1c7a2a83293592e69e517f7a842fa0fce2c0d34b (commit)
       via  109a022cbf28eab01651b3d5185fe4050961d42b (commit)
       via  976602e8f12d850fae19747c81fe62e3fe1e9ef1 (commit)
       via  de74b3a3bac8961a4084bac85c3c4079601a572c (commit)
       via  bbcada0a3339dd1edc8bbc58d1b6be1302ba48a4 (commit)
       via  c500fee0dff15cb143309036fd64a2a49f2fac17 (commit)
       via  de04f8ffc2bb9c3bcb2f5727863b797a98a5ed94 (commit)
       via  15c1495d7eddb0040413e29b4c037983da6f1301 (commit)
       via  4d1c5e99aec1ba20b73311dedad5affce815f48a (commit)
       via  e9e6904969af1625d5bce834ad9bafa6ea057546 (commit)
      from  fc647da300dffad6409aa1999ec4be150f6286fe (commit)

Summary of changes:
 Changes                                  |   14 +-
 MANIFEST                                 |   26 ++-
 brackup-restore                          |   17 ++-
 brackup-target                           |    4 +-
 brackup-verify-inventory                 |   13 +-
 lib/Brackup.pm                           |    2 +-
 lib/Brackup/Config.pm                    |    8 +-
 lib/Brackup/File.pm                      |    6 +-
 lib/Brackup/Restore.pm                   |   62 +++++--
 lib/Brackup/Target/Amazon.pm             |    4 +-
 lib/Brackup/Target/Riak.pm               |  326 ++++++++++++++++++++++++++++++
 lib/Brackup/Test.pm                      |   21 ++-
 t/{01-backup-sftp.t => 01-backup-riak.t} |   12 +-
 t/07-restore-conflict.t                  |   81 ++++++++
 14 files changed, 542 insertions(+), 54 deletions(-)
 create mode 100644 lib/Brackup/Target/Riak.pm
 copy t/{01-backup-sftp.t => 01-backup-riak.t} (74%)
 create mode 100644 t/07-restore-conflict.t
 create mode 100644 t/data/zero-length.txt

- Log -----------------------------------------------------------------
commit e9e6904969af1625d5bce834ad9bafa6ea057546
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Sep 9 12:48:22 2010 +0000

    Add Brackup::Target::Riak, allowing brackups to a riak cluster.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@318 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/Changes b/Changes
index 33df2f3..2c26604 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,5 @@
+  - add Riak target, allowing backups to a riak cluster (Gavin Carr)
+
   - add uid/gid info to metafile, and use in restores (where possible)
     (Gavin Carr)
 
diff --git a/brackup-target b/brackup-target
index 2c219cd..ad48551 100755
--- a/brackup-target
+++ b/brackup-target
@@ -134,10 +134,10 @@ sub CMD_list_backups {
         '-' x 11,
         '-' x 8);
     foreach my $si (sort { $b->time <=> $a->time } $target->backups) {
-        printf("%-32s %-30s %10d\n",
+        printf("%-32s %-30s %10s\n",
                $si->filename,
                $si->localtime,
-               $si->size);
+               $si->size || '?');
     }
     return 1;
 }
diff --git a/lib/Brackup/Config.pm b/lib/Brackup/Config.pm
index d7486d8..665bb79 100644
--- a/lib/Brackup/Config.pm
+++ b/lib/Brackup/Config.pm
@@ -175,7 +175,10 @@ sub list_targets {
 }
 
 sub load_target {
-    my ($self, $name) = @_;
+    my ($self, $name, %opts) = @_;
+    my $testmode = delete $opts{testmode};
+    croak("Unknown options: " . join(', ', keys %opts)) if %opts;
+
     my $confsec = $self->{"TARGET:$name"} or
         die "Unknown target '$name'\n";
 
@@ -190,7 +193,8 @@ sub load_target {
     my $target = $class->new($confsec);
 
     if (my @unk_config = $confsec->unused_config) {
-        die "Unknown config params in TARGET:$name: @unk_config\n";
+        die "Unknown config params in TARGET:$name: @unk_config\n"
+             unless $testmode;
     }
     return $target;
 }
diff --git a/lib/Brackup/Target/Riak.pm b/lib/Brackup/Target/Riak.pm
new file mode 100644
index 0000000..90c08fc
--- /dev/null
+++ b/lib/Brackup/Target/Riak.pm
@@ -0,0 +1,329 @@
+package Brackup::Target::Riak;
+use strict;
+use warnings;
+use base 'Brackup::Target';
+use Net::Riak 0.08;
+
+# fields in object:
+#   host_url
+#   r
+#   w
+#   dw
+#   bucket_prefix
+#   client - Net::Riak client object
+#   bucket - hashref holding 'chunk' and 'backup' bucket references
+#   content_type - hashref holding 'chunk' and 'backup' content types
+#
+
+sub new {
+    my ($class, $confsec) = @_;
+    my $self = $class->SUPER::new($confsec);
+
+    $self->{host_url} = $confsec->value("riak_host_url") || 'http://127.0.0.1:8098';
+    $self->{r} = $confsec->value("riak_r");
+    $self->{w} = $confsec->value("riak_w");
+    $self->{dw} = $confsec->value("riak_dw");
+    $self->{bucket_prefix} = $confsec->value("riak_bucket_prefix") || 'brackup';
+
+    $self->_init;
+
+    return $self;
+}
+
+sub _init {
+    my $self = shift;
+
+    my %quorum_attr = ();
+    $quorum_attr{r}  = $self->{r}  if $self->{r};
+    $quorum_attr{w}  = $self->{w}  if $self->{w};
+    $quorum_attr{dw} = $self->{dw} if $self->{dw};
+
+    # Construct client
+    $self->{client} = Net::Riak->new( host => $self->{host_url}, %quorum_attr );
+
+    # The default Net::Riak useragent timeout is only 3s
+    $self->{client}->useragent->timeout(30);
+
+    $self->{bucket} = {
+      chunk  => $self->{client}->bucket( $self->{bucket_prefix} . "-chunks"  ),
+      backup => $self->{client}->bucket( $self->{bucket_prefix} . "-backups" ),
+    };
+    $self->{content_type} = {
+      chunk  => 'x-danga/brackup-chunk',
+      backup => 'x-danga/brackup-meta',
+    };
+}
+
+# w and dw aren't required for stores, so omitted here
+sub backup_header {
+    my ($self) = @_;
+    return {
+        RiakHostUrl  => $self->{host_url},
+        RiakBucketPrefix => $self->{bucket_prefix},
+        $self->{r} ? ( RiakR => $self->{r} ) : (),
+    };
+}
+
+# Location and backup_prefix aren't required for restores, so they're omitted here
+sub new_from_backup_header {
+    my ($class, $header, $confsec) = @_;
+
+    my $host_url =      ($ENV{'RIAK_HOST_URL_LIST'} || 
+                         $header->{RiakHostUrl} || 
+                         $confsec->value('riak_host_url') || 
+                         'http://127.0.0.1:8098');
+    my $r =             ($ENV{RIAK_R} ||
+                         $header->{RiakR} ||
+                         $confsec->value('riak_r'));
+    my $bucket_prefix = ($ENV{'RIAK_BUCKET_PREFIX'} || 
+                         $header->{RiakBucketPrefix} ||
+                         $confsec->value('riak_bucket_prefix') ||
+                         'brackup');
+
+    my $self = bless {}, $class;
+    $self->{host_url} = $host_url;
+    $self->{r} = $r;
+    $self->{bucket_prefix} = $bucket_prefix;
+
+    $self->_init;
+
+    return $self;
+}
+
+# riak seems to give transient errors with degraded nodes, so retry reads and writes
+sub _retry {
+    my ($self, $op, $sub, $ok) = @_;
+    $ok ||= sub {
+        my $obj = shift;
+        return $obj && $obj->exists;
+    };
+
+    my $obj;
+    my $n_fails = 0;
+    while ($n_fails < 5) {
+        $obj = $sub->();
+        last if $ok->($obj);
+
+        $n_fails++;
+        warn "Error $op ... will do retry \#$n_fails in 5 seconds ...\n";
+        sleep 5;
+    }
+
+    return $obj;
+}
+
+sub _load {
+    my ($self, $type, $key) = @_;
+
+    my $bucket = $self->{bucket}->{$type} or die "Invalid type '$type'";
+    my $content_type = $self->{content_type}->{$type};
+
+    my $obj = $self->_retry("loading $type $key", sub { $bucket->get($key) });
+
+    return unless $obj->exists;
+    return unless $obj->content_type eq $content_type;
+
+    return $obj;
+}
+
+sub has_chunk {
+    my ($self, $chunk) = @_;
+    my $dig = $chunk->backup_digest;   # "sha1:sdfsdf" format scalar
+
+    eval { $self->_load(chunk => $dig) } or return 0;
+
+    return 1;
+}
+
+sub load_chunk {
+    my ($self, $dig) = @_;
+
+    my $obj = $self->_load(chunk => $dig) or return;
+
+    return \ $obj->data;
+}
+
+sub _store {
+    my ($self, $type, $key, $data) = @_;
+
+    my $bucket = $self->{bucket}->{$type} or die "Invalid type '$type'";
+    my $content_type = $self->{content_type}->{$type};
+
+    my $sub = sub {
+        my $obj = $bucket->new_object($key, $data,
+            content_type  => $content_type,
+        );
+        $obj->store;
+    };
+
+    my $obj = $self->_retry("storing $type $key", $sub);
+
+    unless ($obj->exists) {
+        warn "Error uploading chunk again: " . $obj->status . "\n";
+        return 0;
+    }
+    return 1;
+}
+
+sub store_chunk {
+    my ($self, $chunk) = @_;
+    my $dig = $chunk->backup_digest;
+    my $fh = $chunk->chunkref;
+    my $chunkref = do { local $/; <$fh> };
+
+    return $self->_store(chunk => $dig, $chunkref);
+}
+
+sub delete_chunk {
+    my ($self, $dig) = @_;
+
+    my $obj = $self->_load(chunk => $dig) or return;
+
+    $obj->delete;
+}
+
+# returns a list of names of all chunks
+sub chunks {
+    my $self = shift;
+
+    my $chunks = $self->_retry("loading chunks", 
+        sub { $self->{bucket}->{chunk}->get_keys({stream => 1}) },
+        sub { my $chunks = shift; $chunks && ref $chunks eq 'ARRAY' },
+    );
+
+    return unless $chunks;
+    return @$chunks;
+}
+
+sub store_backup_meta {
+    my ($self, $name, $fh) = @_;
+    my $content = do { local $/; <$fh> };
+
+    return $self->_store(backup => $name, $content);
+}
+
+sub backups {
+    my $self = shift;
+
+    my $backups = $self->_retry("loading backups", 
+        sub { $self->{bucket}->{backup}->get_keys({stream => 1}) },
+        sub { my $backups = shift; $backups && ref $backups eq 'ARRAY' },
+    );
+
+    my @ret = ();
+    foreach my $backup (@$backups) {
+        # Riak has no explicit mtime/size metadata
+        my @elements = split /-/, $backup;
+        push @ret, Brackup::TargetBackupStatInfo->new($self, $backup,
+                                                      time => $elements[$#elements],
+                                                      size => 0);
+    }
+
+    return @ret;
+}
+
+sub get_backup {
+    my ($self, $name, $output_file) = @_;
+
+    my $obj = $self->_load(backup => $name) or return;
+
+	$output_file ||= "$name.brackup";
+    open(my $out, ">$output_file") or die "Failed to open $output_file: $!\n";
+    my $outv = syswrite($out, $obj->data);
+    die "download/write error" unless $outv == do { use bytes; length $obj->data };
+    close $out;
+    return 1;
+}
+
+sub delete_backup {
+    my ($self, $name) = @_;
+
+    my $obj = $self->_load(backup => $name) or return;
+
+    $obj->delete;
+}
+
+sub chunkpath {
+    my ($self, $dig) = @_;
+    return $dig;
+}
+
+sub size {
+    my ($self, $dig) = @_;
+
+    my $obj = $self->_load(chunk => $dig) or return 0;
+
+    return $obj->_headers->{'Content-Length'};
+}
+
+1;
+
+=head1 NAME
+
+Brackup::Target::Riak - backup to a Riak key-value store
+
+=head1 EXAMPLE
+
+In your ~/.brackup.conf file:
+
+  [TARGET:riak1]
+  type = Riak
+  riak_host_url = http://192.168.1.1:8098/
+  riak_r = 1
+  riak_bucket_prefix = brackup-test
+
+=head1 CONFIG OPTIONS
+
+All options may be omitted unless specified.
+
+=over
+
+=item B<type>
+
+I<(Mandatory.)> Must be "B<Riak>".
+
+=item B<riak_host_url>
+
+URL specifying your riak cluster endpoint. Default: http://127.0.0.1:8098/.
+
+=item B<riak_r> 
+
+riak read quorum - how many replicas need to agree when retrieving an object. 
+Default: 2.
+
+=item B<riak_w> 
+
+riak write quorum - how many replicas to write to before returning success. 
+Default: 2.
+
+=item B<riak_dw> 
+
+riak durable write quorum - how many replicas to write to durable storage 
+before returning success. Default: 2.
+
+=item B<riak_bucket_prefix>
+
+Prefix to use on riak buckets. Probably only worth changing if you want to
+store (and separate) multiple brackups in a riak cluster. Default: brackup.
+
+=back
+
+=head1 SEE ALSO
+
+L<Brackup::Target>
+
+L<Net::Riak> -- required module to use Brackup::Target::Riak
+
+=head1 AUTHOR
+
+Gavin Carr E<lt>gavin at openfusion.com.auE<gt>.
+
+Copyright (c) 2010 Gavin Carr.
+
+This module is free software. You may use, modify, and/or redistribute 
+this software under the same terms as perl itself.
+
+=cut
+
+# vim:sw=4:et
+
diff --git a/lib/Brackup/Test.pm b/lib/Brackup/Test.pm
index 0fa5784..b666e1b 100644
--- a/lib/Brackup/Test.pm
+++ b/lib/Brackup/Test.pm
@@ -66,7 +66,7 @@ sub do_backup {
         $confsec->add("inventorydb_file" => $inv_filename);
         $confsec->add("path" => $backup_dir);
         $conf->add_section($confsec);
-        $target = $conf->load_target("test_restore");
+        $target = $conf->load_target("test_restore", testmode => 1);
     }
 
     ok($target, "have a target ($target)");
diff --git a/t/01-backup-riak.t b/t/01-backup-riak.t
new file mode 100644
index 0000000..2e881ef
--- /dev/null
+++ b/t/01-backup-riak.t
@@ -0,0 +1,58 @@
+# -*-perl-*-
+#
+# Backup test of riak target - set $ENV{BRACKUP_TEST_RIAK} to run
+#
+
+use strict;
+use Test::More;
+
+use Brackup::Test;
+use FindBin qw($Bin);
+use Brackup::Util qw(tempfile);
+
+if ($ENV{BRACKUP_TEST_RIAK}) {
+  plan tests => 25;
+} else {
+  plan skip_all => "\$ENV{BRACKUP_TEST_RIAK} not set";
+}
+
+############### Backup
+
+my ($digdb_fh, $digdb_fn) = tempfile();
+close($digdb_fh);
+my $root_dir = "$Bin/data";
+ok(-d $root_dir, "test data to backup exists");
+my $backup_file = do_backup(
+                            with_confsec => sub {
+                                my $csec = shift;
+                                $csec->add("path",          $root_dir);
+                                $csec->add("chunk_size",    "2k");
+                                $csec->add("digestdb_file", $digdb_fn);
+                            },
+                            with_targetsec => sub {
+                                my $tsec = shift;
+                                $tsec->add("type",          'Riak');
+                                $tsec->add("riak_host_url", $ENV{RIAK_HOST_URL} || 'http://127.0.0.1:8098');
+                            },
+                            );
+
+############### Restore
+
+# Full restore
+my $restore_dir = do_restore($backup_file);
+ok_dirs_match($restore_dir, $root_dir);
+
+# --just=DIR restore
+my $just_dir = do_restore($backup_file, prefix => 'my_dir');
+ok_dirs_match($just_dir, "$root_dir/my_dir");
+
+# --just=FILE restore
+my $just_file = do_restore($backup_file, prefix => 'huge-file.txt');
+ok_files_match("$just_file/huge-file.txt", "$root_dir/huge-file.txt");
+
+# --just=DIR/FILE restore
+my $just_dir_file = do_restore($backup_file, prefix => 'my_dir/sub_dir/program.sh');
+ok_files_match("$just_dir_file/program.sh", "$root_dir/my_dir/sub_dir/program.sh");
+
+# vim:sw=4:et
+

commit 4d1c5e99aec1ba20b73311dedad5affce815f48a
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Sep 9 16:06:19 2010 +0000

    Update Target::Amazon to accept Net::Amazon::S3 ENV vars (Alex Vandiver).
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@319 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Target/Amazon.pm b/lib/Brackup/Target/Amazon.pm
index d55e394..cea0f94 100644
--- a/lib/Brackup/Target/Amazon.pm
+++ b/lib/Brackup/Target/Amazon.pm
@@ -81,11 +81,13 @@ sub new_from_backup_header {
     my ($class, $header, $confsec) = @_;
 
     my $accesskey     = ($ENV{'AWS_KEY'} || 
+                         $ENV{'AWS_ACCESS_KEY_ID'} ||
                          $header->{AWSAccessKeyID} || 
                          $confsec->value('aws_access_key_id') || 
                          _prompt("Your Amazon AWS access key? "))
         or die "Need your Amazon access key.\n";
     my $sec_accesskey = ($ENV{'AWS_SEC_KEY'} || 
+                         $ENV{'AWS_ACCESS_KEY_SECRET'} ||
                          $confsec->value('aws_secret_access_key') || 
                          _prompt("Your Amazon AWS secret access key? "))
         or die "Need your Amazon secret access key.\n";

commit 15c1495d7eddb0040413e29b4c037983da6f1301
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Wed Oct 20 18:58:37 2010 +0000

    Bump Target::Riak Net::Riak version requirement to 0.09.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@320 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Target/Riak.pm b/lib/Brackup/Target/Riak.pm
index 90c08fc..7bd61ef 100644
--- a/lib/Brackup/Target/Riak.pm
+++ b/lib/Brackup/Target/Riak.pm
@@ -2,7 +2,7 @@ package Brackup::Target::Riak;
 use strict;
 use warnings;
 use base 'Brackup::Target';
-use Net::Riak 0.08;
+use Net::Riak 0.09;
 
 # fields in object:
 #   host_url
@@ -41,9 +41,6 @@ sub _init {
     # Construct client
     $self->{client} = Net::Riak->new( host => $self->{host_url}, %quorum_attr );
 
-    # The default Net::Riak useragent timeout is only 3s
-    $self->{client}->useragent->timeout(30);
-
     $self->{bucket} = {
       chunk  => $self->{client}->bucket( $self->{bucket_prefix} . "-chunks"  ),
       backup => $self->{client}->bucket( $self->{bucket_prefix} . "-backups" ),
@@ -54,7 +51,7 @@ sub _init {
     };
 }
 
-# w and dw aren't required for stores, so omitted here
+# w and dw aren't required for restores, so omitted here
 sub backup_header {
     my ($self) = @_;
     return {

commit de04f8ffc2bb9c3bcb2f5727863b797a98a5ed94
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Oct 21 19:02:01 2010 +0000

    Fix buglet in Brackup::Target::Riak::size.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@321 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Target/Riak.pm b/lib/Brackup/Target/Riak.pm
index 7bd61ef..bfb0d4b 100644
--- a/lib/Brackup/Target/Riak.pm
+++ b/lib/Brackup/Target/Riak.pm
@@ -250,7 +250,7 @@ sub size {
 
     my $obj = $self->_load(chunk => $dig) or return 0;
 
-    return $obj->_headers->{'Content-Length'};
+    return $obj->_headers->content_length;
 }
 
 1;

commit c500fee0dff15cb143309036fd64a2a49f2fac17
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Fri Oct 22 11:45:36 2010 +0000

    Tweaks to brackup-verify-inventory.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@322 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/brackup-verify-inventory b/brackup-verify-inventory
index e126b95..f7423f2 100755
--- a/brackup-verify-inventory
+++ b/brackup-verify-inventory
@@ -114,7 +114,8 @@ my $inv_db = $target->inventory_db
 print "Fetching list of chunks from target\n" if $opt_verbose;
 my %chunks = map { $_ => 1 } $target->chunks;
 
-my ($count, $ok, $bad) = (0, 0, 0);
+print "Checking inventory entries\n" if $opt_verbose;
+my ($count, $ok, $bad, $skip) = (0, 0, 0, 0);
 my $total = $inv_db->count / 100;
 my %ok = ();
 my $deleting = $opt_delete ? ' - deleting from inventory' : '';
@@ -143,7 +144,8 @@ while (my ($key, $value) = $inv_db->each) {
         next;
     }
 
-    if (my $chunk_size = eval { $target->size($path) }) {
+    my $chunk_size = eval { $target->size($path) };
+    if (defined $chunk_size) {
         if ($chunk_size == $size) {
             $ok{$dig} = 1;
             $ok++;
@@ -155,9 +157,14 @@ while (my ($key, $value) = $inv_db->each) {
             $bad++;
         }
     }
+    else {
+        warn "Warning: no size returned for chunk $path, skipping\n";
+        $skip++;
+    }
 }
 
-print "Checked $count inventory entries, $ok good, $bad bad.\n" unless $opt_quiet;
+print "Checked $count inventory entries, $ok good, $bad bad, $skip skipped.\n" 
+    unless $opt_quiet;
 exit $bad ? 1 : 0;
 
 # vim:sw=4

commit bbcada0a3339dd1edc8bbc58d1b6be1302ba48a4
Author: brad at danga.com <brad at danga.com@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Mon Nov 1 01:04:32 2010 +0000

    Permit '0' as a filename.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@323 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/Changes b/Changes
index 2c26604..b93ec38 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,5 @@
+  - permit 0 as a filename.  https://rt.cpan.org/Ticket/Display.html?id=62004
+
   - add Riak target, allowing backups to a riak cluster (Gavin Carr)
 
   - add uid/gid info to metafile, and use in restores (where possible)
diff --git a/lib/Brackup/File.pm b/lib/Brackup/File.pm
index a6ad2de..8d1cec7 100644
--- a/lib/Brackup/File.pm
+++ b/lib/Brackup/File.pm
@@ -23,7 +23,7 @@ sub new {
     croak("Unknown options: " . join(', ', keys %opts)) if %opts;
 
     die "No root object provided." unless $self->{root} && $self->{root}->isa("Brackup::Root");
-    die "No path provided." unless $self->{path};
+    die "No path provided." unless defined($self->{path});  # note: permit "0"
     $self->{path} =~ s!^\./!!;
 
     return $self;

commit de74b3a3bac8961a4084bac85c3c4079601a572c
Author: brad at danga.com <brad at danga.com@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Mon Nov 1 01:18:35 2010 +0000

    Checking in changes prior to tagging of version 1.10.
    
    Changelog diff is:
    
    Index: Changes
    ===================================================================
    --- Changes	(revision 323)
    +++ Changes	(working copy)
    @@ -1,3 +1,5 @@
    +1.10 (2010-10-31)
    +
       - permit 0 as a filename.  https://rt.cpan.org/Ticket/Display.html?id=62004
    
       - add Riak target, allowing backups to a riak cluster (Gavin Carr)
    @@ -34,8 +36,8 @@
    
       - write metafile as we go, instead of big bang at the end (Gavin Carr)
    
    -  - add Brackup::BackupStats implementation, brackup stats output, and
    -    --save-stats argument (Gavin Carr)
    +- add Brackup::BackupStats implementation, brackup stats output, and
    +--save-stats argument (Gavin Carr)
    
       - fix filename trailing whitespace problems with open (Gavin Carr)
    
    
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@324 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/Changes b/Changes
index b93ec38..8b8ab33 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,5 @@
+1.10 (2010-10-31)
+
   - permit 0 as a filename.  https://rt.cpan.org/Ticket/Display.html?id=62004
 
   - add Riak target, allowing backups to a riak cluster (Gavin Carr)
@@ -34,8 +36,8 @@
 
   - write metafile as we go, instead of big bang at the end (Gavin Carr)
 
-  - add Brackup::BackupStats implementation, brackup stats output, and 
-    --save-stats argument (Gavin Carr)
+- add Brackup::BackupStats implementation, brackup stats output, and 
+--save-stats argument (Gavin Carr)
 
   - fix filename trailing whitespace problems with open (Gavin Carr)
 
diff --git a/MANIFEST b/MANIFEST
index c58ee91..57ba696 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,9 +1,14 @@
+Changes
+HACKING
+MANIFEST			This list of files
+MANIFEST.SKIP
+Makefile.PL
+TODO
 brackup
 brackup-mount
 brackup-restore
 brackup-target
 brackup-verify-inventory
-Changes
 doc/data-structures.txt
 doc/databases.txt
 doc/design-decisions.txt
@@ -11,13 +16,12 @@ doc/exampleconfig.txt
 doc/notes.txt
 doc/overview.txt
 doc/todo.txt
-HACKING
 lib/Brackup.pm
 lib/Brackup/Backup.pm
 lib/Brackup/BackupStats.pm
+lib/Brackup/ChunkIterator.pm
 lib/Brackup/Chunker/Default.pm
 lib/Brackup/Chunker/MP3.pm
-lib/Brackup/ChunkIterator.pm
 lib/Brackup/CompositeChunk.pm
 lib/Brackup/Config.pm
 lib/Brackup/ConfigSection.pm
@@ -29,8 +33,8 @@ lib/Brackup/Dict/SQLite.pm
 lib/Brackup/Dict/SQLite2.pm
 lib/Brackup/DigestCache.pm
 lib/Brackup/File.pm
-lib/Brackup/GPGProcess.pm
 lib/Brackup/GPGProcManager.pm
+lib/Brackup/GPGProcess.pm
 lib/Brackup/InventoryDatabase.pm
 lib/Brackup/Manual/Overview.pod
 lib/Brackup/Metafile.pm
@@ -46,16 +50,15 @@ lib/Brackup/Target/Filebased.pm
 lib/Brackup/Target/Filesystem.pm
 lib/Brackup/Target/Ftp.pm
 lib/Brackup/Target/GoogleAppEngine.pm
+lib/Brackup/Target/Riak.pm
 lib/Brackup/Target/Sftp.pm
 lib/Brackup/TargetBackupStatInfo.pm
 lib/Brackup/Test.pm
 lib/Brackup/Util.pm
-Makefile.PL
-MANIFEST			This list of files
-MANIFEST.SKIP
 t/00-use.t
 t/01-backup-filesystem.t
 t/01-backup-ftp.t
+t/01-backup-riak.t
 t/01-backup-sftp.t
 t/02-gpg.t
 t/03-composite-filesystem.t
@@ -68,20 +71,20 @@ t/05-filename-escaping.t
 t/06-config-inheritance.t
 t/data-2/000-dup1.txt
 t/data-2/000-dup2.txt
+t/data-2/README
 t/data-2/gzip.txt.gz
 t/data-2/my-link.txt
 t/data-2/my_dir/sub_dir/another-file.txt
 t/data-2/my_dir/sub_dir/program.sh
 t/data-2/pubring-test.gpg
 t/data-2/pubring-test2.gpg
-t/data-2/README
 t/data-2/secring-test.gpg
 t/data-2/secring-test2.gpg
 t/data-2/test-file.txt
-t/data-weird-filenames/000-dup1.txt
-t/data-weird-filenames/000-dup2.txt
 't/data-weird-filenames/filename whitespace'
 't/data-weird-filenames/filename_trailing_whitespace   '
+t/data-weird-filenames/000-dup1.txt
+t/data-weird-filenames/000-dup2.txt
 t/data-weird-filenames/huge-file.txt
 t/data-weird-filenames/my-link.txt
 t/data-weird-filenames/pubring-test.gpg
@@ -100,4 +103,3 @@ t/data/secring-test.gpg
 t/data/secring-test2.gpg
 t/data/test-file.txt
 t/misc/brackup.conf
-TODO
diff --git a/lib/Brackup.pm b/lib/Brackup.pm
index d27de80..98fcba4 100644
--- a/lib/Brackup.pm
+++ b/lib/Brackup.pm
@@ -1,7 +1,7 @@
 package Brackup;
 use strict;
 use vars qw($VERSION);
-$VERSION = '1.09';
+$VERSION = '1.10';
 
 use Brackup::Config;
 use Brackup::ConfigSection;

commit 976602e8f12d850fae19747c81fe62e3fe1e9ef1
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Wed Nov 3 19:04:45 2010 +0000

    Bump Target::Riak Net::Riak dependency to version 0.10.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@326 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Target/Riak.pm b/lib/Brackup/Target/Riak.pm
index bfb0d4b..fe1ecf5 100644
--- a/lib/Brackup/Target/Riak.pm
+++ b/lib/Brackup/Target/Riak.pm
@@ -2,7 +2,7 @@ package Brackup::Target::Riak;
 use strict;
 use warnings;
 use base 'Brackup::Target';
-use Net::Riak 0.09;
+use Net::Riak 0.10;
 
 # fields in object:
 #   host_url

commit 109a022cbf28eab01651b3d5185fe4050961d42b
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Nov 4 20:43:01 2010 +0000

    Add conflict => [skip|overwrite|update] support to Brackup::Restore.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@327 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Restore.pm b/lib/Brackup/Restore.pm
index 3b8ab48..1181a1e 100644
--- a/lib/Brackup/Restore.pm
+++ b/lib/Brackup/Restore.pm
@@ -6,6 +6,7 @@ use Digest::SHA1;
 use POSIX qw(mkfifo);
 use Fcntl qw(O_RDONLY O_CREAT O_WRONLY O_TRUNC);
 use String::Escape qw(unprintable);
+use File::stat;
 use Brackup::DecryptedFile;
 use Brackup::Decrypt;
 
@@ -17,6 +18,7 @@ sub new {
     $self->{prefix}  = delete $opts{prefix};  # directory/file filename prefix, or "" for all
     $self->{filename}= delete $opts{file};    # filename we're restoring from
     $self->{config}  = delete $opts{config};  # brackup config (if available)
+    $self->{conflict} = delete $opts{conflict};
     $self->{verbose} = delete $opts{verbose};
 
     $self->{_local_uid_map} = {};  # remote/metafile uid -> local uid
@@ -244,12 +246,36 @@ sub _exec_statinfo_updates {
     }
 }
 
+# Check if $self->{conflict} setting allows us to skip this item
+sub _can_skip {
+    my ($self, $full, $it) = @_;
+
+    if ($self->{conflict} eq 'skip') {
+        return 1;
+    } elsif ($self->{conflict} eq 'overwrite') {
+        return 0;
+    } elsif ($self->{conflict} eq 'update') {
+        my $st = stat $full
+            or die "stat on '$full' failed: $!\n";
+        return 1 if defined $it->{Mtime} && $st->mtime >= $it->{Mtime};
+    }
+    else {
+        die "Invalid '--conflict' setting '$self->{conflict}'\n";
+    }
+    return 0;
+}
+
 sub _restore_directory {
     my ($self, $full, $it) = @_;
 
+    # Apply conflict checks to directories
+    if (-d $full && $self->{conflict}) {
+        return if $self->_can_skip($full, $it);
+    }
+
     unless (-d $full) {
         mkdir $full or    # FIXME: permissions on directory
-            die "Failed to make directory: $full ($it->{Path})";
+            die "Failed to make directory: $full ($it->{Path}): $!";
     }
 
     $self->_update_statinfo($full, $it);
@@ -259,18 +285,30 @@ sub _restore_link {
     my ($self, $full, $it) = @_;
 
     if (-e $full) {
-        # TODO: add --conflict={skip,overwrite} option, defaulting to nothing: which dies
-        die "Link $full ($it->{Path}) already exists.  Aborting.";
+        die "Link $full ($it->{Path}) already exists.  Aborting." 
+            unless $self->{conflict};
+        return if $self->_can_skip($full, $it);
+
+        # Can't overwrite symlinks, so unlink explicitly if we're not skipping
+        unlink $full 
+            or die "Failed to unlink link $full: $!";
     }
-    symlink $it->{Link}, $full
-        or die "Failed to link";
+
+    symlink $it->{Link}, $full or
+        die "Failed to link $full: $!";
 }
 
 sub _restore_fifo {
     my ($self, $full, $it) = @_;
 
     if (-e $full) {
-        die "Named pipe/fifo $full ($it->{Path}) already exists.  Aborting.";
+        die "Named pipe/fifo $full ($it->{Path}) already exists.  Aborting."
+            unless $self->{conflict};
+        return if $self->_can_skip($full, $it);
+
+        # Can't overwrite fifos, so unlink explicitly if we're not skipping
+        unlink $full 
+            or die "Failed to unlink fifo $full: $!";
     }
 
     mkfifo($full, $it->{Mode}) or die "mkfifo failed: $!";
@@ -282,8 +320,9 @@ sub _restore_file {
     my ($self, $full, $it) = @_;
 
     if (-e $full && -s $full) {
-        # TODO: add --conflict={skip,overwrite} option, defaulting to nothing: which dies
-        die "File $full ($it->{Path}) already exists.  Aborting.";
+        die "File $full ($it->{Path}) already exists.  Aborting."
+            unless $self->{conflict};
+        return if $self->_can_skip($full, $it);
     }
 
     sysopen(my $fh, $full, O_CREAT|O_WRONLY|O_TRUNC) or die "Failed to open '$full' for writing: $!";

commit 1c7a2a83293592e69e517f7a842fa0fce2c0d34b
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Nov 4 20:44:21 2010 +0000

    Add --conflict => [skip|overwrite|update] to brackup-restore.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@328 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/brackup-restore b/brackup-restore
index d48e2c7..ec65886 100755
--- a/brackup-restore
+++ b/brackup-restore
@@ -42,6 +42,15 @@ Restore just the directory named (and all its contents).
 
 Restore just the file named.
 
+=item --conflict=skip|overwrite|update
+
+How to handle files that already exist (with size > zero bytes). 
+'skip' means don't restore, keeping the existing file. 'overwrite'
+means always restore, replacing the existing file. 'update' means
+overwrite iff the file we are restoring is newer than the existing one.
+
+Default is to abort the restore if a file already exists.
+
 =item --config=NAME
 
 Brackup config file to use instead of default.
@@ -79,7 +88,7 @@ use lib "$Bin/lib";
 use Brackup;
 use Brackup::Util qw(tempfile);
 
-my ($opt_verbose, $meta_file, $opt_help, $restore_dir, $opt_all, $prefix, $config_file);
+my ($opt_verbose, $meta_file, $opt_help, $restore_dir, $opt_all, $prefix, $conflict, $config_file);
 
 usage() unless
     GetOptions(
@@ -89,6 +98,7 @@ usage() unless
                'help'      => \$opt_help,
                'all'       => \$opt_all,
                'just=s'    => \$prefix,
+               'conflict=s'=> \$conflict,
                'config=s'  => \$config_file,
                );
 
@@ -103,6 +113,8 @@ usage("Backup metafile '$meta_file' doesn't exist")  unless -e $meta_file;
 usage("Backup metafile '$meta_file' isn't a file")   unless -f $meta_file;
 usage("Restore directory '$restore_dir' isn't a directory") if -e $restore_dir && ! -d $restore_dir;
 usage("Config file '$config_file' doesn't exist")    if $config_file && ! -f $config_file;
+usage("Invalid --conflict option '$conflict' (not skip|overwrite|update)")
+    if $conflict && $conflict !~ m/^(skip|overwrite|update)$/;
 $prefix ||= "";  # with -all, "", which means everything
 
 if (! -e $restore_dir) {
@@ -117,6 +129,7 @@ my $restore = Brackup::Restore->new(
                                     prefix => $prefix,
                                     file   => $meta_file,
                                     config => $config,
+                                    conflict => $conflict,
                                     verbose => $opt_verbose,
                                     );
 

commit b4eb24444a3234d270bd0af3360df4236b7f5801
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Nov 4 20:46:33 2010 +0000

    Record mtime even for symlinks, for 'restore --conflict update'.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@329 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/File.pm b/lib/Brackup/File.pm
index 8d1cec7..b428ae5 100644
--- a/lib/Brackup/File.pm
+++ b/lib/Brackup/File.pm
@@ -222,8 +222,10 @@ sub as_rfc822 {
     }
     $set->("Chunks", join("\n ", map { $_->to_meta } @$schunk_list));
 
+    # Record mtime even for links (which we can't restore), for restore --conflict update
+    $set->("Mtime", $st->mtime);
+
     unless ($self->is_link) {
-        $set->("Mtime", $st->mtime);
         $set->("Atime", $st->atime) unless $self->root->noatime;
 
         my $mode = $self->mode;

commit 580a48898e00ef7a30bef3d84e12aec6802fe030
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Nov 4 20:47:45 2010 +0000

    Add unit tests for brackup-restore --conflict.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@330 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/lib/Brackup/Test.pm b/lib/Brackup/Test.pm
index b666e1b..947caa9 100644
--- a/lib/Brackup/Test.pm
+++ b/lib/Brackup/Test.pm
@@ -137,16 +137,21 @@ sub check_inventory_db {
 
 sub do_restore {
     my ($backup_file, %opts) = @_;
-    my $prefix     = delete $opts{'prefix'} || "";   # default is restore everything
-    my $restore_should_die = delete $opts{'restore_should_die'};
+    my $prefix              = delete $opts{'prefix'} || "";   # default is restore everything
+    my $restore_should_die  = delete $opts{'restore_should_die'};
+    my $restore_dir         = delete $opts{'restore_dir'};
+    my $conflict            = delete $opts{'conflict'};
     die if %opts;
-    my $restore_dir = tempdir( CLEANUP => $ENV{BRACKUP_TEST_NOCLEANUP} ? 0 : 1 );
-    ok_dir_empty($restore_dir);
+    if (! $restore_dir) {
+        $restore_dir = tempdir( CLEANUP => $ENV{BRACKUP_TEST_NOCLEANUP} ? 0 : 1 );
+        ok_dir_empty($restore_dir);
+    }
 
     my $restore = Brackup::Restore->new(
-                                        to     => $restore_dir,
-                                        prefix => $prefix,
-                                        file   => $backup_file,
+                                        to       => $restore_dir,
+                                        prefix   => $prefix,
+                                        file     => $backup_file,
+                                        conflict => $conflict,
                                         );
     ok($restore, "have restore object");
     my $rv = eval { $restore->restore; };
diff --git a/t/07-restore-conflict.t b/t/07-restore-conflict.t
new file mode 100644
index 0000000..45c38f6
--- /dev/null
+++ b/t/07-restore-conflict.t
@@ -0,0 +1,81 @@
+# -*-perl-*-
+
+use strict;
+use Test::More;
+
+use Brackup::Test;
+use FindBin qw($Bin);
+use Brackup::Util qw(tempfile);
+
+############### Backup
+
+my ($digdb_fh, $digdb_fn) = tempfile();
+close($digdb_fh);
+my $root_dir = "$Bin/data";
+ok(-d $root_dir, "test data to backup exists");
+my $backup_file = do_backup(
+                            with_confsec => sub {
+                                my $csec = shift;
+                                $csec->add("path",          $root_dir);
+                                $csec->add("chunk_size",    "2k");
+                                $csec->add("digestdb_file", $digdb_fn);
+                            },
+                            );
+
+
+############### Monkey-patch Restore _update_statinfo and _restore_link to record restored files
+
+my @restored = ();
+{ 
+    no warnings 'redefine';
+    my $update_statinfo = \&Brackup::Restore::_update_statinfo;
+    *Brackup::Restore::_update_statinfo = sub {
+        my ($self, $full, $it) = @_;
+        $update_statinfo->(@_);
+        push @restored, $it->{Path};
+    };
+    my $restore_link = \&Brackup::Restore::_restore_link;
+    *Brackup::Restore::_restore_link = sub {
+        my ($self, $full, $it) = @_;
+        if ($restore_link->(@_)) {
+            push @restored, $it->{Path};
+        }
+    };
+}
+
+############### Restore
+
+# Full restore
+my $restore_dir = do_restore($backup_file);
+ok_dirs_match($restore_dir, $root_dir);
+
+# Restore again, same restore_dir - should die without --conflict flag
+ok(unlink("$restore_dir/000-dup1.txt"), 'removed 000-dup1.txt for --conflict undef test');
+do_restore($backup_file, restore_dir => $restore_dir, restore_should_die => 1);
+
+# Restore again, same restore_dir - should NOT die with --conflict skip
+ at restored = ();
+ok(unlink("$restore_dir/000-dup2.txt"), 'removed 000-dup2.txt for --conflict skip test');
+do_restore($backup_file, restore_dir => $restore_dir, conflict => 'skip');
+ok_dirs_match($restore_dir, $root_dir);
+# restored here: 000-dup1.txt + 000-dup2.txt + zero-length.txt
+is_deeply([ sort @restored ], [ '000-dup1.txt', '000-dup2.txt', 'zero-length.txt' ], 'restore count == 3');
+
+# Restore again, same restore_dir - should NOT die with --conflict overwrite
+ at restored = ();
+ok(unlink("$restore_dir/000-dup2.txt"), 'removed 000-dup2.txt for --conflict overwrite test');
+ok(utime(10000, 10000, "$restore_dir/test-file.txt"), "utime changed for restore_dir test-file.txt");
+do_restore($backup_file, restore_dir => $restore_dir, conflict => 'overwrite');
+ok_dirs_match($restore_dir, $root_dir);
+ok(scalar(@restored) > 10, "restore_count > 10");
+
+# Restore again, same restore_dir - should NOT die with --conflict update
+ at restored = ();
+ok(unlink("$restore_dir/000-dup2.txt"), 'removed 000-dup2.txt for --conflict update test');
+ok(utime(10000, 10000, "$restore_dir/test-file.txt"), "utime changed for restore_dir test-file.txt");
+do_restore($backup_file, restore_dir => $restore_dir, conflict => 'update');
+ok_dirs_match($restore_dir, $root_dir);
+# restored here: 000-dup2.txt + test-file.txt + zero-length.txt
+is_deeply([ sort @restored ], [ '000-dup2.txt', 'test-file.txt', 'zero-length.txt' ], 'restore count == 3');
+
+done_testing;
diff --git a/t/data/zero-length.txt b/t/data/zero-length.txt
new file mode 100644
index 0000000..e69de29

commit 3e984db819301a2508db37f77c89e88f3f845ec2
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Thu Nov 4 20:52:51 2010 +0000

    Add restore --conflict entry to Changes.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@331 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/Changes b/Changes
index 8b8ab33..a43998e 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,7 @@
+
+  - add --conflict [skip|overwrite|update] options to brackup-restore (Gavin
+    Carr)
+
 1.10 (2010-10-31)
 
   - permit 0 as a filename.  https://rt.cpan.org/Ticket/Display.html?id=62004

commit b20908dfc1494918e4b7acb2f43dbe830d88dc7b
Author: gavin at openfusion.com.au <gavin at openfusion.com.au@c3152786-d43a-0410-8499-09eb43983aed>
Date:   Fri Nov 5 11:16:09 2010 +0000

    Add new restore conflict unit test files to MANIFEST.
    
    git-svn-id: http://brackup.googlecode.com/svn/trunk@332 c3152786-d43a-0410-8499-09eb43983aed

diff --git a/MANIFEST b/MANIFEST
index 57ba696..d511e0d 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -69,6 +69,7 @@ t/04-gc-ftp.t
 t/04-gc-sftp.t
 t/05-filename-escaping.t
 t/06-config-inheritance.t
+t/07-restore-conflict.t
 t/data-2/000-dup1.txt
 t/data-2/000-dup2.txt
 t/data-2/README
@@ -102,4 +103,5 @@ t/data/pubring-test2.gpg
 t/data/secring-test.gpg
 t/data/secring-test2.gpg
 t/data/test-file.txt
+t/data/zero-length.txt
 t/misc/brackup.conf

commit 38ee669ac3b9bd77cc2b58c8acedcf892ea1bb37
Merge: fc647da b20908d
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu Dec 23 16:14:47 2010 -0500

    Merge remote branch 'trunk'
    
    This folds our "skip identical contents" change into a new --conflict
    option, "identical"
    
    Conflicts:
    	lib/Brackup/Restore.pm
    	lib/Brackup/Target/Amazon.pm

diff --cc brackup-restore
index d48e2c7,ec65886..44e6e8b
--- a/brackup-restore
+++ b/brackup-restore
@@@ -42,6 -42,15 +42,17 @@@ Restore just the directory named (and a
  
  Restore just the file named.
  
 -=item --conflict=skip|overwrite|update
++=item --conflict=skip|overwrite|update|identical
+ 
+ How to handle files that already exist (with size > zero bytes). 
+ 'skip' means don't restore, keeping the existing file. 'overwrite'
+ means always restore, replacing the existing file. 'update' means
 -overwrite iff the file we are restoring is newer than the existing one.
++overwrite iff the file we are restoring is newer than the existing
++one.  'identical' means skip files iff their hash matches the expected
++value.
+ 
+ Default is to abort the restore if a file already exists.
+ 
  =item --config=NAME
  
  Brackup config file to use instead of default.
diff --cc lib/Brackup/Restore.pm
index baacb29,1181a1e..17a8357
--- a/lib/Brackup/Restore.pm
+++ b/lib/Brackup/Restore.pm
@@@ -6,9 -6,9 +6,10 @@@ use Digest::SHA1
  use POSIX qw(mkfifo);
  use Fcntl qw(O_RDONLY O_CREAT O_WRONLY O_TRUNC);
  use String::Escape qw(unprintable);
+ use File::stat;
  use Brackup::DecryptedFile;
  use Brackup::Decrypt;
 +use Brackup::Util qw(io_sha1);
  
  sub new {
      my ($class, %opts) = @_;
@@@ -245,6 -246,25 +247,27 @@@ sub _exec_statinfo_updates 
      }
  }
  
+ # Check if $self->{conflict} setting allows us to skip this item
+ sub _can_skip {
+     my ($self, $full, $it) = @_;
+ 
+     if ($self->{conflict} eq 'skip') {
+         return 1;
+     } elsif ($self->{conflict} eq 'overwrite') {
+         return 0;
+     } elsif ($self->{conflict} eq 'update') {
+         my $st = stat $full
+             or die "stat on '$full' failed: $!\n";
+         return 1 if defined $it->{Mtime} && $st->mtime >= $it->{Mtime};
++    } elsif ($self->{conflict} eq 'identical') {
++        return $self->_digest_matches($it, $full);
+     }
+     else {
+         die "Invalid '--conflict' setting '$self->{conflict}'\n";
+     }
+     return 0;
+ }
+ 
  sub _restore_directory {
      my ($self, $full, $it) = @_;
  

-----------------------------------------------------------------------



More information about the Bps-public-commit mailing list