#! @PERL@
# Copyright (c) 2009-2012 Zmanda, Inc.  All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com

use lib '@amperldir@';
use strict;
use warnings;

use Getopt::Long;

use Amanda::Device qw( :constants );
use Amanda::Debug qw( :logging );
use Amanda::Config qw( :init :getconf config_dir_relative );
use Amanda::Util qw( :constants );
use Amanda::Changer;
use Amanda::Constants;
use Amanda::MainLoop;
use Amanda::MainLoop qw( :GIOCondition );
use Amanda::Header;
use Amanda::Holding;
use Amanda::Cmdline;
use Amanda::Tapelist;
use Amanda::Xfer qw( :constants );

sub usage {
    my ($msg) = @_;
    print STDERR "$msg\n" if $msg;
    print STDERR <<EOF;
Usage: amrestore [--config config] [-b blocksize] [-r|-c|-C] [-p] [-h]
    [-f filenum] [-l label] [--exact-match] [-o configoption]*
    [--continue-on-filter-error]
    {device | [--holding] holdingfile}
    [hostname [diskname [datestamp [hostname [diskname [datestamp ... ]]]]]]"));
EOF
    exit(1);
}

##
# main

Amanda::Util::setup_application("amrestore", "server", $CONTEXT_CMDLINE, "amanda", "amanda");

