#!/usr/bin/perl

##################################################################################################
##################################################################################################
##################################################################################################
# Created 17.10.2012 Dugin Sergey (c) Ltd Backupland Ver.3.4.0
# Modified 08.06.2018 5:33 add detect charset table Ver.3
# Modified 09.08.2018 17:41 add $dbname Ver.3.1
# Modified 09.08.2018 19:02 add charset=NULL for view Ver.3.2
# Modified 20.09.2018 19:02 Ver.3.3
# Modified 26.09.2018 14:49 add auto path for dumps Ver.3.3.1
# Modified 28.09.2018 17:29 add root ispmanager Ver.3.3.2
# Modified 03.10.2018 20:02 update Ver.3.3.3-4
# Modified 12.11.2018 19:30 update Ver.3.3.4
# Modified 21.12.2018 4:44 update Ver.3.3.5
# Modified 13.03.2019 20:37 update Ver.3.3.6
# Modified 19.12.2019 10:11 update Ver.3.3.7 add empty password for mariadb
# Modified 04.03.2020 19:06 update Ver.3.3.8 add REPAIR TABLE
# Modified 24.07.2020 13:17 update Ver.3.3.9 add bzip on table different operating mode
# Modified 28.04.2021 05:37 update Ver.3.4.0 fix bug my login="LOGIN" password="PASSWOD" and -e "/root/.my.cnf"
# Modified 20.10.2021 18:44 update Ver.3.4.1 change buzip2 to bzcat"
# Modified 24.06.2022 18:50 update Ver.3.4.2 fix bug remote backup line 200 add
# Modified 04.03.2023 14:29 update Ver.3.4.3 fix path for mysqldump
# Modified 04.03.2024 06:50 update Ver.3.4.4 fix error
# For contacts info@backupland.com
#
#Бэкап баз делается по таблично и после дампа каждая таблица жмется bzip2 индивидуально
#такая схема выбрана чтобы было удобнее восстановить 1 конкретную таблицу
#
#Если нужно восстановить сразу все таблицы можно сделать так:
#1) зайти в папку с дампами таблиц
#2) запустите файл !alldump.sh
#
#Если нужно создать базу то, название базы и кодировка находятся в файле !database_name.txt
#для этой операции у вас должно быть административные права
#5) mysql -uЮЗЕР -pПАРОЛЬ < !имя_базы.txt
#
#Так же на всякий случай отдельно сохраняется все структура базы в файле !structure_database.txt
#можно из этого файла восстановить базу и все таблицы без данных
#Будьте осторожны команда ниже затрет все данные в этой базе если они там есть и воссоздаст пустую структуру
#6) mysql -uЮЗЕР -pПАРОЛЬ < !структура_всей_базы.txt
#
########### секция для проверки ошибок
#BEGIN
#{
#  use CGI::Carp qw(carpout);
#  open(LOG, ">>$0.err");
#  carpout(LOG);
#  close(LOG);
#}
#
#use CGI qw(:standard);
#use CGI::Carp qw/fatalsToBrowser/;
########### секция для проверки ошибок
##################################################################################################
##################################################################################################
##################################################################################################

my $login="LOGIN";# Если изменить LOGIN и PASSWOD на что-то другое, то дамп будет делаться из этих данных если оставить как есть и будет файл /root/.my.cnf, то даные из него будут браться
my $password="PASSWOD";
my $hostname="localhost";#Если получаете ошибку в строке которой есть Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock'  переключите localhost на 127.0.0.1 либо исправьте значение переменной $socked
#my $hostnameip="127.0.0.1";#Эта переменная перешибает значение переменной $hostname, нужно если во внешнем конфиге указан localhost, а по факту это нужно изменить.
my $pathbackup="";# если путь не задан создастся папка bkp_mysql рядом с этим скриптом пример: my $pathbackup="/root/backup/bkp_mysql";
my $dayrotate=2;#дней ротации бэкапов, более старые будут удаляться, сработает только если будет запускаться по крон, каждый день в одно и тоже время
#имя базы не нужно, делается дамп всех доступных баз под этим логином и паролем
my $socked="";
my $repair="no"; #Если не нужно попробовать починить таблицы, то тогда no 
#my $repair="yes"; #Если нужно попробовать починить таблицы, то тогда yes Чинится командой REPAIR TABLE
my $bzip=0; #Если ноль то сначала делается дамп, а потом сжатие, так дамп будет более консистентный, так как быстрее сделается и во время дампа в базе будет меньше изменений, а сжатие дампа после будет
#my $bzip=1;#Если один, то делается дамп таблицы потом он жмется, так дольше, но места на диске занимает значительно меньше в процессе создания и архивации, акутально когда на диске крайне места, работает чуть медленне чем $bzip=0, больше грузит процессор
#my $bzip=2;#Если два, то делается дамп таблицы и сразу жмется на диск в потоке, самый медленный способ, но места для хранения нужно меньше всего и бэкап может быть менее консистентный чем первые два способа
my $mysqldump_bin="/usr/bin/mysqldump"; #/mnt/usr/bin/mysqldump или /usr/bin/mysqldump или mysqldump или ваш вариант

