#!/usr/bin/perl
# Copyright 1999-2012. Parallels IP Holdings GmbH. All Rights Reserved.

use strict;
use warnings;

# Searching for Backup/Migration libs

my $agentsShareDir;
my $productRootD;
my $agentSessionPath;

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

  $productRootD = getProductRootD();

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

use Getopt::Long;
use Carp qw( confess );

use PleskX;
use Logging;
use Error qw|:try|;
use SpecificConfig;
use XmlNode;
use File::Temp;
use File::Copy;
use File::Path;
use File::Basename;
use Encoding;
use POSIX;
use IPC::Run;
use PmmCli;
use DumpStatus;
use AgentConfig;
use HelpFuncs;

$Error::Debug = 1;

sub usage {
  my $exitcode = shift;

  my $usage = <<'EOF';
Usage: plesk_agent_manager <command> [<options>] <arguments>

Commands:

  server         Backs up whole Plesk.

  resellers-name Backs up selected resellers. Reseller's logins are read from command line,
                 space-separated. If no resellers provided, backs up all resellers
                 on the host.

  resellers-id   Backs up selected resellers. Reseller's identificators are read from command line,
                 space-separated. If no resellers provided, backs up all resellers
                 on the host.

  clients-name   Backs up selected clients. Client's logins are read from command line,
                 space-separated. If no clients provided, backs up all clients
                 on the host.

  clients-id     Backs up selected clients. Client's identificators are read from command line,
                 space-separated. If no clients provided, backs up all clients
                 on the host.

  domains-name   Backs up selected domains. Domain's names are read from command line,
                 space-separated. If no domains provided, backs up all domains
                 on the host.

  domains-id     Backs up selected domains. Domain's identificators are read from command line,
                 space-separated. If no domains provided, backs up all domains
                 on the host.

                 Use Exclude options to exclude some resellers/clients/domains.

  export-dump-as-file
                 Export dump from Local repository to single file
                 Option --dump-file-name provide dump file name to export

  help           Shows this help page

General options:

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

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

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

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

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

  -z|--no-gzip   Do not compress content files

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

  --only-mail
                 Backup only mail configuration and content of selected objects.

  --only-hosting
                 Backup only hosting configuration and content of selected objects.

  --suspend
                 Suspend domains during backup operation.

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

  --dump-file-name <dump-file-name>
                 Dump file name to export (with path or relative to dump repository)

  -d|--description=<description>
                 Add description to the dump

FTP options:

 --ftp-login=<ftp_login>
                 Specify the FTP login
 --ftp-password=<ftp_password>
                 Specify the FTP password (used with '--ftp-login')
 --ftp-passive-mode
                 Use FTP passive mode

Internal options:

  --backup-profile-id=<backup-profile-id>
                 Backup profile identificator

  --backup-profile-name=<backup-profile-name>
                 Backup profile name ( default profile name is 'backup' )

  --session-path=<session_path>
                 Session path to store logs and status.

  --migration-mode
                 Run in migration mode.

  --owner-uid=<guid_of_user_who_start_backup>
                 Backup owner unique identificator.

  --owner-type=<type_of_user_who_start_backup>
                 Backup owner type.

  --dump-rotation=<count>
                 Number of backups stored in repository

Exclude options:

  --exclude-reseller=<obj1>,<obj2>,...
                 Exclude resellers from backup list.
                 Reseller's logins are read from command line, comma-separated.
                 If no resellers provided, resellers are not backuped

  --exclude-reseller-file=<file>
                 Exclude resellers listed in file from backup list.
                 File should contain list of reseller's logins one per line.

  --exclude-client=<obj1>,<obj2>,...
                 Exclude clients from backup list.
                 Client's logins are read from command line, comma-separated.
                 If no clients provided, clients are not backuped

  --exclude-client-file=<file>
                 Excludes clients listed in file from backup list.
                 File should contain list of client's logins one per line.

  --exclude-domain=<obj1>,<obj2>,...
                 Exclude domains from backup list.
                 Domain's names are read from command line, comma-separated.
                 If no domains provided, domains are not backuped

  --exclude-domain-file=<file>
                 Exclude domains listed in file from backup list.
                 File should contain list of domain's names one per line.


Output file option:
  --output-file=<output_file>
                 /fullpath/filename - regular file,

  ftp://[<login>[:<password>]@]<server>/<filepath> - storing the backup to ftp server.
  FTP_PASSWORD environment variable can be used for setting password.
  FTP option '--ftp-login' can be used for setting login.
  FTP option '--ftp-password' (with '--ftp-login') can be used for setting password.

                 Used to import dump from repository into the single file.

Backup Node option:
  --backup-node=ftp://[<login>[:<password>]@]<server>/<pathtorepo>

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 parseOutpuFile{
  my( $outputFile, $settings, $ftplogin, $ftppwd, $ftppasv ) = @_;

  if ($outputFile =~ /^ftps?:\/\//) {
    if ($outputFile =~ /^(ftp|ftps):\/\/(?:([^:]*)(?::([^:]*))?@)?([^\/:@]+)(?::([0-9]+))?(?:\/?(.*?)([^\/]*?))$/) {
      my %ftp;

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

      $ftp{'password'} = $ftppwd if defined $ftppwd;
      $ftp{'login'} = $ftplogin if defined $ftplogin;

      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'} = $4;
      $ftp{'port'} = $5;
      $ftp{'path'} = $6;
      $ftp{'file'} = $7;
      $ftp{'passive'} = 1 if $ftppasv;
      $settings->{'ftp'} = \%ftp;
    }
    else {
      die 'Bad FTP file format';
    }
  }
  return;
}


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

  my $command = '';
  my $optParser = Getopt::Long::Parser->new(config => ['bundling']);

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

  my ( $split, $outputfile, $ftplogin, $ftppwd, $ftppasv, $backupnode );
  my ( $objectsFromFileName, $profileId, $profileName, $sessionPath, $owneruid, $ownertype, $dumpfilename, $dumpRotation, $description );
  my ( $excludeResellers, $excludeResellerFile, $excludeClients, $excludeClientFile, $excludeDomains, $excludeDomainFile );
  my ( $noWholeVhost, $noWholeMail );
  my ( $pathToSchema );
  my ( $encryptionKey );

  $command = shift @ARGV;

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

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

  $optParser->getoptions("verbose|v" => sub { $res{'verbose'} += 1 },
                         "configuration|c" => sub { $res{'configuration'} = 1 },
                         "split|s:s" => \$split,
                         "no-gzip|z" => sub { $res{'gzip'} = 0 },
                         "output-file=s" => \$outputfile,

                         "ftp-login:s" => \$ftplogin,
                         "ftp-password:s" => \$ftppwd,
                         "ftp-passive-mode" => sub { $ftppasv = 1 },
                         "from-file|f=s" => \$objectsFromFileName,
                         "only-mail" => sub { $res{'only-mail'} = 1 },
                         "only-hosting" => sub { $res{'only-hosting'} = 1 },
                         #"only-database" => sub { $res{'only-database'} = 1 },
                         "suspend" => sub { $res{'suspend'} = 1 },
                         "dump-rotation=s" => \$dumpRotation,
                         "description|d=s" => \$description,

                         "skip-logs" => sub { $res{'skip-logs'} = 1 },
                         "backup-profile-id=s" => \$profileId,
                         "backup-profile-name=s" => \$profileName,
                         "session-path=s" => \$sessionPath,
                         "owner-uid=s" => \$owneruid,
                         "owner-type=s" => \$ownertype,
                         "dump-file-name:s" => \$dumpfilename,
                         "migration-mode" => sub{ $res{'migration-mode'} = 1 },

                         "exclude-reseller=s" => \$excludeResellers,
                         "exclude-reseller-file=s" => \$excludeResellerFile,
                         "exclude-client=s" => \$excludeClients,
                         "exclude-client-file=s" => \$excludeClientFile,
                         "exclude-domain=s" => \$excludeDomains,
                         "exclude-domain-file=s" => \$excludeDomainFile,

                         "no-whole-vhost" => sub { $noWholeVhost = 1; },
                         "no-whole-mail" =>  sub { $noWholeMail = 1; },
                         "get-size" =>  sub { $res{'get-size'} = 1; },
                         "validate-by-schema=s" => \$pathToSchema,
                         "no-sign" => sub { $res{'no-sign'} = 1; },
                         "backup-node=s" => \$backupnode,
                         );

  $agentSessionPath = $sessionPath;
  my $size;
  if (defined $split) {
    $size = parseSize($split);
    if($size) {
      my $minSplitSize = 1024*1024; # Minimum split size is 1M
      $size = HelpFuncs::Max($size, $minSplitSize);
    }
  }
  my $rlimit = AgentConfig::getRLimitFsize();
  if($rlimit) {
    $size = HelpFuncs::Min($size, 512*$rlimit);
  }
  $res{'split-size'} = $size;

  $res{'dump-rotation'} = $dumpRotation + 0 if $dumpRotation && ($dumpRotation + 0) > 0;

  $res{'description'} = $description if $description;

  $res{'validate-by-schema'} = $pathToSchema if $pathToSchema;

  die " '--ftp-password' option should be used with '--ftp-login' option!" if defined $ftppwd and not $ftplogin;
  die "Use only one of the options: 'only-mail', 'only-hosting'" if exists $res{'only-mail'} && exists $res{'only-hosting'};

  if( defined $outputfile ){
     $res{'output-file'} = $outputfile;
     parseOutpuFile( $outputfile, \%res, $ftplogin, $ftppwd, $ftppasv );
  }

  if( defined $backupnode ) {
     $res{'backup-node'} = $backupnode;
     $res{'passive'} = $ftppasv;
     $res{'ftp-password'} = ($ftppwd eq '' && defined $ENV{'FTP_PASSWORD'}) ? $ENV{'FTP_PASSWORD'} : $ftppwd;
  }

  die "'owner-type' option should be used with '--owner-uid' option!" if (defined $ownertype) && ( !defined $owneruid);

  $res{'profile-id'} = $profileId if defined $profileId;
  $res{'profile-name'} = $profileName if defined $profileName;
  $res{'session-path'} = $sessionPath if defined $sessionPath;
  $res{'owner-uid'} = $owneruid if defined $owneruid;
  $res{'owner-type'} = $ownertype if defined $ownertype;

  $res{'no-whole-vhost'} = 1 if $noWholeVhost;
  $res{'no-whole-mail'} = 1 if $noWholeMail;

  $res{'exclude-reseller'} = [split(/,/, $excludeResellers)]  if defined $excludeResellers;
  if ($excludeResellerFile) {
      my @objects = readObjects($excludeResellerFile);
      push @{$res{'exclude-reseller'}}, @objects if scalar(@objects)>0;
   }

  $res{'exclude-client'} = [split(/,/, $excludeClients)]  if defined $excludeClients;
  if ($excludeClientFile) {
      my @objects = readObjects($excludeClientFile);
      push @{$res{'exclude-client'}}, @objects if scalar(@objects)>0;
   }

  $res{'exclude-domain'} = [split(/,/, $excludeDomains)]  if defined $excludeDomains;
  if ($excludeDomainFile) {
      my @objects = readObjects($excludeDomainFile);
      push @{$res{'exclude-domain'}}, @objects if scalar(@objects)>0;
   }


  if ($command eq "server" ){
    $res{'all'} = $res{'server'} = 1;

    die "'from-file' option should not be specified with 'server' command" if $objectsFromFileName;
  }
  elsif( $command eq "resellers-name" || $command eq "resellers-id" ||
         $command eq "clients-name" || $command eq "clients-id" ||
         $command eq "domains-name" || $command eq "domains-id" )
  {
     if( $objectsFromFileName ){
         my @objects = readObjects($objectsFromFileName);
         $res{$command} = \@objects;
     }
     elsif( scalar(@ARGV)>0 ){ $res{$command} = \@ARGV; }
     else{ $res{ "$command-all" } = 1; }
  }
  elsif( $command eq 'export-dump-as-file' ){
    if( not $dumpfilename ){
      usage(1);
    }
    $res{'export-dump-file'} = $dumpfilename;
    die "option '--output-file' required" if not $outputfile;
  }
  else{
    die "Unknown command '$command'";
  }
  return %res;
}

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)};
  }
  return;
}

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( exists $settings{'output-file'} ) {
    die "Unable to backup directed to stdout" if $settings{'output-file'} eq '-';
  }

  my $status;
  my $dumpLogPath;
  if( defined $settings{'session-path'} ){
    $dumpLogPath = "$settings{'session-path'}/psadump.log";
    $status = DumpStatus::createMigration( "$settings{'session-path'}/dump-status.xml" );
    Logging::setXmlLogging();
  }
  else{
    $status = DumpStatus::createBackup();
  }

  Logging::open($dumpLogPath, $ENV{'CUSTOM_LOG'});

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

  my $psaConf = SpecificConfig->new();
  my $psadumpdir = $psaConf->get( 'DUMP_D' );
  my $migrationMode;
  my $suspend;
  die "Cannnot determine dump directory from psa.conf or directory does not exists [$psadumpdir]!" unless -d $psadumpdir;

  my $dumpdir;

  if ( exists $settings{'backup-node'}) {
    $dumpdir = $settings{'backup-node'};
  } else {
    $dumpdir = $psadumpdir;
  }

  if( exists $settings{'migration-mode'} ){
    $migrationMode = 1;
     die "Migration mode requires session-path is set!" if not defined $settings{'session-path'};
    $dumpdir = $settings{'session-path'};
  }

  if( exists $settings{'suspend'} ){
     $suspend = 1;
     die "Suspend option requires session-path is set!" if not defined $settings{'session-path'};
  }

  if (exists $settings{'export-dump-file'} ) {
    my $files = executeExportDumpFile( $dumpdir, \%settings );
    if( defined $settings{'session-path'} ){
      open DUMP_RES, "> $settings{'session-path'}/dump-name";
      foreach my $fileInfo(@{$files}) { print DUMP_RES "$fileInfo\n"; }
      close DUMP_RES;
    }
    else{
      print STDERR "Output files\n";
      foreach my $fileInfo(@{$files}) { print STDERR "$fileInfo\n"; }
    }
    return 0;
  }

  my $space_reserved = 30*1024*1024;
  my $storage = Storage::Storage::createFileStorage( ($settings{'gzip'} and not exists $settings{'get-size'})
                                                   , $dumpdir
                                                   , $settings{'split-size'}
                                                   , (($migrationMode or exists $settings{'no-sign'}) ? undef : 1)
                                                   , $space_reserved
                                                   , $settings{'passive'} );

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

  if( exists $settings{'profile-name'} ){
    die "Can not use profile name with migration mode" if $migrationMode;
    if( exists $settings{'profile-id'} ) {
      $agent->setBackupProfileFileName( $settings{'profile-name'}, $settings{'profile-id'} );
    }
    else{
      $agent->setBackupProfileFileName( $settings{'profile-name'} );
    }
  }
  if( exists $settings{'owner-uid'} ) {
     $agent->setBackupOwnerGuid( $settings{'owner-uid'}, ( exists $settings{'owner-type'} ? $settings{'owner-type'} : '' ) );
  }
  else{
     $agent->setBackupOwnerGuid();
  }

  $agent->turnOnMigrationMode() if $migrationMode;
  $agent->turnOnListingOnlyMode() if $migrationMode;
  $agent->setDumpWholeVHost() if not exists $settings{'no-whole-vhost'};
  $agent->setDumpWholeMail() if not exists $settings{'no-whole-mail'};

  $agent->setSuspend( $suspend, $settings{'session-path'} ) if ($suspend and not exists $settings{'get-size'} );

  $agent->setDescription( $settings{'description'} ) if exists $settings{'description'};

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

  if (exists $settings{'all'}) {
    $agent->selectAll();
    $agent->selectAdminInfo();
    $agent->selectServerSettings();
  }
  if (exists $settings{'resellers-name-all'} or exists $settings{'resellers-id-all'}) {
    $agent->selectAllResellers();
  }
  if (exists $settings{'clients-name-all'} or exists $settings{'clients-id-all'} ) {
    $agent->selectAllClients();
  }
  if (exists $settings{'domains-name-all'} or exists $settings{'domains-id-all'} ) {
    $agent->selectAllDomains();
  }
  if (exists $settings{'resellers-name'}) {
    $agent->selectResellers(@{$settings{'resellers-name'}});
  }
  if (exists $settings{'clients-name'}) {
    $agent->selectClients(@{$settings{'clients-name'}});
  }
  if (exists $settings{'domains-name'}) {
    $agent->selectDomains(@{$settings{'domains-name'}});
  }
  if (exists $settings{'resellers-id'}) {
    $agent->selectResellersById( @{$settings{'resellers-id'}} );
  }
  if (exists $settings{'clients-id'}) {
    $agent->selectClientsById( @{$settings{'clients-id'}} );
  }
  if (exists $settings{'domains-id'}) {
    $agent->selectDomainsById( @{$settings{'domains-id'}} );
  }
  if( exists $settings{'server'} ){
    $agent->selectServerSettings();
  }

  if( exists $settings{'exclude-reseller'} ) {
      $agent->excludeResellers( @{$settings{'exclude-reseller'}} );
  }
  if( exists $settings{'exclude-client'} ) {
      $agent->excludeClients( @{$settings{'exclude-client'}} );
  }
  if( exists $settings{'exclude-domain'} ) {
      $agent->excludeDomains( @{$settings{'exclude-domain'}} );
  }

  if( exists $settings{'get-size'} ){
    my $returnCode = 0;
    try {
      Logging::setVerbosity(4);
      my $res = $agent->getSize();
      print $res;
    }
    catch Error with {
      my $error = shift;
      $agent->Cleanup();
      $storage->CleanupFiles();
      my $errmsg = "Unable to get backup size";
      print STDERR "$errmsg: $error\n";
      Logging::debug("$errmsg: $error");
      Logging::error($errmsg,'fatal');
      $returnCode = 3;
    };
    if ($returnCode != 0) {
      return $returnCode;
    }

    $status->finish();
    Logging::close();

    return 0;
  }

  my $pid = $$;
  local $SIG{INT} = sub{ $storage->CleanupFiles() if $$==$pid; die "The dump terminated unexpected by signal"; };
  my $returnCode = 0;
  try {
    my $res = $agent->dump();
    if ($res!=0) {
      Logging::error("Dump failed");
      $returnCode = 1;
    }
  } catch Error with {
    my $error = shift;
    $agent->Cleanup();
    $storage->CleanupFiles();
    my $errmsg = "Unable to create dump";
    print STDERR "$errmsg: $error\n";
    Logging::debug("$errmsg: $error");
    Logging::error($errmsg,'fatal');
    $returnCode = 1;
  };
  if ($returnCode != 0) {
    return $returnCode;
  }

  local $SIG{INT} = 'DEFAULT';

  checkDump($storage, $dumpdir, \%settings) if (!$migrationMode && (!exists $settings{'backup-node'} || exists $settings{'validate-by-schema'}));

  my $mainFileName = $storage->getMainDumpXmlFile();
  my $mainFileFolder = $storage->getFilePathFromId( $mainFileName );
  $mainFileFolder = "/$mainFileFolder" if $mainFileFolder;
  $mainFileFolder .= '/' if $mainFileFolder && substr($mainFileFolder, -1, 1 ) ne '/';

  my @printFileInfo;
  # upload to ftp
  if( defined $settings{'ftp'} ) {
      print "\nUploading backup to ftp\n" if $settings{'verbose'};

      my @files = $storage->getDumpFiles( $storage->getFilePathFromId( $mainFileName ) );

      my $prefix = '';
      my $thisProfileName = $mainFileName;
      if( $mainFileName =~ /(.*?)_info_(\d{10}).xml$/ ){
        $thisProfileName = $1;
        $prefix = $2;
      }
      else {
        Logging::error( "Invalid dump file name '$mainFileName'. Cannot determine backup prefix." );
      }

      $dumpdir = prepareDumpToExport("$dumpdir$mainFileFolder", exists($settings{'profile-name'}) ? $settings{'profile-name'} : 'backup', $prefix);

      my (undef, $relativeDumpPath) = File::Basename::fileparse($storage->getMainDumpXmlRelativePath());
      my @dumpfiles = exportFilesTo( "$dumpdir", \@files, $dumpdir, $mainFileName, 0, $settings{'split-size'}, 1, $relativeDumpPath ne './' ? $relativeDumpPath : undef );

      my %ftp = %{$settings{'ftp'}};
      my $ftpOutputFileName;
      $ENV{'DUMP_STORAGE_PASSWD'} = $ftp{'password'};
      my $result = putFilesToFtp( \%ftp, $thisProfileName, $prefix, 1, $settings{'verbose'}, $dumpdir, \@dumpfiles, \$ftpOutputFileName );
      foreach my $dumpfile(@dumpfiles){
         Logging::debug( "Unlink file '$dumpfile'" );
         unlink "$dumpfile";
      }
      if( not $result ){
        Logging::error( "The dump have been made successfully but can not be imported to FTP due to errors. \n"
                       ."The dump '$mainFileName' have been leaved at repository. Please export it manually", 'ExportFailed' );
        die "Export dump to FTP failed";
      }
      else{
        $storage->CleanupFiles();
        Logging::debug( "The dump have been exported to ftp successfully" );
        @printFileInfo = @{$result};
      }
      if( exists $settings{'dump-rotation'}  ){ #rotate dump at FTP
      try {
              Logging::debug( "Rotate dump '$ftpOutputFileName' at FTP" );
              my $ftpUrl = "$ftp{'protocol'}://$ftp{'login'}\@$ftp{'server'}";
              $ftpUrl .= ":$ftp{'port'}" if ($ftp{'port'});
              rotateDump( $ftpUrl, $ftpOutputFileName , $settings{'dump-rotation'}, $agent->getBackupOwnerGuid(), 0,
              		  (exists $settings{'session-path'} ? $settings{'session-path'} : undef ), (exists $ftp{'passive'} ? 1 : undef) );
              Logging::debug( "The dump have been rotated" );
         } catch Error with {
           my $error = shift;
           Logging::error( "Unable to rotate dump: $error", 'UtilityError' );
           print STDERR "Unable to rotate dump: $error\n";
         };
      }
   }
  elsif( defined $settings{'output-file'} ){
       Logging::debug( "Export dump to file '$settings{'output-file'}'" );

       my $relativeDumpPath = '';
       my $dumpPrefix = '';
       my $dumFileName = '';
       if( "$mainFileFolder$mainFileName" =~ /(.*\/)?(.*)_info_(\d{10}).*.xml/ ){
         $relativeDumpPath = $1;
         $dumFileName = $2;
         $relativeDumpPath = '' if not $relativeDumpPath;
         $relativeDumpPath = "/$relativeDumpPath" if $relativeDumpPath;
         $dumpPrefix = $3;
       }
       else {
         die "Invalid dump file name '$mainFileFolder$mainFileName'. Please give path to <info>.xml!";
       }

       my $dumpProfileName = $dumFileName;
       my $idx = index( $dumpProfileName, "_" );
       $dumpProfileName = substr( $dumpProfileName, 0, $idx ) if $idx>0;

       my $newdir;
       $newdir = prepareDumpToExport("$dumpdir$mainFileFolder", $dumpProfileName, $dumpPrefix);

       my @files = getFileList( $newdir, $dumpProfileName, $dumpPrefix, $newdir );

       my $targetDir;
       my $targetFile = $settings{'output-file'};
       if( $targetFile =~ /(.*\/)(.*)/ ){
         $targetDir = $1;
       }
       else{
         if( not $targetFile =~ /\/.*/ ){
           $targetDir = '';
         }
         else{
           $targetDir = '/';
         }
       }
       $targetFile = substr( $targetFile, length($targetDir) );
       my (undef, $relativeDumpPath) = File::Basename::fileparse($storage->getMainDumpXmlRelativePath());
       @printFileInfo = exportFilesTo( $newdir, \@files, $targetDir, $targetFile, 0, $settings{'split-size'}, 0, $relativeDumpPath ne './' ? $relativeDumpPath : undef);
       $storage->CleanupFiles();
   }
  elsif ( defined $settings{'backup-node'}) {
    $storage->CleanupFiles();
    $ENV{'DUMP_STORAGE_PASSWD'} = $settings{'ftp-password'};
    checkDump($storage, $dumpdir, \%settings) if (!$migrationMode && (!exists $settings{'validate-by-schema'}));
  }
  else{
    if( !$migrationMode ){
       push @printFileInfo, "$mainFileFolder$mainFileName";
       if( exists $settings{'dump-rotation'} && !$migrationMode ){
           try {
                Logging::debug( "Rotate dump" );
                rotateDump( "$dumpdir$mainFileFolder", $storage->getMainDumpXmlRelativePath(), $settings{'dump-rotation'}, $agent->getBackupOwnerGuid(), 1,
                            (exists $settings{'session-path'} ? $settings{'session-path'} : undef ), undef );
                Logging::debug( "The dump have been rotated" );
         } catch Error with {
            my $error = shift;
            Logging::error( "Unable to rotate dump: $error" );
            print STDERR "Unable to rotate dump: $error\n";
         };
      }
    }
  }

  $status->finish();
  Logging::close();
  if( defined $settings{'session-path'} ){
    if( !$migrationMode ){
      open DUMP_RES, "> $settings{'session-path'}/dump-name";
      foreach my $fileInfo(@printFileInfo) { print DUMP_RES "$fileInfo\n"; }
      close DUMP_RES;
    }
  }
  else{
    print STDERR "Output files\n";
    foreach my $fileInfo(@printFileInfo) { print STDERR "$fileInfo\n"; }
  }

  return 0;
}

