#!/usr/bin/perl

use strict;
use warnings;

# Searching for Backup/Migration libs

my $agentsShareDir;

BEGIN {
  sub getProductRootD {
    my $configFile = "/etc/psa/psa.conf";
    if (-f $configFile) {
      my $productRootD;
      open CONFIG_FILE, "$configFile";
      while (<CONFIG_FILE>) {
        next if /^#/;
        if (/^(\S+)\s+(.*?)\s*$/) {
          my ($key, $value) = ($1, $2);
          if ($key eq "PRODUCT_ROOT_D") {
            $productRootD = $value;
            last;
          }
        }
      }
      close CONFIG_FILE;
      return $productRootD;
    }
  }

  my $productRootD = getProductRootD();

  if (exists $ENV{'LOCALLIB'}) {
    $agentsShareDir = ".";
    unshift @INC, $agentsShareDir;
  } else {
    if ($productRootD) {
      $agentsShareDir = "$productRootD/PMM/agents/shared";
      unshift @INC, $agentsShareDir;
      unshift @INC, "$productRootD/PMM/agents/PleskX";
    }
  }
}

use Getopt::Long;
use PleskX;
use Logging;
use Error qw|:try|;

sub usage {
  my $exitcode = shift;

  my $usage = <<EOF;
Usage: pleskbackup [<global-options>] <command> [<local-options>] <arguments>
                    <output-file>

Global options:
  -v|--verbose
                 Show more information about backup process. Multiple -v
                 options increase verbosity.

  -c|--configuration
                 Backup only configuration of objects, not the content.

  -s|--split[=<size>]
                 Split the generated backups to the parts. Parts are numbered
                 by appending .NNN suffixes, starting with .001.

                 Size may be specified in kilobytes (<nn>K), megabytes (<nn>M)
                 and gigabytes (<nn>G).

                 '-s' option without argument selects default split size:
                 2 gigabytes.

  -z|--no-gzip   Do not compress backup file

Commands:
  all            Backs up whole Plesk.

  clients        Backs up selected clients. Clients are read from command line,
                 space-separated. If no clients provided, backs up all clients
                 on the host.

  domains        Backs up selected domains. Domains are read from command line,
                 space-separated. If no domains provided, backs up all domains
                 on the host.

                 Options --exclude and --exclude-file may be specified for
                 excluding some clients/domains.

  help           Shows this help page

Local options:
  -f|--from-file=<file>
                 Read list of domains/clients from file, not from command line.
                 File should contain list of domains/clients one per line.

  --exclude=<obj1>,<obj2>,...
                 Excludes listed domains/clients from backup list.

  --exclude-file=<file>
                 Excludes domains/clients listed in file from backup list.
                 File should contain list of domains/clients one per line.

  --skip-logs    Do not save log files in the backup file 

Output file:
  /fullpath/filename - regular file,

  -                  - use stdout for output,

  ftp://[<login>[:<password>]@]<server>/<filepath> - storing the backup to ftp server.
  FTP_PASSWORD environment variable can be used for setting password.
 
EOF

  if ($exitcode) {
    print STDERR $usage;
  } else {
    print $usage;
  }

  exit $exitcode;
}

sub isOldOption {
  my $s = shift;
  return ($s eq '--all' or $s eq '--clients' or $s eq '--domains');
}

#
# Options parsing compatible with 8.0 pleskbackup style
#

sub parseOldOptions {
  my %res;
  my $allBackupFileName;
  my $clientsBackupFileName;
  my $domainsBackupFileName;
  my $help;
  my $listFile;

  $res{'verbose'} = 0;
  $res{'gzip'} = 1;

  print STDERR "*** You are using old-style pleskbackup command-line interface.\n";
  print STDERR "*** Consider switching to new style documented in 'pleskbackup help',\n";
  print STDERR "*** because old style eventually will be dropped.\n";

  my $result = GetOptions("all=s" => \$allBackupFileName,
                          "clients=s" => \$clientsBackupFileName,
                          "domains=s" => \$domainsBackupFileName,
                          "help|h" => \$help,
                          "no-content" => sub { $res{'no-content'} = 1 },
                          "only-mail" => sub { $res{'only-mail'} = 1 },
                          "list=s" => \$listFile,
                          "verbose" => sub { $res{'verbose'} = 1 });

  usage(1) unless $result;
  usage(0) if $help;

  my $commands = 0;
  $commands++ if $allBackupFileName;
  $commands++ if $clientsBackupFileName;
  $commands++ if $domainsBackupFileName;

  die "More than one command specified" if $commands > 1;

  if ($allBackupFileName) {
    $res{'backup-file'} = $allBackupFileName;
    $res{'clients-all'} = $res{'server'} = 1;
    return %res;
  }

  if ($clientsBackupFileName) {
    $res{'backup-file'} = $clientsBackupFileName;
    if ($listFile) {
      my @objects = readObjects($listFile);
      $res{'clients'} = \@objects;
    } else {
      $res{'clients-all'} = 1;
    }
    return %res;
  }

  if ($domainsBackupFileName) {
    $res{'backup-file'} = $domainsBackupFileName;
    if ($listFile) {
      my @objects = readObjects($listFile);
      $res{'clients'} = \@objects;
    } else {
      $res{'domains-all'} = 1;
    }
    return %res;
  }
}