##################################################################################################
##################################################################################################
##################################################################################################


use DBI;

if (-e "/var/lib/mysqld/mysqld.sock")   {$socked=":mysql_socket=/var/lib/mysqld/mysqld.sock";}
elsif (-e "/var/run/mysqld/mysqld.sock"){$socked=":mysql_socket=/var/run/mysqld/mysqld.sock";}
elsif (-e "/var/lib/mysql/mysql.sock")  {$socked=":mysql_socket=/var/lib/mysql/mysql.sock";}

my $path="./";
my $cron=0;

if ($ENV{'SSH_CLIENT'} or $ENV{'SSH_CONNECTION'} or $ENV{'SSH_TTY'})#это запуск руками в шеле
{
  $path=$ENV{'PWD'};
}
else #это скорее всего запуск по крону где перменных окружения минимум
{
  my @fullpath=split(/\//,$0);
  pop @fullpath;
  $path=join("/",@fullpath);
  $cron=1;
}

my  $pathb=$path."/bkp_mysql"; #путь для бэкапа баз по отношению к этому скрипту
if ($pathbackup)
{
  $pathb=$pathbackup;
}

my (@date,@time);

for $i(0..$dayrotate)
{
  @time=(localtime(time()-86400*$i))[0,1,2,3,4,5];$time[5]+=1900;$time[4]++;
  if ($cron==1)
  {
    $date[$i]=sprintf("%02d-%02d-%02d-%02d",$time[5],$time[4],$time[3],$time[2],$time[1],$time[0]);
  }
  else
  {
    $date[$i]=sprintf("%02d-%02d-%02d-%02d-%02d",$time[5],$time[4],$time[3],$time[2],$time[1],$time[0]);
  }
}

if ($cron==1 and length("$pathb/$date[$dayrotate]")>4 and -e "$pathb/$date[$dayrotate]") #если запуск по крону и если длина пути больше 4 - проверка чтобы случано не удалить весь сервер если вдруг переменные будут пустые
{
  `/bin/rm -Rf $pathb/$date[$dayrotate]`;
}

my @mysqldump_strc;
my @mysqldump;
my @mysqldumpbzip;
my @bzip;
my @print;
my @printget;
my $global_i=0;
my $global_n=0;

if (-e "$path/uph.php")
{
  my %hash;
  open(FILE,"$path/uph.php");
  while ($str=<FILE>)
  {
    chomp $str;
    if ($str =~ /^(.+): \|(.+): \|(.+): \|.*$/)
    {
      my $login=$1;
      my $password=$2;
      my $hostname=$3;
      if ($hostname eq "localhost" and $hostnameip){$hostname=$hostnameip}
      my $port=3306;
      if ($hostname =~ /(.+):(.+)/)
      {
        $hostname=$1;
        $port=$2;
      }
      my $hash=ebase64("$hostname$port");
      if ($hash =~ /=/){$hash=$`;}

      my $hashl=ebase64("$login$hostname$port");
      if ($hashl =~ /=/){$hashl=$`;}

      if ($cron==1 and length("$pathb/$hash/$date[$dayrotate]")>4 and -e "$pathb/$hash/$date[$dayrotate]") #если запуск по крону и если длина пути больше 4 - проверка чтобы случано не удалить весь сервер если вдруг переменные будут пустые
      {
        `/bin/rm -Rf $pathb/$hash/$date[$dayrotate]`;
      }

      $hash{$hashl}++;
      if ($hash{$hashl}==1)
      {
        `mkdir -p $pathb/$hash`;
        `echo \"This is dump for LOGIN=$login HOSTNAME=$hostname PORT=$port\" > $pathb/$hash/!README.txt`;
        &bkpmysql(1,$login,$password,$hostname,$port,"$pathb/$hash");
      }
    }
  }
  close(FILE);
}
elsif (-e "/root/.my.cnf" and $login eq "LOGIN" and $password eq "PASSWOD")#если стоит панель ispmanager то в этом файле есть рутовый логин пароль
{
  my $rlogin="root";
  my $rpassword="";
  my $rhostname="localhost";
  open(FILE,"/root/.my.cnf");
  while ($str=<FILE>)
  {
    chomp $str;
    if ($str =~ /^password\s*=\s*(.+)$/)
    {
      $rpassword=$1;
      if ($rpassword =~ /^[\'\"](.*)[\'\"]$/){$rpassword=$1}
    }
  }
  close(FILE);
  if (length($rpassword)>=6)#если длина рутового пароля мускля меньше 6 символов тогда облом
  {
    &bkpmysql(2,$rlogin,$rpassword,$rhostname,3306,$pathb);
  }
  else
  {
    &bkpmysql(3,$login,$password,$hostname,3306,$pathb);
  }
}
else
{
  if ($hostname eq "localhost" and $hostnameip){$hostname=$hostnameip}
  &bkpmysql(4,$login,$password,$hostname,3306,$pathb);
}

for $i(0..$#mysqldump_strc)
{
  print "$printget[$i]\n";
  `$mysqldump_strc[$i]`;
}

if ($bzip==1)
{
  for $i(0..$#mysqldump)
  {
    print "$print[$i]\n";
    `$mysqldump[$i]`;
    print "$bzip[$i]\n";
#   if (-e "/usr/bin/screen")
#   {
#    `screen -dmS backup$i $bzip[$i]`;
#   }
#   else
#   {
     system("$bzip[$i] &");
#   }
  }
}
elsif ($bzip==2)
{
  for $i(0..$#mysqldumpbzip)
  {
    print "$print[$i]\n";
    `$mysqldumpbzip[$i]`;
  }
}
else
{
  for $i(0..$#mysqldump)
  {
    print "$print[$i]\n";
    `$mysqldump[$i]`;
  }

  for $i(0..$#bzip)
  {
    print "$bzip[$i]\n";
    `$bzip[$i]`;
  }
}
exit;

##################################################################################################
sub bkpmysql
{
  my ($pos,$login,$password,$hostname,$port,$pathb)=@_;
  my $dbname="information_schema";
  my $sql="DBI:mysql:$dbname:$hostname:$port$socked";
  my $dbh = DBI->connect($sql,$login,$password,{PrintError => 0,RaiseError => 0});

  if (!$dbh)
  {
    &error($pos,$sql,"$DBI::errstr");
    return;
  }

  $password =~ s/([\&\.\\\+\*\?\^\$\[\]\(\)\{\}\<\>\=\!\|\:\,\;\@])/"\\".$1/ge;

  #$sql="SET OPTION wait_timeout=28800";
  #$dbh->do($sql) || &error(2,$sql,"$DBI::errstr");

  my %name;
  my %char;

  my $sql="show databases";
  my $sth=$dbh->prepare("$sql") || &error(3,$sql,"$DBI::errstr");
  $sth->execute() || &error(4,$sql,"$DBI::errstr");
  while (my @data=$sth->fetchrow_array)
  {
    if ($data[0] ne "information_schema" and $data[0] ne "performance_schema")
    {
      my $dirdb="$pathb/$date[0]/$data[0]";
      unless (-e $dirdb)
      {
        `mkdir -p $dirdb`;
        my $pwd=$password;
        my $lgn=$login;
        if ($login eq "root")
        {
          $pwd="ПАРОЛЬ";
          $lgn="ЛОГИН";
        }
        `echo \"#!/bin/sh\" > $dirdb/!alldump.sh`;
        `echo \"\" >> $dirdb/!alldump.sh`;
        `echo \"bzip2 -d *.bz2 -c >> !alldump.txt\" >> $dirdb/!alldump.sh`;
        `echo \"#Если хотие восстановить базу, нужную строчку ниже раскоментируйте\" >> $dirdb/!alldump.sh`;
        `echo \"#mysql -u$lgn -p$pwd $data[0]<!alldump.txt\" >> $dirdb/!alldump.sh`;
        `echo \"#mysql -u$lgn -p$pwd -h$hostname -P$port $data[0]<!alldump.txt\" >> $dirdb/!alldump.sh`;
        `echo \"#Если хотите восстановить базу без распаковки, строчку ниже раскоментируйте\" >> $dirdb/!alldump.sh`;
        `echo \"#bzcat *.bz2 | mysql -u$lgn -p$pwd $data[0]\" >> $dirdb/!alldump.sh`;
        `echo \"#for mariadb\" >> $dirdb/!alldump.sh`;
        `echo \"#bzcat *.bz2 | mysql $data[0]\" >> $dirdb/!alldump.sh`;
        `chmod 700 $dirdb/!alldump.sh`;
      }
      print "Create Path $dirdb\n";
      $printget[$global_n]="Get Info 1 $data[0]";
      my $ppassword=""; if ($password){$ppassword="-p$password"}
      $mysqldump_strc[$global_n]="$mysqldump_bin -u$login $ppassword -h$hostname -P$port --no-data --databases $data[0] | grep \"^CREATE DATABASE \" > $dirdb/!database_name.txt";
      $global_n++;
      $printget[$global_n]="Get Info 2 $data[0]";
      $mysqldump_strc[$global_n]="$mysqldump_bin -u$login $ppassword -h$hostname -P$port --add-drop-table --no-data $data[0] > $dirdb/!structure_database.txt";
      $global_n++;

      my $sql="SHOW TABLE STATUS from `$data[0]`";
      my $sth1=$dbh->prepare($sql) || &error(5,$sql,"$DBI::errstr");
      $sth1->execute() || &error(6,$sql,"$DBI::errstr");
      while(my $hash_ref = $sth1->fetchrow_hashref)
      {
        $name{"$data[0].$hash_ref->{'Name'}"}=$hash_ref->{'Collation'};
        $char{$hash_ref->{'Collation'}}=1;
      }
      foreach my $char (keys(%char))
      {
        if ($char{$char}==1)
        {
          my $sql="SHOW COLLATION WHERE Collation = '$char'";
          my $sth1=$dbh->prepare("$sql") || &error(7,$sql,"$DBI::errstr");
          $sth1->execute() || &error(8,$sql,"$DBI::errstr");
          while(my $hash_ref = $sth1->fetchrow_hashref)
          {
            $char{$char}=$hash_ref->{'Charset'};
          }
        }
      }
      $sql="SHOW TABLES FROM `$data[0]`";
      my $sth1=$dbh->prepare("$sql") || &error(9,$sql,"$DBI::errstr");
      $sth1->execute() || &error(10,$sql,"$DBI::errstr");
      while (my @data1=$sth1->fetchrow_array)
      {
        $data1[0] =~ s/([\.\\\+\*\?\^\$\[\]\(\)\{\}\<\>\=\!\|\:\,\;\@])/"\\".$1/ge; #в имени таблиц особенно VIEW могут быть спецсимволы их нужно экранировать
        my $ppassword=""; if ($password){$ppassword="-p$password"}
        if ($char{$name{"$data[0].$data1[0]"}}==1)#есть таблицы у которых кодировка NULL почему-то!!! VIEW потому-что
        {
          $mysqldump[$global_i]="$mysqldump_bin -Q --add-drop-table --disable-keys --force --extended-insert=TRUE --single-transaction -u$login $ppassword -h$hostname -P$port  $data[0] $data1[0] > $dirdb/$data1[0].sql";
          $mysqldumpbzip[$global_i]="$mysqldump_bin -Q --add-drop-table --disable-keys --force --extended-insert=TRUE --single-transaction -u$login $ppassword -h$hostname -P$port  $data[0] $data1[0] | bzip2 > $dirdb/$data1[0].sql.bz2";
          $print[$global_i]="NULL: $data[0].$data1[0]";
        }
        else
        {
          $mysqldump[$global_i]="$mysqldump_bin -Q --add-drop-table --disable-keys --force --extended-insert=TRUE --single-transaction --default-character-set=$char{$name{\"$data[0].$data1[0]\"}} -u$login $ppassword -h$hostname -P$port $data[0] $data1[0] > $dirdb/$data1[0].sql";
          $mysqldumpbzip[$global_i]="$mysqldump_bin -Q --add-drop-table --disable-keys --force --extended-insert=TRUE --single-transaction --default-character-set=$char{$name{\"$data[0].$data1[0]\"}} -u$login $ppassword -h$hostname -P$port $data[0] $data1[0] | bzip2 > $dirdb/$data1[0].sql.bz2";
          $print[$global_i]="$char{$name{\"$data[0].$data1[0]\"}}: $data[0].$data1[0]";
          if ($repair eq "yes")
          {
            $sql="REPAIR TABLE `$data[0]`.`$data1[0]`";
            my $res=$dbh->do($sql) || &error(6,$sql,"$DBI::errstr");
            print "$sql; - $res\n";
          }
        }
        $bzip[$global_i]="bzip2 $dirdb/$data1[0].sql";
        $global_i++;
      }
    }
  }
  if ($sth){$sth->finish};
  if ($dbh){$dbh->disconnect};
}
##################################################################################################
sub error
{
  my ($pos,$sql,$error)=@_;
  my $time=scalar(localtime(time()));
  open(FILER,">>$0.sql.err");
  print FILER "[$time] $pos $sql $error\n";
  close(FILER);
}
##################################################################################################
sub ebase64 ($;$)
{
  my $res = "";
  my $eol = $_[1];
  $eol = "\n" unless defined $eol;
  pos($_[0]) = 0;
  while ($_[0] =~ /(.{1,45})/gs) {$res .= substr(pack('u', $1),1);chop($res);}
  $res =~ tr|` -_|AA-Za-z0-9+/|;
  my $padding = (3 - length($_[0]) % 3) % 3;
  $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
  if (length $eol) {$res =~ s/(.{1,76})/$1$eol/g;}
  $res;
}