sub putFilesToFtp{
  my ( $ftp, $dumpFileName, $prefix, $useExt, $verbose, $dumpdir, $dumpfiles, $ftpOutputFileName ) = @_;

  print "\nUploading backup to ftp\n" if $verbose;

  my @ftpFiles;

  my $ftpAdr = $ftp->{'protocol'}."://".$ftp->{'login'}."@".$ftp->{'server'};
  $ftpAdr .= ":$ftp->{port}" if $ftp->{port};
  $ftpAdr .= "/$ftp->{path}" if $ftp->{path};
  $ftpAdr .= '/' if substr( $ftpAdr, -1, 1 ) ne '/';

  my $counter = 0;
  my $ext = '';
  if( $useExt && @{$dumpfiles}>0 ){
    my $ridx = rindex $dumpfiles->[0], '.';
    $ext = substr $dumpfiles->[0], $ridx if $ridx>0;
  }

  foreach my $dumpfile(@{$dumpfiles}){
    my $ftpFilename = $ftp->{'file'};
    if( !$ftpFilename ){
       $ftpFilename = $dumpFileName;
       $ftpFilename .= "_".$prefix;
       $ftpFilename .= $ext;
    }
    $ftpFilename .= "$counter" if $counter;
    if( $ftpOutputFileName && ! ${$ftpOutputFileName} ) {
      ${$ftpOutputFileName} = $ftpFilename;
      if( $ftp->{path} ){
        ${$ftpOutputFileName} = "/" . ${$ftpOutputFileName} if substr( $ftp->{path}, -1, 1 ) ne '/';
        ${$ftpOutputFileName} = $ftp->{path} . ${$ftpOutputFileName};
      }
    }
    Logging::debug( "Uploading file '$dumpfile' to ftp file '$ftpFilename'\n" );
    my $cmd = "$productRootD/admin/bin/pmm-ras --upload-file --file-from=$dumpfile --file-to=$ftpAdr$ftpFilename";
    $cmd .= " --use-ftp-passive-mode" if exists $ftp->{'passive'};
    Logging::debug("Execute: $cmd");
    my $cmdResult = `$cmd`;
    my $retCode = $? >> 8;
    if ($retCode != 0) {
      Logging::error("Can't upload file '$dumpfile' to ftp. Error code: $retCode");
      Logging::debug("Uploader output: $cmdResult");
      return;
    }else{
      push @ftpFiles, "$ftpAdr$ftpFilename";
    }
    $counter += 1;
  }

  return \@ftpFiles;
}