#
# Returns hash:
#
# 'clients-all' => 1
# or
# 'clients' => ['clienta', 'clientb']
# or
# 'domains-all' => 1
# or
# 'domains' => ['clienta', 'clientb']
#
# verbose => [0..5]
# configuration => bool
# only-mail => bool (only with 'clients*')
# backup-file => string
# split-size => int
#

# Split by 2G - 1M by default, for broken FTP/HTTP implementations
my $defaultSplitSize = 2**31 - 2**20;

sub parseOptions {
  usage(0) unless @ARGV;
  usage(0) if $ARGV[0] eq "--help";
  return parseOldOptions() if isOldOption($ARGV[0]);

  my $globalOptParser = new Getopt::Long::Parser(config
                                                 => ['require_order', 'bundling']);

  my %res;
  $res{'verbose'} = 0;
  $res{'gzip'} = 1;

  my $split;

  if (!$globalOptParser->getoptions("verbose|v" => sub { $res{'verbose'} += 1 },
                                    "configuration|c" => sub { $res{'configuration'} = 1 },
                                    "split|s:s" => \$split,
                                    "no-gzip|z" => sub { $res{'gzip'} = 0 }))
  {
    usage(1);
  }

  my $command = '';

  if (defined $split) {
    my $size = parseSize($split);

    $res{'split-size'} = $size < 10000 ? $defaultSplitSize : $size;
    $command = $split if !$size;
  }

  if (!$command) {
    die "No command in command line" unless (@ARGV);
    $command = shift @ARGV;
  }

  usage(0) if $command eq "help";

  die "No backup filename in command line" unless (@ARGV);

  $res{'backup-file'} = pop @ARGV;

  if (substr($res{'backup-file'}, 0, 6) eq 'ftp://') {
	if ($res{'backup-file'} =~ /^ftp:\/\/(?:([^:@]*)(?::([^:]*))?@)?([^\/:@]+)\/(.*?)([^\/]+)$/) {
		my %ftp;

		$ftp{'login'} = defined $1 ? $1 : '';
		$ftp{'password'} = defined $2 ? $2 : '';

		if ($ftp{'password'} eq '' && defined $ENV{'FTP_PASSWORD'}) {
			$ftp{'password'} = $ENV{'FTP_PASSWORD'};
		}

		if ($ftp{'login'} eq '') {
			$ftp{'login'} = 'anonymous';
			$ftp{'password'} = 'plesk@plesk.com' if ($ftp{'password'} eq '');
		}

		die 'FTP password is not specified' if ($ftp{'password'} eq '');

		$ftp{'server'} = $3;
		$ftp{'path'} = $4;
		$ftp{'file'} = $5;
		$res{'backup-file'} = "/tmp/$5";

		$res{'ftp'} = \%ftp;
	}
	else {
		die 'Bad FTP file format';
	}
  }


  if ($command eq "clients" or $command eq "domains" or $command eq "all") {
    my $localOptParser = new Getopt::Long::Parser(config => ['bundling']);

    my ($objectsFromFileName, $excludeFileName, $excludeList);

    my @options = ("from-file|f=s" => \$objectsFromFileName,
                   "only-mail" => sub { $res{'only-mail'} = 1 },
                   "exclude-file=s" => \$excludeFileName,
                   "exclude=s" => \$res{'exclude'},
                   "skip-logs" => sub { $res{'skip-logs'} = 1 }
                 );

    usage(1) unless $localOptParser->getoptions(@options);

    $res{'exclude'} = [split(/,/, $res{'exclude'})] if ($res{'exclude'});

    if ($excludeFileName) {
      my @objects = readObjects($excludeFileName);
      push @{$res{'exclude'}}, @objects;
    }

    if ($command eq "all") {
	   die "'from-file' option should not be specified with 'all' command" if $objectsFromFileName;
	   die "'only-mail' option should not be specified with 'all' command" if $res{'only-mail'};

       $res{'clients-all'} = $res{'server'} = 1;
	}

    die "Both file containing $command and $command in command line specified"
      if @ARGV and $objectsFromFileName;

    return %res, "$command" => \@ARGV if @ARGV;
    if ($objectsFromFileName) {
      my @objects = readObjects($objectsFromFileName);
      return %res, "$command" => \@objects;
    }
    return %res, "$command-all" => 1;
  }

  die "Unknown command '$command'";
}