my $config_overrides = new_config_overrides($#ARGV+1);

my ($opt_config, $opt_blocksize, $opt_raw, $opt_compress, $opt_compress_best,
    $opt_pipe, $opt_header, $opt_filenum, $opt_label, $opt_holding,
    $opt_restore_src, $opt_exact_match);

my $opt_continue_on_filter_error = 0;

debug("Arguments: " . join(' ', @ARGV));
Getopt::Long::Configure(qw(bundling));
GetOptions(
    'version' => \&Amanda::Util::version_opt,
    'help|usage|?' => \&usage,
    'config=s' => \$opt_config,
    'holding' => \$opt_holding,
    'exact-match' => \$opt_exact_match,
    'continue-on-filter-error' => \$opt_continue_on_filter_error,
    'b=i' => \$opt_blocksize,
    'r' => \$opt_raw,
    'c' => \$opt_compress,
    'C' => \$opt_compress_best,
    'p' => \$opt_pipe,
    'h' => \$opt_header,
    'f=i' => \$opt_filenum,
    'l=s' => \$opt_label,
    'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
) or usage();

$opt_compress = 1 if $opt_compress_best;

# see if we have a holding file or a device
usage("Must specify a device or holding-disk file") unless (@ARGV);
$opt_restore_src = shift @ARGV;
if (!$opt_holding) {
    $opt_holding = 1
	if (Amanda::Holding::get_header($opt_restore_src));
}

my $cmd_flags = $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP;
$cmd_flags |= $Amanda::Cmdline::CMDLINE_EXACT_MATCH if $opt_exact_match;
my @opt_dumpspecs = Amanda::Cmdline::parse_dumpspecs([@ARGV], $cmd_flags);

usage("Cannot check a label on a holding-disk file")
    if ($opt_holding and $opt_label);
usage("Cannot use both -r (raw) and -c/-C (compression) -- use -h instead")
    if ($opt_raw and $opt_compress);

# -r implies -h, plus appending ".RAW" to filenames
$opt_header = 1 if $opt_raw;

set_config_overrides($config_overrides);
if ($opt_config) {
    config_init_with_global($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
} else {
    config_init($CONFIG_INIT_GLOBAL, undef);
}
my ($cfgerr_level, @cfgerr_errors) = config_errors();
if ($cfgerr_level >= $CFGERR_WARNINGS) {
    config_print_errors();
    if ($cfgerr_level >= $CFGERR_ERRORS) {
	die("errors processing config file");
    }
}

Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);

my $exit_status = 0;
my $res;

sub failure {
    my ($msg, $finished_cb) = @_;
    print STDERR "ERROR: $msg\n";
    $exit_status = 1;
    if ($res) {
	$res->release(finished_cb => sub {
	    # ignore error
	    $finished_cb->();
	});
    } else {
	$finished_cb->();
    }
}

sub main {
    my ($finished_cb) = @_;

    my $dev;
    my $hdr;
    my $chg;
    my $filenum = $opt_filenum;
    $filenum = 1 if (!$filenum);
    $filenum = 0 + "$filenum"; # convert to integer
    my %all_filter;
    my $restore_done;
    my $restore_err;

    my $steps = define_steps
	cb_ref => \$finished_cb,
	finalize => sub { $chg->quit() if defined $chg };

    step start => sub {
	# first, return to the original working directory we were started in
	if (!chdir Amanda::Util::get_original_cwd()) {
	    return failure("Cannot chdir to original working directory", $finished_cb);
	}

	if ($opt_holding) {
	    $steps->{'read_header'}->();
	} else {
	    my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
	    my ($tl, $message) = Amanda::Tapelist->new($tlf);
	    if (defined $message) {
		return failure($message, $finished_cb);
	    }
	    $chg = Amanda::Changer->new($opt_restore_src, tapelist => $tl);
	    if ($chg->isa("Amanda::Changer::Error")) {
		return failure($chg, $finished_cb);
	    }

	    $chg->load(relative_slot => "current", mode => "read",
		res_cb => $steps->{'slot_loaded'});
	}
    };

    step slot_loaded => sub {
	(my $err, $res) = @_;
	return failure($err, $finished_cb) if $err;

	$dev = $res->{'device'};

	if ($opt_blocksize) {
	    if ((my $r = $dev->property_set("BLOCK_SIZE", $opt_blocksize))) {
		return failure($r, $finished_cb);
	    }

	    # re-read the label with the correct blocksize
	    $dev->read_label();
	}

	if ($dev->status != $DEVICE_STATUS_SUCCESS) {
	    return failure($dev->error_or_status, $finished_cb);
	}

	$steps->{'check_label'}->();
    };

    step check_label => sub {
	if ($dev->status != $DEVICE_STATUS_SUCCESS) {
	    return failure($dev->error_or_status, $finished_cb);
	}

	if ($opt_label) {
	    if ($dev->volume_label ne $opt_label) {
		my $got = $dev->volume_label;
		return failure("Found unexpected label '$got'", $finished_cb);
	    }
	}

	my $lbl = $dev->volume_label;
	print STDERR "Restoring from tape $lbl starting with file $filenum.\n";

	$steps->{'start_device'}->();
    };

    step start_device => sub {
	if (!$dev->start($ACCESS_READ, undef, undef)) {
	    return failure($dev->error_or_status(), $finished_cb);
	}

	$steps->{'read_header'}->();
    };

    step read_header => sub {
	$restore_done = 0;
	$restore_err = undef;
	if ($opt_holding) {
	    print STDERR "Reading from '$opt_restore_src'\n";
	    $hdr = Amanda::Holding::get_header($opt_restore_src);
	} else {
	    $hdr = $dev->seek_file($filenum);
	    if (!$hdr) {
		return failure("while reading next header: " . $dev->error_or_status(),
			    $finished_cb);
	    } elsif ($hdr->{'type'} == $Amanda::Header::F_TAPEEND) {
		return $steps->{'finished'}->();
	    }

	    # seek_file may have skipped ahead; plan accordingly
	    $filenum = $dev->file + 1;
	}

	$steps->{'filter_dumpspecs'}->();
    };

    step filter_dumpspecs => sub {
	if (@opt_dumpspecs and not $hdr->matches_dumpspecs([@opt_dumpspecs])) {
	    if (!$opt_holding) {
		my $dev_filenum = $dev->file;
		print STDERR "amrestore: $dev_filenum: skipping ",
			$hdr->summary(), "\n";
	    }

	    # skip to the next file without restoring this one
	    return $steps->{'next_file'}->();
	}

	if (!$opt_holding) {
	    my $dev_filenum = $dev->file;
	    print STDERR "amrestore: $dev_filenum: restoring ";
	}
	print STDERR $hdr->summary(), "\n";

	$steps->{'xfer_dumpfile'}->();
    };

    step xfer_dumpfile => sub {
	my ($src, $dest);

	# set up the source..
	if ($opt_holding) {
	    $src = Amanda::Xfer::Source::Holding->new($opt_restore_src);
	    $src->start_recovery();
	} else {
	    $src = Amanda::Xfer::Source::Device->new($dev);
	}

	# and set up the destination..
	my $dest_fh;
	if ($opt_pipe) {
	    $dest_fh = \*STDOUT;
	} else {
	    my $filename = sprintf("%s.%s.%s.%d",
		    $hdr->{'name'},
		    Amanda::Util::sanitise_filename("".$hdr->{'disk'}), # workaround SWIG bug
		    $hdr->{'datestamp'},
		    $hdr->{'dumplevel'});
	    if ($hdr->{'partnum'} > 0) {
		$filename .= sprintf(".%07d", $hdr->{'partnum'});
	    }

	    # add an appropriate suffix
	    if ($opt_raw) {
		$filename .= ".RAW";
	    } elsif ($opt_compress) {
		$filename .= ($hdr->{'compressed'} && $hdr->{'comp_suffix'})?
		    $hdr->{'comp_suffix'} : $Amanda::Constants::COMPRESS_SUFFIX;
	    }

	    if (!open($dest_fh, ">", $filename)) {
		return failure("Could not open '$filename' for writing: $!", $finished_cb);
	    }
	}
	$dest = Amanda::Xfer::Dest::Fd->new($dest_fh);

	# set up any filters that need to be applied, decryption first
	my @filters;
	if ($hdr->{'encrypted'} and not $opt_raw) {
	    if ($hdr->{'srv_encrypt'}) {
		push @filters,
		    Amanda::Xfer::Filter::Process->new(
			[ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0, 0, 0, 1);
	    } elsif ($hdr->{'clnt_encrypt'}) {
		push @filters,
		    Amanda::Xfer::Filter::Process->new(
			[ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0, 0, 0, 1);
	    } else {
		return failure("could not decrypt encrypted dump: no program specified",
			    $finished_cb);
	    }

	    $hdr->{'encrypted'} = 0;
	    $hdr->{'srv_encrypt'} = '';
	    $hdr->{'srv_decrypt_opt'} = '';
	    $hdr->{'clnt_encrypt'} = '';
	    $hdr->{'clnt_decrypt_opt'} = '';
	    $hdr->{'encrypt_suffix'} = 'N';
	}
	if (!$opt_raw and $hdr->{'compressed'} and not $opt_compress) {
	    # need to uncompress this file

	    if ($hdr->{'srvcompprog'}) {
		# TODO: this assumes that srvcompprog takes "-d" to decompress
		push @filters,
		    Amanda::Xfer::Filter::Process->new(
			[ $hdr->{'srvcompprog'}, "-d" ], 0, 0, 0, 1);
	    } elsif ($hdr->{'clntcompprog'}) {
		# TODO: this assumes that clntcompprog takes "-d" to decompress
		push @filters,
		    Amanda::Xfer::Filter::Process->new(
			[ $hdr->{'clntcompprog'}, "-d" ], 0, 0, 0, 1);
	    } else {
		push @filters,
		    Amanda::Xfer::Filter::Process->new(
			[ $Amanda::Constants::UNCOMPRESS_PATH,
			  $Amanda::Constants::UNCOMPRESS_OPT ], 0, 0, 0, 1);
	    }
	    
	    # adjust the header
	    $hdr->{'compressed'} = 0;
	    $hdr->{'uncompress_cmd'} = '';
	} elsif (!$opt_raw and !$hdr->{'compressed'} and $opt_compress) {
	    # need to compress this file

	    my $compress_opt = $opt_compress_best?
		$Amanda::Constants::COMPRESS_BEST_OPT :
		$Amanda::Constants::COMPRESS_FAST_OPT;
	    @filters = (
		Amanda::Xfer::Filter::Process->new(
		    [ $Amanda::Constants::COMPRESS_PATH,
		      $compress_opt ], 0, 0, 0, 1),
	    );
	    
	    # adjust the header
	    $hdr->{'compressed'} = 1;
	    $hdr->{'uncompress_cmd'} = " $Amanda::Constants::UNCOMPRESS_PATH " .
		"$Amanda::Constants::UNCOMPRESS_OPT |";
	    $hdr->{'comp_suffix'} = $Amanda::Constants::COMPRESS_SUFFIX;
	}

	# write the header to the destination if requested
	if ($opt_header) {
	    $hdr->{'blocksize'} = Amanda::Holding::DISK_BLOCK_BYTES;
	    $dest_fh->syswrite($hdr->to_string(32768, 32768));
	}

	# start reading all filter stderr
	foreach my $filter (@filters) {
	    my $fd = $filter->get_stderr_fd();
	    $fd.="";
	    $fd = int($fd);
	    my $src = Amanda::MainLoop::fd_source($fd,
						  $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
	    my $buffer = "";
	    $all_filter{$src} = 1;
	    $src->set_callback( sub {
		my $b;
		my $n_read = POSIX::read($fd, $b, 1);
		if (!defined $n_read) {
		    return;
		} elsif ($n_read == 0) {
		    delete $all_filter{$src};
		    $src->remove();
		    POSIX::close($fd);
		    if (!%all_filter and $restore_done) {
			$steps->{'xfer_filter_done'}->();
		    }
		} else {
		    $buffer .= $b;
		    if ($b eq "\n") {
			my $line = $buffer;
			print STDERR "filter stderr: $line";
			chomp $line;
			debug("filter stderr: $line");
			$buffer = "";
		    }
		}
	    });
	}
	
	my $xfer = Amanda::Xfer->new([ $src, @filters, $dest ]);
	my $got_err = undef;
	$xfer->get_source()->set_callback(sub {
	    my ($msg_src, $msg, $xfer) = @_;

	    if ($msg->{'type'} == $XMSG_INFO) {
		Amanda::Debug::info($msg->{'message'});
	    } elsif ($msg->{'type'} == $XMSG_ERROR) {
		if ((defined $src && $msg->{'elt'} == $src) ||
		    (defined $dest && $msg->{'elt'} == $dest)) {
		    $got_err = $msg->{'message'};
		} elsif (!$opt_continue_on_filter_error) {
		    $got_err = $msg->{'message'};
		} else {
		    print STDERR "ERROR: $msg->{'message'}\n";
		}
	    } elsif ($msg->{'type'} == $XMSG_DONE) {
		$msg_src->remove();
		$steps->{'xfer_done'}->($got_err);
	    }
	});
	$xfer->start();
    };

    step xfer_done => sub {
	my ($err) = @_;
	$restore_err = $err;
	$restore_done = 1;
	return if %all_filter;

	$steps->{'xfer_filter_done'}->();
     };

    step xfer_filter_done => sub {
	return failure($restore_err, $finished_cb) if $restore_err;

	$steps->{'next_file'}->('extracted');
    };

    step next_file => sub {
	my ($extracted) = @_;
	# amrestore does not loop over multiple files when reading from holding
	# when outputting to a pipe amrestore extracts only the first file
	if ($opt_holding or ($opt_pipe and $extracted)) {
	    return $steps->{'finished'}->();
	}

	# otherwise, try to read the next header from the device
	$steps->{'read_header'}->();
    };

    step finished => sub {
	if ($res) {
	    $res->release(finished_cb => $steps->{'quit'});
	} else {
	    $steps->{'quit'}->();
	}
    };

    step quit => sub {
	my ($err) = @_;
	$res = undef;
	return failure($err, $finished_cb) if $err;

	$finished_cb->();
    };
}
main(\&Amanda::MainLoop::quit);
Amanda::MainLoop::run();
Amanda::Util::finish_application();
exit $exit_status;