sub putFilesToLocal{
  my ( $outputFileName, $dumpFileName, $prefix, $useExt, $verbose, $dumpdir, $dumpfiles ) = @_;

  my  $ftpOutputFileName = '';
  my @ftpFiles;
  my $err;
  my $outputDir = '';
  my $outputFile = '';

  if(substr($outputFileName,-1) eq '/'){
     $outputDir = $outputFileName;
  }
  else {
   $outputFile = $outputFileName;
   if( $outputFile =~ /(.*\/)(.*)/ ){
      $outputDir = $1;
   }
   else{
     if( not $outputFile =~ /\/.*/ ){
       $outputDir = '';
     }
     else{
       $outputDir = '/';
     }
   }
   $outputFile = substr( $outputFile, length($outputDir) );
  }

  my $counter = 0;
  my $ext = '';

  if( $useExt && @{$dumpfiles}>0 ){
    my $ridx = rindex $dumpfiles->[0], '.';
    $ext = substr $dumpfiles->[0], $ridx if $ridx>0;
  }

  foreach my $dumpfile(@{$dumpfiles}){
    if( not $err ) {
      my $ftpFilename = $outputFile;
      if( !$ftpFilename ){
        $ftpFilename = $dumpFileName;
        $ftpFilename .= "_".$prefix;
        $ftpFilename .= $ext;
      }
      else {
        $ftpOutputFileName = $ftpFilename;
      }

      $ftpFilename .= "$counter" if $counter;
      Logging::debug( "Uploading file '$dumpfile' to ftp file '$outputDir$ftpFilename'\n" );
      copy($dumpfile, $outputDir.$ftpFilename) or die "move '$dumpfile' to '$outputDir$ftpFilename' failed: $!";
      push @ftpFiles, "$ftpFilename";
    }
    $counter += 1;
  }

  return \@ftpFiles;
}