my %multipliers = ( '' => 1,
                    'k' => 1024,
                    'm' => 1024*1024,
                    'g' => 1024*1024*1024,
                    't' => 1024*1024*1024*1024 );

sub parseSize {
  my ($size) = @_;
  if ($size =~ /^(\d+)([kmgt]?)$/i) {
    return $1 * $multipliers{lc($2)};
  }
}

sub readObjects {
  my ($filename) = @_;
  open OBJECTS, "$filename" or die "Unable to open $filename";
  my @objects = <OBJECTS>;
  chomp @objects;
  close OBJECTS;
  return @objects;
}

sub perform {
  my (%settings) = @_;

  if ($settings{'backup-file'} eq '-' and $settings{'split-size'}) {
    die "Unable to split backup directed to stdout";
  }

  if ($settings{'verbose'} > 1) {
    Logging::setVerbosity($settings{'verbose'} > 2 ? 5 : $settings{'verbose'});
  } else {
    Logging::setVerbosity(1);
  }

  my $storage = Storage::Storage::createMimeStorage($settings{'gzip'}, $settings{'backup-file'},
                                                    $settings{'split-size'});
  my $status = DumpStatus::createBackup();

  if ($settings{'verbose'} > 0) {
    $status->setOutput(\*STDERR);
  }

  my $agent = PleskX->new($storage, $status, $agentsShareDir, $settings{'skip-logs'});

  if ($settings{'only-mail'}) {
    $agent->setDumpType($PleskX::ONLY_MAIL);
  }
  if ($settings{'configuration'}) {
    $agent->setDumpType($PleskX::CONFIGURATION);
  }

  if (exists $settings{'clients-all'}) {
    $agent->selectAll();
  }
  if (exists $settings{'domains-all'}) {
    $agent->selectAllDomains();
  }
  if (exists $settings{'clients'}) {
    $agent->selectClients(@{$settings{'clients'}});
  }
  if (exists $settings{'domains'}) {
    $agent->selectDomains(@{$settings{'domains'}});
  }

  if (exists $settings{'server'}) {
    $agent->selectServerSettings();
  }

  if (exists $settings{'exclude'}) {
    if (exists $settings{'clients-all'} || exists $settings{'clients'}) {
      $agent->excludeClients(@{$settings{'exclude'}});
    } else {
      $agent->excludeDomains(@{$settings{'exclude'}});
    }
  }

  my $res = $agent->dump();

  # upload to ftp
  if ($res == 0 && defined $settings{'ftp'}) {
	print "\nUploading backup to ftp\n" if ($settings{'verbose'});

	use Net::FTP;

	my %ftp = %{$settings{'ftp'}};
	my $FTP = Net::FTP->new($ftp{'server'}, Debug => $settings{'verbose'} > 1 ? 1 : 0);

	if (!defined $FTP) {
		print "ERROR: $@\n";
	}
	elsif (!$FTP->login($ftp{'login'}, $ftp{'password'})) {
		print "ERROR: Can't login to the ftp server\n";
	}
	elsif ($ftp{'path'} ne "" && !$FTP->cwd($ftp{'path'})) {
		print "ERROR: Can't change dir to $ftp{path}\n";
	}
	else {
		my $n = 0;
		my $part = '';
		my $file;
		my $err = 0;

		$FTP->binary();

		while (-f "/tmp/".($file = $ftp{'file'}.$part)) {
			print "Uploading file $file\n" if ($settings{'verbose'} && !$err);

			if (!$err && !$FTP->put("/tmp/$file", $file)) {
				print "ERROR: Can't upload file $file to ftp\n";
				$err = 1;
			}

			unlink("/tmp/$file");
			$part = sprintf('.%03d', ++$n);
		}

		$FTP->quit;
	}
  }

  $status->finish();
  return $res;
}

sub main {
  my %settings;
  try {
    %settings = parseOptions();
  } catch Error with {
    my $error = shift;
    print STDERR "Unable to parse options: $error\n";
    exit(2);
  };

  try {
    return perform(%settings);
  } catch Error with {
    my $error = shift;
    print STDERR "Runtime error: $error\n";
    exit(1);
  }
}

main();

# Local Variables:
# mode: cperl
# cperl-indent-level: 2
# indent-tabs-mode: nil
# tab-width: 4
# End:
