package Brackup::Restore;
use strict;
use warnings;
use Carp qw(croak);
use Digest::SHA1;
use Brackup::Util qw(tempfile slurp valid_params);
sub new {
my ($class, %opts) = @_;
my $self = bless {}, $class;
$self->{to} = delete $opts{to}; # directory we're restoring to
$self->{prefix} = delete $opts{prefix}; # directory/file filename prefix, or "" for all
$self->{file} = delete $opts{file}; # filename we're restoring from
$self->{verbose} = delete $opts{verbose};
$self->{prefix} =~ s!/$!! if $self->{prefix};
$self->{_stats_to_run} = []; # stack (push/pop) of subrefs to reset stat info on
die "Destination directory doesn't exist" unless $self->{to} && -d $self->{to};
die "Backup file doesn't exist" unless $self->{file} && -f $self->{file};
croak("Unknown options: " . join(', ', keys %opts)) if %opts;
$self->decrypt_file_if_needed;
return $self;
}
# returns a hashref of { "foo" => "bar" } from { ..., "Driver-foo" => "bar" }
sub _driver_meta {
my $src = shift;
my $ret = {};
foreach my $k (keys %$src) {
next unless $k =~ /^Driver-(.+)/;
$ret->{$1} = $src->{$k};
}
return $ret;
}
sub decrypt_file_if_needed {
my $self = shift;
my $meta = slurp($self->{file});
if ($meta =~ /[\x00-\x08]/) { # silly is-binary heuristic
my $new_file = $self->_decrypt_data(\$meta,
want_tempfile => 1,
no_batch => 1,
);
warn "Decrypted metafile $self->{file} to $new_file; using that to restore from...\n";
$self->{file} = $new_file;
scalar <STDIN>;
}
}
sub restore {
my ($self) = @_;
my $parser = $self->parser;
my $meta = $parser->readline;
my $driver_class = $meta->{BackupDriver};
die "No driver specified" unless $driver_class;
my $driver_meta = _driver_meta($meta);
eval "use $driver_class; 1;" or die
"Failed to load driver ($driver_class) to restore from: $@\n";
my $target = eval {"$driver_class"->new_from_backup_header($driver_meta); };
if ($@) {
die "Failed to instantiate target ($driver_class) for restore, perhaps it doesn't support restoring yet?\n\nThe error was: $@";
}
$self->{_target} = $target;
$self->{_meta} = $meta;
while (my $it = $parser->readline) {
if ($self->{prefix}) {
next unless $it->{Path} =~ s/^\Q$self->{prefix}\E(?:\/|$)//;
}
my $full = $self->{to} . "/" . $it->{Path};
my $type = $it->{Type} || "f";
die "Unknown filetype: type=$type, file: $it->{Path}" unless $type =~ /^[ldf]$/;
# restore default modes from header
$it->{Mode} ||= $meta->{DefaultFileMode} if $type eq "f";
$it->{Mode} ||= $meta->{DefaultDirMode} if $type eq "d";
warn " * restoring $it->{Path} to $full\n" if $self->{verbose};
$self->_restore_link ($full, $it) if $type eq "l";
$self->_restore_directory($full, $it) if $type eq "d";
$self->_restore_file ($full, $it) if $type eq "f";
}
warn " * fixing stat info\n" if $self->{verbose};
$self->_exec_statinfo_updates;
warn " * done\n" if $self->{verbose};
return 1;
}
sub _output_temp_filename {
my $self = shift;
return $self->{_output_temp_filename} ||= ( (tempfile())[1] || die );
}
sub _encrypted_temp_filename {
my $self = shift;
return $self->{_encrypted_temp_filename} ||= ( (tempfile())[1] || die );
}
sub _decrypt_data {
my $self = shift;
my $dataref = shift;
my %opts = valid_params(['no_batch', 'want_tempfile'], @_);
# find which key we're decrypting it using, else return chunk
# unmodified in the not-encrypted case
if ($self->{_meta}) {
my $rcpt = $self->{_meta}{"GPG-Recipient"} or
return $dataref;
}
unless ($ENV{'GPG_AGENT_INFO'} ||
@Brackup::GPG_ARGS ||
$self->{warned_about_gpg_agent}++)
{
my $err = q{#
# WARNING: trying to restore encrypted files,
# but $ENV{'GPG_AGENT_INFO'} not present.
# Are you running gpg-agent?
#
};
$err =~ s/^\s+//gm;
warn $err;
}
my $output_temp = $opts{want_tempfile} ?
(tempfile())[1]
: $self->_output_temp_filename;
my $enc_temp = $self->_encrypted_temp_filename;
_write_to_file($enc_temp, $dataref);
my @list = ("gpg", @Brackup::GPG_ARGS,
"--use-agent",
!$opts{no_batch} ? ("--batch") : (),
"--trust-model=always",
"--output", $output_temp,
"--yes", "--quiet",
"--decrypt", $enc_temp);
system(@list)
and die "Failed to decrypt with gpg: $!\n";
return $output_temp if $opts{want_tempfile};
my $data = Brackup::Util::slurp($output_temp);
return \$data;
}
sub _update_statinfo {
my ($self, $full, $it) = @_;
push @{ $self->{_stats_to_run} }, sub {
if (defined $it->{Mode}) {
chmod(oct $it->{Mode}, $full) or
die "Failed to change mode of $full: $!";
}
if ($it->{Mtime} || $it->{Atime}) {
utime($it->{Atime}, $it->{Mtime}, $full) or
die "Failed to change utime of $full: $!";
}
};
}
sub _exec_statinfo_updates {
my $self = shift;
# change the modes/times in backwards order, going from deep
# files/directories to shallow ones. (so we can reliably change
# all the directory mtimes without kernel doing it for us when we
# modify files deeper)
while (my $sb = pop @{ $self->{_stats_to_run} }) {
$sb->();
}
}
sub _restore_directory {
my ($self, $full, $it) = @_;
unless (-d $full) {
mkdir $full or # FIXME: permissions on directory
die "Failed to make directory: $full ($it->{Path})";
}
$self->_update_statinfo($full, $it);
}
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.";
}
symlink $it->{Link}, $full
or die "Failed to link";
}
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.";
}
open (my $fh, ">$full") or die "Failed to open $full for writing";
my @chunks = grep { $_ } split(/\s+/, $it->{Chunks} || "");
foreach my $ch (@chunks) {
my ($offset, $len, $enc_len, $dig) = split(/;/, $ch);
my $dataref = $self->{_target}->load_chunk($dig) or
die "Error loading chunk $dig from the restore target\n";
my $len_chunk = length $$dataref;
# using just a range of the file
# TODO: inefficient! we don't want to download the chunk from the
# target multiple times. better to cache it locally, or at least
# only fetch a region from the target (but that's still kinda inefficient
# and pushes complexity into the Target interface)
if ($enc_len =~ /^(\d+)-(\d+)$/) {
my ($from, $to) = ($1, $2);
# file range. gotta be at least as big as bigger number
unless ($len_chunk >= $to) {
die "Backup chunk $dig isn't at least as big as range: got $len_chunk, needing $to\n";
}
my $region = substr($$dataref, $from, $to-$from);
$dataref = \$region;
} else {
# using the whole chunk, so make sure fetched size matches
# expected size
unless ($len_chunk == $enc_len) {
die "Backup chunk $dig isn't of expected length: got $len_chunk, expecting $enc_len\n";
}
}
my $decrypted_ref = $self->_decrypt_data($dataref);
print $fh $$decrypted_ref;
}
close($fh) or die "Close failed";
if (my $good_dig = $it->{Digest}) {
die "not capable of verifying digests of from anything but sha1"
unless $good_dig =~ /^sha1:(.+)/;
$good_dig = $1;
open (my $readfh, $full) or die "Couldn't reopen file for verification";
my $sha1 = Digest::SHA1->new;
$sha1->addfile($readfh);
my $actual_dig = $sha1->hexdigest;
# TODO: support --onerror={continue,prompt}, etc, but for now we just die
unless ($actual_dig eq $good_dig || $full =~ m!\.brackup-digest\.db\b!) {
die "Digest of restored file ($full) doesn't match";
}
}
$self->_update_statinfo($full, $it);
}
# returns iterator subref which returns hashrefs or undef on EOF
sub parser {
my $self = shift;
return Brackup::Metafile->open($self->{file});
}
sub _write_to_file {
my ($file, $ref) = @_;
open (my $fh, ">$file") or die "Failed to open $file for writing: $!\n";
print $fh $$ref;
close($fh) or die;
die unless -s $file == length $$ref;
return 1;
}
sub DESTROY {
my $self = shift;
unlink(grep { $_ } ($self->{_output_temp_filename},
$self->{_encrypted_temp_filename}));
}
1;
syntax highlighted by Code2HTML, v. 0.9.1