sub getDiscoveredFileList{
  my ( $path, $dumpPrefix, $datemask, $base ) = @_;
  my @ret;
  opendir DIR, $path;
  my @dirs = readdir( DIR );
  closedir DIR;
  foreach my $file(@dirs){
    if( $file ne '.' and $file ne '..' ){
      next if index( $file, "$dumpPrefix\_" )!=0;
      my $fullName = "$path/$file";
      if( -d $fullName and $file =~ /.*_(\d{10}).*/s ){
         if( $1 eq $datemask ){
            opendir DIR, $fullName;
            my @discoveredFiles = readdir( DIR );
            closedir DIR;
            foreach my $discoveredFile(@discoveredFiles){
              if( $discoveredFile ne '.' and $discoveredFile ne '..' ){
                push @ret, substr( "$fullName/$discoveredFile", length($base) );
              }
            }
            return @ret;
         }
      }
    }
  }
  return @ret;
}

sub getFileList{
  my ( $path, $dumpPrefix, $datemask, $base ) = @_;
  my @ret;
  $path .= '/' if substr( $path, -1, 1 ) ne '/';
  $base = $path if not $base;
  $base .= '/' if substr( $base, -1, 1 ) ne '/';
  opendir DIR, $path;
  my @dirs = readdir( DIR );
  closedir DIR;
  foreach my $file(@dirs){
    if( $file ne '.' and $file ne '..' ){
        my $fullName = "$path$file";
        if( -d $fullName ){
           if( $file eq '.discovered' ){
             push @ret, getDiscoveredFileList( $fullName, $dumpPrefix, $datemask, $base );
           }
           else{
            push @ret, getFileList( $fullName, $dumpPrefix, $datemask, $base );
          }
        }
        else{
          next if index( $file, "$dumpPrefix\_" ) != 0;
          if( $file =~ /.*_(\d{10}).*/s ){
            push @ret, substr( $fullName, length($base) ) if $1 eq $datemask;
          }
        }
    }
  }
  return @ret;
}


