Резервное копирование сайта с удалённого сервера по FTP, включая автоматическое создание дампа базы данных

Дано: есть у вас сайт на удалённом сервере, доступ к которому предоставлен только по FTP (это к файлам, а к базе данных вам дали логин/пароль и ссылку на phpMyAdmin). Планировщик cron на удалённом сервере вам использовать тоже нельзя (и к нему нет доступа). Задача: организовать регулярное резервное копирования файлов и базы данных на ваш компьютер или сервер.

Проблемы бы не существовало, если б копировать надо было только файлы. Но нам как-то надо регулярно получать и дампы базы. Это притом, что удалённых команд через SSH мы на сервере с сайтом выполнять не можем.

Я сначала опешил, но решение родилось вполне тривиальное — дампить базу PHP-скриптом, который будет запускаться нашим скриптом резервного копирования с нашего сервера перед сливом файлов.

Ещё раз по пунктам:

  1. На удалённом сервере селим PHP-скрипт, умеющий делать дамп базы и записывать его в локальный файл в корне сайта.
  2. На своём сервере пишем скрипт (тут уж можно на bash), который:
    1. Дёргает PHP-скрипт на удалённом сервере.
    2. Дожидается его исполнения.
    3. Идёт по ftp на удалённый сервер и копирует оттуда все файлы сайта, включая актуальный дамп базы.
    4. Всё скаченное пакует в tar.gz.
    5. Удаляет слишком старые tar.gz (ротирует резервные копии, чтобы на нашем сервере не кончилось вдруг дисковое пространство).

Итак, сначала пишем php-скрипт для удалённого сервера (я этот файл назвал dbdump.php, звучит круто):

function backup_tables($user, $pass, $dbname, $host = 'localhost') {
  // соединяемся с базой
  $link = mysql_connect($host, $user, $pass);
  mysql_select_db($dbname, $link);
  // получаем список таблиц
  $tables = array();
  $result = mysql_query('SHOW TABLES');
  while($row = mysql_fetch_row($result)) {
    $tables[] = $row[0];
  }
  // обходим все таблицы
  foreach($tables as $table) {
    // выгребаем из каждой таблицы данные
    $result = mysql_query('SELECT * FROM '.$table);
    $num_fields = mysql_num_fields($result);
    // удаляем таблицу при импорте
    $return.= 'DROP TABLE '.$table.';';
    // создаём при импорте таблицу заново
    $row2 = mysql_fetch_row(mysql_query('SHOW CREATE TABLE '.$table));
    $return.= '\n\n'.$row2[1].';\n\n';
    // вставляем все строки в созданную таблицу
    for ($i = 0; $i < $num_fields; $i++) {
      while($row = mysql_fetch_row($result)) {
        $return.= 'INSERT INTO '.$table.' VALUES(';
        for($j=0; $j<$num_fields; $j++) {
          $row[$j] = addslashes($row[$j]);
          $row[$j] = ereg_replace('\n','\\n',$row[$j]);
          if (isset($row[$j])) {
            $return.= '"'.$row[$j].'"';
          } else {
            $return.= '""';
          }
          if ($j<($num_fields-1)) { $return.= ','; }
        }
        $return.= ");";
      }
    }
    $return.="\n\n\n";
  }
  // генерируем уникальное имя для дампа, добавляя в него случайный элемент  
  $filename = 'db-backup-'.date("Y-m-d-H-i").'-'.$dbname.'-'.( md5( $pass.rand() ) ).'.sql';
  // сохраняем дамп в файл  
  $handle = fopen($filename,'w+');
  fwrite($handle,$return);
  fclose($handle);
} 

