diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/App/BorgRestore.pm | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/lib/App/BorgRestore.pm b/lib/App/BorgRestore.pm index 746dffc..4c67397 100644 --- a/lib/App/BorgRestore.pm +++ b/lib/App/BorgRestore.pm @@ -5,6 +5,19 @@ use warnings; our $VERSION = "2.0.0"; +use autodie; +use Cwd qw(abs_path getcwd); +use DateTime; +use File::Basename; +use File::Path qw(mkpath); +use File::Slurp; +use File::Spec; +use File::Temp; +use Getopt::Long; +use List::Util qw(any all); +use Pod::Usage; +use Time::HiRes; + =pod =encoding utf-8 @@ -62,6 +75,349 @@ sub debug { say STDERR @_ if $self->{opts}->{debug}; } +sub find_archives { + my $self = shift; + my $path = shift; + + my $db_path = $self->get_cache_path('archives.db'); + + my $db = $self->open_db($db_path); + + my %seen_modtime; + my @ret; + + $self->debug("Building unique archive list"); + + my $archives = $db->get_archives_for_path($path); + + for my $archive (@$archives) { + my $modtime = $archive->{modification_time}; + + if (defined($modtime) && (!$seen_modtime{$modtime}++)) { + push @ret, $archive; + } + } + + if (!@ret) { + printf "\e[0;91mWarning:\e[0m Path '%s' not found in any archive.\n", $path; + } + + @ret = sort { $a->{modification_time} cmp $b->{modification_time} } @ret; + + return \@ret; +} + +sub select_archive_timespec { + my $self = shift; + my $archives = shift; + my $timespec = shift; + + my $seconds = $self->timespec_to_seconds($timespec); + if (!defined($seconds)) { + say STDERR "Error: Invalid time specification"; + return; + } + + my $target_timestamp = time - $seconds; + + $self->debug("Searching for newest archive that contains a copy before ", $self->format_timestamp($target_timestamp)); + + for my $archive (reverse @$archives) { + if ($archive->{modification_time} < $target_timestamp) { + return $archive; + } + } + + return; +} + +sub format_timestamp { + my $self = shift; + my $timestamp = shift; + + state $timezone = DateTime::TimeZone->new( name => 'local' ); + my $dt = DateTime->from_epoch(epoch => $timestamp, time_zone => $timezone); + return $dt->strftime("%a. %F %H:%M:%S %z"); +} + +sub timespec_to_seconds { + my $self = shift; + my $timespec = shift; + + if ($timespec =~ m/^(?<value>[0-9.]+)(?<unit>.+)$/) { + my $value = $+{value}; + my $unit = $+{unit}; + + my %factors = ( + s => 1, + second => 1, + seconds => 1, + minute => 60, + minutes => 60, + h => 60*60, + hour => 60*60, + hours => 60*60, + d => 60*60*24, + day => 60*60*24, + days => 60*60*24, + m => 60*60*24*31, + month => 60*60*24*31, + months => 60*60*24*31, + y => 60*60*24*365, + year => 60*60*24*365, + years => 60*60*24*365, + ); + + if (exists($factors{$unit})) { + return $value * $factors{$unit}; + } + } + + return; +} + +sub restore { + my $self = shift; + my $path = shift; + my $archive = shift; + my $destination = shift; + + $destination = App::BorgRestore::Helper::untaint($destination, qr(.*)); + $path = App::BorgRestore::Helper::untaint($path, qr(.*)); + my $archive_name = App::BorgRestore::Helper::untaint_archive_name($archive->{archive}); + + printf "Restoring %s to %s from archive %s\n", $path, $destination, $archive->{archive}; + + my $basename = basename($path); + my $components_to_strip =()= $path =~ /\//g; + + $self->debug(sprintf("CWD is %s", getcwd())); + $self->debug(sprintf("Changing CWD to %s", $destination)); + mkdir($destination) unless -d $destination; + chdir($destination) or die "Failed to chdir: $!"; + + my $final_destination = abs_path($basename); + $final_destination = App::BorgRestore::Helper::untaint($final_destination, qr(.*)); + $self->debug("Removing ".$final_destination); + File::Path::remove_tree($final_destination); + App::BorgRestore::Borg::restore($components_to_strip, $archive_name, $path); +} + +sub get_cache_dir { + my $self = shift; + return "$App::BorgRestore::Settings::cache_path_base/v2"; +} + +sub get_cache_path { + my $self = shift; + my $item = shift; + return $self->get_cache_dir()."/$item"; +} + +sub get_temp_path { + my $self = shift; + my $item = shift; + + state $tempdir_obj = File::Temp->newdir(); + + my $tempdir = $tempdir_obj->dirname; + + return $tempdir."/".$item; +} + +sub add_path_to_hash { + my $self = shift; + my $hash = shift; + my $path = shift; + my $time = shift; + + my @components = split /\//, $path; + + my $node = $hash; + + if ($path eq ".") { + if ($time > $$node[1]) { + $$node[1] = $time; + } + return; + } + + # each node is an arrayref of the format [$hashref_of_children, $mtime] + # $hashref_of_children is undef if there are no children + for my $component (@components) { + if (!defined($$node[0]->{$component})) { + $$node[0]->{$component} = [undef, $time]; + } + # update mtime per child + if ($time > $$node[1]) { + $$node[1] = $time; + } + $node = $$node[0]->{$component}; + } +} + +sub get_missing_items { + my $self = shift; + my $have = shift; + my $want = shift; + + my $ret = []; + + for my $item (@$want) { + my $exists = any { $_ eq $item } @$have; + push @$ret, $item if not $exists; + } + + return $ret; +} + +sub handle_removed_archives { + my $self = shift; + my $db = shift; + my $borg_archives = shift; + + my $start = Time::HiRes::gettimeofday(); + + my $existing_archives = $db->get_archive_names(); + + # TODO this name is slightly confusing, but it works as expected and + # returns elements that are in the previous list, but missing in the new + # one + my $remove_archives = $self->get_missing_items($borg_archives, $existing_archives); + + if (@$remove_archives) { + for my $archive (@$remove_archives) { + $self->debug(sprintf("Removing archive %s", $archive)); + $db->begin_work; + $db->remove_archive($archive); + $db->commit; + $db->vacuum; + } + + my $end = Time::HiRes::gettimeofday(); + $self->debug(sprintf("Removing archives finished after: %.5fs", $end - $start)); + } +} + +sub handle_added_archives { + my $self = shift; + my $db = shift; + my $borg_archives = shift; + + my $archives = $db->get_archive_names(); + my $add_archives = $self->get_missing_items($archives, $borg_archives); + + for my $archive (@$add_archives) { + my $start = Time::HiRes::gettimeofday(); + my $lookuptable = [{}, 0]; + + $self->debug(sprintf("Adding archive %s", $archive)); + + my $proc = App::BorgRestore::Borg::list_archive($archive, \*OUT); + while (<OUT>) { + # roll our own parsing of timestamps for speed since we will be parsing + # a huge number of lines here + # example timestamp: "Wed, 2016-01-27 10:31:59" + if (m/^.{4} (?<year>....)-(?<month>..)-(?<day>..) (?<hour>..):(?<minute>..):(?<second>..) (?<path>.+)$/) { + my $time = POSIX::mktime($+{second},$+{minute},$+{hour},$+{day},$+{month}-1,$+{year}-1900); + #$self->debug(sprintf("Adding path %s with time %s", $+{path}, $time)); + $self->add_path_to_hash($lookuptable, $+{path}, $time); + } + } + $proc->finish() or die "borg list returned $?"; + + $self->debug(sprintf("Finished parsing borg output after %.5fs. Adding to db", Time::HiRes::gettimeofday - $start)); + + $db->begin_work; + $db->add_archive_name($archive); + my $archive_id = $db->get_archive_id($archive); + $self->save_node($db, $archive_id, undef, $lookuptable); + $db->commit; + $db->vacuum; + + my $end = Time::HiRes::gettimeofday(); + $self->debug(sprintf("Adding archive finished after: %.5fs", $end - $start)); + } +} + +sub build_archive_cache { + my $self = shift; + my $borg_archives = App::BorgRestore::Borg::borg_list(); + my $db_path = $self->get_cache_path('archives.db'); + + # ensure the cache directory exists + mkpath($self->get_cache_dir(), {mode => oct(700)}); + + if (! -f $db_path) { + $self->debug("Creating initial database"); + my $db = $self->open_db($db_path); + $db->initialize_db(); + } + + my $db = $self->open_db($db_path); + + my $archives = $db->get_archive_names(); + + $self->debug(sprintf("Found %d archives in db", scalar(@$archives))); + + $self->handle_removed_archives($db, $borg_archives); + $self->handle_added_archives($db, $borg_archives); + + if ($self->{opts}->{debug}) { + $self->debug(sprintf("DB contains information for %d archives in %d rows", scalar(@{$db->get_archive_names()}), $db->get_archive_row_count())); + } +} + +sub open_db { + my $self = shift; + my $db_path = shift; + + return App::BorgRestore::DB->new($db_path); +} + +sub save_node { + my $self = shift; + my $db = shift; + my $archive_id = shift; + my $prefix = shift; + my $node = shift; + + for my $child (keys %{$$node[0]}) { + my $path; + $path = $prefix."/" if defined($prefix); + $path .= $child; + + my $time = $$node[0]->{$child}[1]; + $db->add_path($archive_id, $path, $time); + + save_node($db, $archive_id, $path, $$node[0]->{$child}); + } +} + +sub get_mtime_from_lookuptable { + my $self = shift; + my $lookuptable = shift; + my $path = shift; + + my @components = split /\//, $path; + my $node = $lookuptable; + + for my $component (@components) { + $node = $$node[0]->{$component}; + if (!defined($node)) { + return; + } + } + return $$node[1]; +} + +sub update_cache { + my $self = shift; + $self->debug("Checking if cache is complete"); + $self->build_archive_cache(); + $self->debug("Cache complete"); +} + 1; __END__ |