sub executeExportDumpFile{
   my( $dumpdir, $settings ) = @_;

   my $dumpfile = $settings->{'export-dump-file'};

   my $fullPathToDump = $dumpfile;

   if( $dumpfile =~ /\/.*/ ){
        if( index( $dumpfile, "$dumpdir" )==0 ){
           $dumpfile = substr( $dumpfile, length($dumpdir) + 1 )
        }
   }

   my ($targetDir, $targetFile, $useExt);
   $useExt = 0;
   my $fileExport = 0;

   if(defined $settings->{'ftp'}){
      if(not $settings->{'ftp'}{'file'}){
         $useExt = 1;
      }
   }
   else {
      if( substr($settings->{'output-file'},-1) eq '/'){
             $useExt = 1;
      }
   }

   my $fh;
   ($fh, $targetFile ) = File::Temp::tempfile( "$productRootD/PMM/tmp/backupXXXXXX" );
   close( $fh );

   if( $targetFile =~ /(.*\/)(.*)/ ){
      $targetDir = $1;
   }

   else{
     if( not $targetFile =~ /\/.*/ ){
       $targetDir = '';
     }
     else{
       $targetDir = '/';
     }
   }

   $targetFile = substr( $targetFile, length($targetDir) );

   my $relativeDumpPath = '';
   my $dumpPrefix = '';
   my $dumFileName = '';
   if( $dumpfile =~ /(.*\/)?(.*)_info_(\d{10}).*.xml/ ){
       $relativeDumpPath = $1;
       $dumFileName = $2;
       $relativeDumpPath = '' if not $relativeDumpPath;
       $relativeDumpPath = "/$relativeDumpPath" if $relativeDumpPath;
       $dumpPrefix = $3;
   }
   else {
       die "Invalid dump file name '$dumpfile'. Please give path to <info>.xml!";
   }
  my $dumpProfileName = $dumFileName;
  my $idx = index( $dumpProfileName, "_" );
  $dumpProfileName = substr( $dumpProfileName, 0, $idx ) if $idx>0;

  $dumpdir = prepareDumpToExport("$dumpdir/$relativeDumpPath", $dumpProfileName, $dumpPrefix);

  my @files = exportDumpFileTo( "$dumpdir", $dumpProfileName, $dumpPrefix, $targetDir, $targetFile, $settings->{'gzip'}, $settings->{'split-size'}, $useExt );

  my $resultFiles = \@files;
  if( defined $settings->{'ftp'} ) {
       my %ftp = %{$settings->{'ftp'}};
#       $dumpPrefix = "_$dumpPrefix" if $dumpPrefix;
       $resultFiles = putFilesToFtp( \%ftp, $dumFileName, $dumpPrefix, 1, $settings->{'verbose'}, $targetDir, \@files );
       foreach my $file(@files){
          Logging::debug( "Unlink file '$file'" );
          unlink ($file);
       }
       if( not $resultFiles ){
          Logging::error( "The export have been made successfully but can not be loaded to FTP due to errors above." );
          die "Export dump to FTP failed";
       }
  }
  else{
#    $dumpPrefix = "_$dumpPrefix" if $dumpPrefix;
    $resultFiles = putFilesToLocal( $settings->{'output-file'},$dumFileName, $dumpPrefix, 1, $settings->{'verbose'}, $targetDir, \@files );
    foreach my $file(@files){
       Logging::debug( "Unlink file '$file'" );
       unlink ($file);
    }
    if( not $resultFiles ){
       Logging::error( "The export have been made successfully but can not be loaded to FTP due to errors above." );
       die "Export dump to FTP failed";
    }
  }
  return $resultFiles;
}

 sub prepareDumpToExport {
  my ($repoDir, $dumpProfile, $dumpPrefix) = @_;

  my @files = getFileList( $repoDir, $dumpProfile, $dumpPrefix, $repoDir );

  my $tempDir = File::Temp::tempdir( "$productRootD/PMM/tmp/backupXXXXXX", CLEANUP => 1 );

  Logging::debug( "Temporary directory created: " . $tempDir );

   my ($fh, $targetFile);
   ($fh, $targetFile ) = File::Temp::tempfile( "$productRootD/PMM/tmp/xmlFilesListXXXXXX", UNLINK => 1 );
   print $fh "<dump-files source-directory=\"".$repoDir."\">\n";

  foreach my $file (@files) {
    my ($filename, $dirname) = File::Basename::fileparse($file);

    my $ridx = rindex $filename, '.';
    my $ext = substr $filename, $ridx if $ridx>0;
    if ( $ext and $ext eq ".xml" ) {
      print $fh "<file>" . $file . "</file>\n";
    } else {
      File::Path::mkpath($tempDir . "/" . $dirname);
      if (index($dirname, "discovered") > 0) {
        copy($repoDir . "/" . $file, $tempDir . "/" . $dirname);
      } else {
        symlink($repoDir . "/" . $file, $tempDir . "/" . $file);
      }
    }
  }
  print $fh "</dump-files>";
  close $fh;

  my $encryptCmd = AgentConfig::getEncryptUtil();
  $encryptCmd .= " --decrypt-by-plesk ";
  $encryptCmd .= " -backup-files-map " . $targetFile;
  $encryptCmd .= " -output-directory " . $tempDir;

  Logging::debug( "Exec: $encryptCmd" );
  my $ret = `$encryptCmd`;
  my $retCode = $? >> 8;
  if( $retCode!=0 ){
    Logging::error( "Cannot encrypt dump file (ErrorCode: $retCode, STDOUT:$ret) [Error:$!]. This is not fatal error!", 'UtilityError' );
    return $repoDir;
  }

  return $tempDir;
}