// получаем массив свежих дампов, созданных в текущем часу
$freshdumps = glob("db-backup-".date("Y-m-d-H-")."*.sql");
// если свежих дампов нет, то...
if(!$freshdumps) {
  // создаём новый дамп, вызвав заготовленную выше функцию  
  backup_tables('username', 'password', 'database', 'host');
  // получаем массив всех дампов
  $dumps = glob("db-backup-*.sql");
  // удаляем дампы, созданные более 3 суток назад
  foreach ($dumps as $dump) {
    if ( time()-filectime($dump) > 3 * 86400 ) {  
      unlink($dump);
    }
  }   
}

Несколько замечаний:

  1. В имя дампа мы подмешиваем случайные значения для того, чтобы нельзя было догадаться, по какой ссылке дамп можно скачать (не забываем, что все дампы у нас лежат в корне сайта). Можно было с этим и не заморачиваться, а создать в .htaccess правило, не позволяющее веб-серверу отдавать наружу файлы с расширением .htaccess. Но меня увлекло 🙂
  2. Этот PHP-скрипт защищён от DOS`а, т.е. вредный злодей не сможет запускать данный скрипт каждые 5 секунд, создавая нагрузку на сервер и провоцируя хостера отключить к чертям сайт. Скрипт проверяет, есть ли уже дамп за текущий час, если есть, то дамп не создаётся. Созданием дампа раз в час — это не нагрузка даже для виртуального хостинга. Мы ведь делаем одни быстрые SELECT`ы, а значит даже если в вашей базе будет 30-50 МБ данных, то скрипт всё равно отработает за считанные секунды.
  3. Скрипт ротирует прежние дампы, удаляя всё, что старше 3 суток.

Теперь пишем локальный bash-скрипт dobackup.sh для своего сервера:

#!/bin/bash
now=$(date +"%Y-%m-%d")
/usr/bin/wget -O - -q -t 1 http://domain.tld/dbdump.php 2>&1
/usr/bin/wget -rq -P backup/current ftp://login:password@domain.tld $
/usr/bin/tar cfvzp backup/$now.tgz backup/current
/usr/bin/rm -r backup/current
/usr/bin/find backup/ -name *.tgz -type f -mtime +14 -exec rm {} \;

Несколько замечаний:

  1. Скрипт размещаем в ту директорию, где предполагается хранить бэкапы
  2. Юзеру, от имени которого будет выполняться скрипт, через crontab -e в планировщик добавляем задание 30 1 * * * ~/dobackup.sh 2>&1 — чтобы скрипт запускался еженощно в 1:30
  3. Всемогущий wget сначала дёргает удалённый дампер на исполнение, а потом скачивает все файлы доступные по FTP (да, оказывается wget умеет отлично качать по FTP, сохраняя все права на файлы и директории)
  4. Последней строкой скрипта мы убиваем архивы старше 14 дней (ротируем бэкапы, чтобы не переполнить диск — делайте так всегда, иначе брошенный без вашего присмотра автономный сервер сам себя и повесит спустя пару месяцев или лет)
  5. Можно было бы наладить какое-то более плотное взаимодействие между нашим локальным скриптом и удалённым дампером, но я не стал далее углубляться: работает и ладно.

Такое вот получилось решение. Не лишённое быдлокода конечно же, но работающее вполне.

На всякий случай скажу, что если у вас к удалённому серверу есть доступ по SSH, то всё надо делать через консольный mysqldump и идейно верный rsync.

3 комментария

  1. Deep
    Апр 02, 2013 @ 17:47:50

    Спасибо! Пригодилось.

    Reply

  2. foxss
    Апр 18, 2017 @ 23:31:00

    я только непонял, вот это нахрена?
    // удаляем таблицу при импорте
    $return.= ‘DROP TABLE ‘.$table.’;’;
    // создаём при импорте таблицу заново
    $row2 = mysql_fetch_row(mysql_query(‘SHOW CREATE TABLE ‘.$table));
    $return.= ‘\n\n’.$row2[1].’;\n\n’;

    Reply

    • kostin
      Апр 20, 2017 @ 22:29:25

      Это на случай, если у вас структура таблицы в дампе и в текущей базе отличается.

      Reply

Leave a Reply

*