sub exportDumpFileTo{
 my ( $repoDir, $dumpPrefix, $dateMask, $targetDir, $targetFile, $gzip, $splitsize, $useExt )= @_;
   my @files = getFileList( $repoDir, $dumpPrefix, $dateMask, $repoDir );
   if( scalar(@files)>0 ){
      return exportFilesTo( "$repoDir", \@files, $targetDir, $targetFile, $gzip, $splitsize, $useExt, undef );
   }
   else{
     die "Could not find files in dump [Path '$repoDir', prefix '$dumpPrefix']!";
   }
}

sub exportFilesTo{
  my ( $startdir, $files, $targetDir, $targetFile, $gzip, $splitsize, $useExt, $relativePath ) = @_;

  if ( defined $relativePath ) {
    $startdir .= "/" . $relativePath;
    foreach my $file ( @{$files} ) {
      $file = substr( $file, length($relativePath) );
    }
  }

  Logging::debug( "Export '" . scalar(@{$files}) . "' files from dir '$startdir' to path'$targetDir/$targetFile'" );
  my $tarStorage = Storage::Storage::createFileStorage( $gzip, $targetDir, $splitsize );
  $tarStorage->setNoDefExtension() if not $useExt;
  if( $tarStorage->addTar( $targetFile, 'directory' => $startdir, 'include' => $files, 'follow_symlinks' => 1 ) ){
    my @files = $tarStorage->getDumpFiles();
    $targetDir = "$targetDir/" if $targetDir and substr( $targetDir, -1, 1 ) ne '/';
    foreach my $file(@files) { 
      $file = "$targetDir$file";
    }
    signDumpFile($files[-1]);
    return @files;
  }
  else{
    die "Cannot export files\n";
  }
}

sub signDumpFile {
  my ($dumpFile) = @_;
  my $signCmd = AgentConfig::backupSignUtil()." --sign-dump $dumpFile";
  my $cmdResult = `$signCmd`;
  my $retCode = $? >> 8;
  if ($retCode != 0) {
    my $err = "Can't export dump, because backup sign action failed with error $retCode";
    $err .= ": $cmdResult" if $cmdResult;
    die $err;
  }
}

sub checkDump {
  my ($storage, $dumpdir, $settings) = @_;
  my $checkDumpRes = 0;

  try {
    Logging::debug( "Check dump" );
    my $mainFileRelativePath = $storage->getMainDumpXmlRelativePath();
    if (exists $settings->{'validate-by-schema'}) {
      my $cmd = AgentConfig::xmllintBin() . " --noout --schema $settings->{'validate-by-schema'} $dumpdir/$mainFileRelativePath";
      Logging::debug( "Execute: $cmd" );
      $checkDumpRes = $? >> 8;
    } else {
      my $dumpDir = $storage->getFullOutputPath();
      my $sessionPath = (exists $settings->{'session-path'} ? $settings->{'session-path'} : undef );
      Logging::debug( "Check dump started ( File: '$mainFileRelativePath' in the repository '$dumpDir' )" );

      my $cmd = "$productRootD/admin/bin/pmm-ras --get-dump-info --dump-file-specification=$mainFileRelativePath --with-feedback";
      $cmd .= " --session-path=$sessionPath --verbose" if $sessionPath;
      $cmd .= " --dump-storage=".$settings->{'backup-node'} if defined $settings->{'backup-node'};
      $cmd .= " --use-ftp-passive-mode" if defined $settings->{'passive'};
      Logging::debug( "Execute: $cmd" );
      my $cmdResult = `$cmd` or die "Cannot execute :$cmd";
      my $retCode = $? >> 8;
      Logging::debug( "The check dump is executed with errorcode '$retCode'" );
      Logging::debug( "The check dump output:$cmdResult" );
      $checkDumpRes = PmmCli::parseCheckDumpResult( $cmdResult );
    }
    Logging::debug( "The check dump return '$checkDumpRes'" );
  }
  catch Error with {
    my $error = shift;
    Logging::error( "Unable to check dump: $error",'CheckDump' );
    print STDERR "Unable to check dump: $error\n";
    $checkDumpRes = 1;
  };

  if( $checkDumpRes!=0 ){
    Logging::error( "The dump have been invalidated by check-dump operation",'CheckDump' );
    die "The check dump failed with code '$checkDumpRes'. The dump can contain invalid data!";
  }
  Logging::debug( "The dump have been validated successfully" );
}

sub rotateDump{
  my( $dumpDir, $dumpFileName, $dumpRotation, $ownerguid, $structured, $sessionPath, $passiveMode ) = @_;
  Logging::debug( "Dump rotation started File: '$dumpFileName' in the repository '$dumpDir'. Set backup's count to '$dumpRotation'" );
  my $cmd = "$productRootD/admin/bin/pmm-ras --rotate-dump --dump-rotation=$dumpRotation --guid=$ownerguid --dump-specification=$dumpFileName";
  $cmd .= " --dump-storage=$dumpDir";
  $cmd .= " --storage-structured" if $structured;
  $cmd .= " --session-path=$sessionPath --verbose" if $sessionPath;
  $cmd .= " --use-ftp-passive-mode" if defined $passiveMode;
  Logging::debug( "Execute: $cmd" );
  my $cmdResult = `$cmd`;
  my $retCode = $? >> 8;
  Logging::debug( "The dump rotation is executed with errorcode '$retCode'" );
  Logging::debug( "The dump rotation output:$cmdResult" ) if $cmdResult;
  die "The dump rotation is failed with code '$retCode'" if $retCode!=0;
  return;
}

sub processError{
  my ( $data, $isError, $prevName, $prevType, $prevObject, $execRes ) = @_;
  my ( $name, $type, $object );
  return  if !$data;
  return  if ref($data) ne 'ARRAY';
  return  if scalar(@{$data})==0;
  $name = "backup";
  $type = "backupowner";
  if( scalar(@{$data})>1 ){
   $name = $data->[1];
   $type = $data->[2] if scalar(@{$data})>2;
  }

  if( ! $$prevObject || !$$prevName || $$prevName ne $name || !$$prevType || $$prevType ne $type ){
    $object = XmlNode->new( 'object' );
    $object->setAttribute( 'name', $name );
    $object->setAttribute( 'type', $type );
    $$prevObject = $object;
    $execRes->addChild( $object );
  }
  else{
    $object = $$prevObject;
  }
  my $msg = XmlNode->new( 'message' );
  $msg->setAttribute( 'code', 'msgtext' );
  $msg->setAttribute( 'severity', ( $isError ? 'error' : 'warning' ) );
  $msg->setText( $data->[0] );
  $object->addChild( $msg );
  return;
}

sub writeMigrationResult{
  my $sessionPath = $agentSessionPath;
  if( $sessionPath && -d $sessionPath ){
    Logging::serializeXmlLog("$sessionPath/migration.result");
  }
  return;
}

sub main {
  $SIG{__DIE__} = \&confess;
  my %settings;
  my $returnCode = 0;
  try {
    %settings = parseOptions();
  } catch Error with {
    my $error = shift;
    my $errmsg = "Unable to parse options";
    print STDERR "$errmsg: $error\n";
    Logging::debug("$errmsg: $error");
    Logging::error($errmsg,'fatal');
    $returnCode = 2;
  };
  if ($returnCode != 0) {
    return $returnCode;
  }

  $returnCode = 0;
  try {
    $returnCode = perform(%settings);
  } catch Error with {
    my $error = shift;
    my $errmsg = "Runtime error";
    print STDERR "$errmsg: $error\n";
    Logging::debug("$errmsg: $error\n" . $error->stacktrace() . "\n");
    Logging::error("$errmsg: $error", 'fatal');
    $returnCode = 1;
  };
  return $returnCode;
}

my $exitcode = main();
writeMigrationResult();
exit($exitcode);

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