Резервное копирование веб-проектов на Яндекс.Диск

В далекие детские годы я не понимал важность резервного копирования данных. Но, как говорится, понимание приходит с опытом. Зачастую опыт бывает очень горький. В моем случае хостинг два раза убивал базу сайта MathInfinity, созданного еще в студенческие годы.

Большие проекты могут позволить себе выделить целые сервера для резервного копирования. Однако, существует огромное количество небольших проектов, работающих лишь на вашем энтузиазме. Эти проекты также нуждаются в резервном копировании.

Идея создания архивов на сервисах вроде Dropbox, Ubuntu One, Яндекс Диск, Диск Google и др. уже давно притягивала мое внимание. Десятки гигабайт бесплатного места, которое теоретически можно использовать для резервирования данных.

Теперь эта идея получила мое первое воплощение. В качестве сервиса для создания архивов был выбран Яндекс Диск.

На гениальность идеи я не претендую. И, конечно, изобретение велосипеда началось с поиска готовых решений в Интернете. Весь найденный код либо уже не работал, либо имел совершенно нечитаемый вид. Я же предпочитаю понимать как работают мои приложения.

Не скажу, что API сервисов Яндекса имеют отличную документацию. Однако там есть примеры и ссылки на конкретные стандарты. Этого вполне хватило.

После изучения проблемы задача резервирования данных распалась на следующие пункты:

  1. Регистрация приложения
  2. Авторизация в Яндексе при помощи OAuth
  3. Операции с Яндекс.Диском
  4. Создание и отправка резервной копии на Яндекс диск
  5. Выполнение копирования по крону


Последние два пункта — дело техники, но все же я решил включить их в описание.

Я давно использую фреймворк Limb. И чтобы не изобретать колес к своему велосипеду ниже будут приводиться коды классов
с использованием данного фреймворка. Все классы и функции с префиксом lmb являются стандартными классами и функциями Limb.

Регистрация приложения


Сначала необходимо зарегистрировать свое приложение. Процесс регистрации приложения очень прост. Данная процедура описана в Документации Яндекса.
От вас требуется заполнить простую форму, в которой среди всего прочего необходимо дать разрешение на использование вашего Яндекс диска приложением. В результате заполнения полей формы вам будут выданы id приложения и пароль приложения. Их необходимо использовать для получения токена. У меня данный процесс занял 3 минуты.

Авторизация в Яндексе при помощи OAuth


Для выполнения операций с диском, необходимо указывать OAuth токен. В стандарте OAuth описано несколько вариантов получения токена. Ту решено идти самым простым путем. В соответствии со стандартом OAuth п.4.3.2 токен можно получить прямым запросом к сервису с использованим логина и пароля от учетной записи Яндекса (учетная запись может быть любой).
Небольшой поиск по документации, позволил написать следующий класс:

Код класса получения токена
class YaAuth
{
  protected $token;
  protected $error;
  protected $create_time;
  protected $ttl;
  protected $app_id;
  protected $conf;
  protected $logger;
  
  function __construct($conf,$logger)
  {
    $this->logger = $logger;
    $this->app_id = $conf->get('oauth_app_id');
    $this->clear();
    $this->conf = $conf;
  }

  function getToken()
  {
    if($this->checkToken())
      return $this->token;

    $url = $this->conf->get('oauth_token_url');
    $curl = lmbToolkit::instance()->getCurlRequest();
    
    $curl->setOpt(CURLOPT_HEADER,0);
    $curl->setOpt(CURLOPT_REFERER,$this->conf->get('oauth_referer_url'));
    $curl->setOpt(CURLOPT_URL,$url);
    
    $curl->setOpt(CURLOPT_CONNECTTIMEOUT,1);
    $curl->setOpt(CURLOPT_FRESH_CONNECT,1);
    $curl->setOpt(CURLOPT_RETURNTRANSFER,1);
    $curl->setOpt(CURLOPT_FORBID_REUSE,1);
    $curl->setOpt(CURLOPT_TIMEOUT,4);

    $curl->setOpt(CURLOPT_SSL_VERIFYPEER,false);
  
    $post = 'grant_type=password&client_id='.$this->conf->get('oauth_app_id').
            '&client_secret='.$this->conf->get('oauth_app_secret').
            '&username='.$this->conf->get('oauth_login').
            '&password='.$this->conf->get('oauth_password');

    $header = array(/*'Host: oauth.yandex.ru',*/
                    'Content-type: application/x-www-form-urlencoded',
                    'Content-Length: '.strlen($post)
                   );
    
    $curl->setOpt(CURLOPT_HTTPHEADER,$header);

    $json = $curl->open($post);

    if(!$json)
    {
      $this->error = $curl->getError();
      $this->logger->log('','ERROR', $this->error);
      return false;
    }

    $http_code = $curl->getRequestStatus();

    if(($http_code!='200') && ($http_code!='400'))
    {
      $this->error = "Request Status is ".$http_code;
      $this->logger->log('','ERROR', $this->error);
      return false;
    }
  
    $result = json_decode($json, true);

    if (isset($result['error']) && ($result['error'] != ''))
    {
      $this->error = $result['error'];
      $this->logger->log('','ERROR', $this->error);
      return false;
    }

    $this->token = $result['access_token'];
    $this->ttl = (int)$result['expires_in']; 
    $this->create_time = (int)time();
    return $this->token;
  }
 
  function clear()
  {
    $this->token = '';
    $this->error = '';
    $this->counter_id = '';
    $this->create_time = 0;
    $this->ttl = -1;
  }

  
  
  function checkToken()
  {
    if ($this->ttl <= 0) return false;
  
    if (time()>($this->ttl+$this->create_time))
    {
      $this->error = 'token_outdated';
      $this->logger->log('','ERROR', $this->error);
      return false;
    }
    return true;
  }
  
  function getError()
  {
    return $this->error;
  }
  
}



Все параметры требуемые для авторизации выносим в конфиг. В качестве конфига может выступать любой объект поддерживающий get и set методы.
Для возможности ведения лога выполняемых действий в конструктор класса передается объект для ведения лога работы. Его код можно найти в архиве с примером.
Собственно у класса два основных метода getToken и checkToken. Первый выполняет cUrl запрос на получение токена, а второй проверяет не устарел ли токен.

Операции с Яндекс.Диском


После получения токена, можно выполнять операции с Яндекс диском.
Яндекс диск позволяет выполнять много различных запросов. Для моих целей необходимы следующие операции:
  • Создание папки
  • Загрузка файла на Яндекс диск
  • Удаление файла с Яндекс диска
  • Скачивание файла с Яндекс диска
  • Получение списка объектов содержащихся в папке
  • Определение существования объекта на диска и его тип

Все операции выполняем с использование cUrl. Конечно, все это можно сделать с использованием сокетов, однако мне важно простота кода. Все операции с Яндекс диском соответствуют протоколу WebDav. В документации API Яндекс диска подробно расписаны примеры выполнения запросов и ответов на эти запросы. Код класса для работы с диском приведен ниже:
Код класса выполнения операций с диском
class YaDisk
{ 
  protected $auth;
  protected $config;
  protected $error;
  protected $token;
  protected $logger;
  protected $url;
  
  function __construct($token,$config,$logger)
  {
    $this->auth = $auth;
    $this->config = $config; 
    $this->token = $token;
    $this->logger = $logger;
  } 

  function getCurl($server_dst)
  {
    $curl = lmbToolkit::instance()->getCurlRequest();
    $curl->setOpt(CURLOPT_SSL_VERIFYPEER,false);
    $curl->setOpt(CURLOPT_PORT,$this->config->get('disk_port'));
    $curl->setOpt(CURLOPT_CONNECTTIMEOUT,2);
    $curl->setOpt(CURLOPT_RETURNTRANSFER,1);
    $curl->setOpt(CURLOPT_HEADER, 0);
    $curl->setOpt(CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_1_1);
    $uri = new lmbUri($this->config->get('disk_server_url'));
    $uri = $uri->setPath($server_dst)->toString();
    $curl->setOpt(CURLOPT_URL,$uri);
    $header = array('Accept: */*',
                    "Authorization: OAuth {$this->token}"
                   );
    $curl->setOpt(CURLOPT_HTTPHEADER,$header);
    return $curl;
  }

  function getResult($curl, $codes = array())
  {
    if($curl->getError())
    {
      $this->error = $curl->getError();
      echo $this->error;
      $this->logger->log('','ERROR', $this->error);
      return false;
    } 
    else
    {
      if (!in_array($curl->getRequestStatus(),$codes))
      {
        $this->error = 'Response http error:'.$curl->getRequestStatus();
        $this->logger->log('','ERROR', $this->error);
        return false;
      }
      else
      {
        return true;
      }
    }
  }

  function mkdir($server_dst)
  {
    $curl = $this->getCurl($server_dst);
    $curl->setOpt(CURLOPT_CUSTOMREQUEST,"MKCOL");
    $response = $curl->open();
    return $this->getResult($curl, array(201,405));//405 код коЕвращается если папка уже есть на сервере
  }

  function upload($local_src,$server_dst)
  {
    $local_file = fopen($local_src,"r");
    $curl = $this->getCurl($server_dst);
    //$curl->setOpt(CURLOPT_CUSTOMREQUEST,"PUT");
    $curl->setOpt(CURLOPT_PUT, 1);
    $curl->setOpt(CURLOPT_INFILE,$local_file);
    $curl->setOpt(CURLOPT_INFILESIZE, filesize($local_src));
    $header = array('Accept: */*',
                    "Authorization: OAuth {$this->token}",
                    'Expect: '
                   );
    $curl->setOpt(CURLOPT_HTTPHEADER,$header);
    $response = $curl->open();
    fclose($local_file);
    return $this->getResult($curl, array(200,201,204));    
  }

  function download($server_src,$local_dst)
  {
    $local_file = fopen($local_dst,"w");
    $curl = $this->getCurl($server_src);
    $curl->setOpt(CURLOPT_HTTPGET, 1);
    $curl->setOpt(CURLOPT_HEADER, 0);
    $curl->setOpt(CURLOPT_FILE,$local_file);
    $response = $curl->open();
    fclose($local_file);
    return $this->getResult($curl, array(200));    
  }

  function rm($server_src)
  {
    $curl = $this->getCurl($server_src);
    $curl->setOpt(CURLOPT_CUSTOMREQUEST,"DELETE");
    $response = $curl->open();
    return $this->getResult($curl, array(200));    
  }  
  
  function ls($server_src)
  {
    $curl = $this->getCurl($server_src);
    $curl->setOpt(CURLOPT_CUSTOMREQUEST,"PROPFIND");
    $header = array('Accept: */*',
                    "Authorization: OAuth {$this->token}",
                    'Depth: 1',
                   );
    $curl->setOpt(CURLOPT_HTTPHEADER,$header);
    $response = $curl->open();
    if($this->getResult($curl, array(207)))
    {
      $xml = simplexml_load_string($response,"SimpleXMLElement" ,0,"d",true);
      $list = array();
      foreach($xml as $item)
      {
        if(isset($item->propstat->prop->resourcetype->collection))
          $type = 'd';
        else
          $type = 'f';
        $list[]=array('href'=>(string)$item->href,'type'=>$type);
      }
      return $list; 
    }
    return false;    
  }

  //Ugly. 
  function exists($server_src)
  { 
    $path = dirname($server_src);
    $list = $this->ls($path);
    if($list === false)
    {
      $this->error = 'Не могу получить список файлов';
      $this->logger->log('','ERROR', $this->error);
      return false;
    }
    foreach($list as $item)
      if(rtrim($item['href'],'/')==rtrim($server_src,'/'))
        return true;
    return false;
  }

  //Ugly.
  function is_file($server_src)
  { 
    $path = dirname($server_src);
    $list = $this->ls($path);
    if($list === false)
    {
      $this->error = 'Не могу получить список файлов';
      $this->logger->log('','ERROR', $this->error);
      return false;
    }
    foreach($list as $item)
      if( (rtrim($item['href'],'/')==rtrim($server_src,'/') ) && ($item['type']=='f') )
        return true;
    return false;
  }

  //Ugly. 
  function is_dir($server_src)
  { 
    $path = dirname($server_src);
    $list = $this->ls($path);
    if($list === false)
    {
      $this->error = 'Не могу получить список файлов';
      $this->logger->log('','ERROR', $this->error);
      return false;
    }
    foreach($list as $item)
      if( (rtrim($item['href'],'/')==rtrim($server_src,'/') ) && ($item['type']=='d') )
        return true;
    return false;
  }
}


Все методы классов имеют говорящие имена mkdir, upload, download, ls, rm, поэтому подробно останавливаться на них не будем. Все сводятся формированию и выполнению запроса с помощью cUrl. К каждому запросу необходимо добавлять токен, полученный выше.
Делать полный разбор ответа, честно говоря делать было лень. Поэтому в ответе просто проверяется статус запроса, если он совпадает с ожидаемым, то считаем операцию выполненной успешно. В противном случае записываем ошибку в лог.
Реализация методов is_dir, is_file, exists ужасна, но я не собираюсь работать с папками в который больше 10 файлов. Именно поэтому они реализованы с использованием метода ls.
Теперь в моем распоряжении есть инструмент для управления диском. Пусть он немного ущербный, но все же — это инструмент.

Создание и отправка резервной копии на Яндекс диск


Резервную копию будем создавать по следующему алгоритму:
  1. Удаляем с Яндекс диска лишние бэкапы. Если на диске скопилось более n бэкапов, то старые удаляем., число n берем из конфига.
  2. В некоторой временной папке создаем дамп базы Mysql. В моем коде это выполняется вызовом команды mysqldump.
  3. В эту же папку копируем файлы которые надо сохранить.
  4. Архивируем папку с созданными файлами.
  5. Полученный архив копируем на Яндекс Диск
  6. Удаляем временные файлы

Возможны вариации последнего набора действий. Тут полет фантазии не ограничен. Мне же достаточно указанного набора.
Указанные действия можно выполнить при помощи следующего класса.

Создание архива и отправка его на диск
class YaBackup
{
  protected $disk;
  protected $db;
  protected $logger;
  protected $backup_number;  

  function __construct($backupconfig)
  {
    $config = lmbToolkit::instance()->getConf('yandex');
    $this->logger = YaLogger::instance();
        
    $auth = new YaAuth($config,$this->logger);
    $token = $auth->getToken();
    if($token == '') throw Exception('Не могу получить токен');
    $this->disk = new YaDisk($token,$config,$this->logger);

    $this->db = $backupconfig->get('db');
    $this->folders = $backupconfig->get('folders');
    $this->tmp_dir = $backupconfig->get('tmp_dir');
    $this->project = $backupconfig->get('project');
    $this->backup_number = $backupconfig->get('stored_backups_number');
    $this->server_dir = $backupconfig->get('dir');
    
    $time = time();
    $this->archive = date("Y-m-d",$time).'-'.$time;
  }

  function execute()
  {
    $this->logger->log("Начат бекап проекта ".$this->project,"START_PROJECT");
    $this->_clean();
    $this->logger->log("Удаление старых копий");
    $this->_deleteOld();
    $this->logger->log("Создание дампа базы");
    $this->_makeDump();
    $this->logger->log("Копирование необходимых файлов"); 
    $this->_copyFolders();
    $this->logger->log("Создание архива"); 
    $this->_createArchive();
    $this->logger->log("Копирование на Яндекс.Диск");
    $this->_upload();
    $this->logger->log("Удаление временных файлов"); 
    $this->_clean();
    $this->logger->log("Бекап проекта ".$this->project." завершен", "END_PROJECT");
  }

  protected function _clean()
  { 
    lmbFs::rm($this->getProjectDir());
  }

  protected function _deleteOld()
  {
    $list = $this->disk->ls($this->server_dir.'/'.$this->project);
    $paths=array();
    $n=0;
    foreach($list as $item)
    {
      //Имена архивов имеют вид Y-m-d-timestamp.tar.gz. В качестве ключа массива используем timestamp.
      $parts = explode('-',basename(rtrim($item['href'],'/')));
      if(isset($parts[3]) && ($item['type']=='f'))
      { 
        $tm = explode('.',$parts[3]);
        $paths[(integer)$tm[0]] = $item['href'];
        $n++;
      }
    }
    ksort($paths);//сортируем массив по ключам от меньшего к большему
    for($i=$n;$i>$this->backup_number-1;$i--)
    {
      $item = array_shift($paths);
      $this->logger->log("Удаление ".$item);
      $this->disk->rm($item); 
    }    
  }

  protected function _upload()
  {
    $archive = $this->archive.'.tar.gz';
    
    //создаем дирректории на яндекс диске 
    $this->logger->log("Создаем папки на Яндекс.Диске"); 
    $this->disk->mkdir($this->server_dir);
    $res = $this->disk->mkdir($this->server_dir.'/'.$this->project);
    //Копируем архив    
    $this->logger->log("Копируем архив на Яндекс.Диск"); 
    $this->disk->upload($this->getProjectDir().'/'.$archive,$this->server_dir.'/'.$this->project.'/'.$archive);
    
    if($res) 
      $this->logger->log("Копирование на Яндекс.Диск завершено успешно"); 
    else
      $this->logger->log("Копирование на Яндекс.Диск завершено завершено с ошибкой"); 
  }

  protected function getProjectDir()
  {
    return $this->tmp_dir.'/'.$this->project;
  }

  protected function _copyFolders()
  {
    lmbFs:: mkdir($this->getProjectDir() . '/folders');

    $folders = $this->folders;

    foreach($folders as $key => $value)
    {
      lmbFs:: mkdir($this->getProjectDir() . '/folders/' . $key);
      lmbFs:: cp($value, $this->getProjectDir() . '/folders/' . $key);
    }
  }

  protected function _createArchive()
  {
    $archive = $this->archive;
    $dir = $this->getProjectDir();
    //переписать через system
    `cd $dir && find . -type f -exec tar rvf "$archive.tar" '{}' \;`;  
    `cd $dir && gzip $archive.tar`;
  }  

  protected function _makeDump()
  {
    $host = $this->db['host'];
    $user = $this->db['user'];
    $password = $this->db['password'];
    $database = $this->db['database'];
    $charset = $this->db['charset'];

    lmbFs:: mkdir($this->getProjectDir() . '/base');
    $sql_schema = $this->getProjectDir() . '/base/schema.mysql';
    $sql_data = $this->getProjectDir() . '/base/data.mysql';
    
    //создаем дамп
    $this->mysql_dump_schema($host, $user, $password, $database, $charset, $sql_schema);
    $this->mysql_dump_data($host, $user, $password, $database, $charset, $sql_data);
  }
  
  //Следующие методы лучше вынести в отдельный файл
  protected function mysql_dump_schema($host, $user, $password, $database, $charset, $file, $tables = array())
  {
    $password = ($password)? '-p' . $password : '';
    $cmd = "mysqldump -u$user $password -h$host " .
           "-d --default-character-set=$charset " .
           "--quote-names --allow-keywords --add-drop-table " .
           "--set-charset --result-file=$file " .
           "$database " . implode('', $tables);

    
    $this->logger->log("Начинаем создавать дамп базы в '$file' file...");

    system($cmd, $ret);

    if(!$ret)
      $this->logger->log("Дамп базы создан (" . filesize($file) . " bytes)");
    else
      $this->logger->log("Ошибка создания дампа базы");;
  }

  protected function mysql_dump_data($host, $user, $password, $database, $charset, $file, $tables = array())
  {
    $password = ($password)? '-p' . $password : '';
    $cmd = "mysqldump -u$user $password -h$host " .
           "-t --default-character-set=$charset " .
           "--add-drop-table --create-options --quick " .
           "--allow-keywords --max_allowed_packet=16M --quote-names " .
           "--complete-insert --set-charset --result-file=$file " .
           "$database " . implode('', $tables);


    $this->logger->log("Начинаем создавать дамп данных в '$file' file...");

    system($cmd, $ret);

    if(!$ret)
      $this->logger->log("Дамп данных создан! (" . filesize($file) . " bytes)");
    else
     $this->logger->log("Ошибка создания дампа базы");;
  }
  
}



Причесывать код последнего класса не стал. Думаю заинтересованный читатель сам сможет добавить, убрать или изменить методы под свои нужды. Работа с сводится к загрузке конфига в класс через конструктор и выполнению метода execute

Выполнение копирования по крону


Так сложилось, что все задачи крона я реализую в виде наследников класса:

CronJob
abstract class CronJob
{
  abstract function run();
}


Комментарии тут излишни.
Для каждого проекта я создаю класс примерно такого содержания:
Класс запуска задачи по расписанию
class YaBackupJob extends CronJob
{
  protected $conf;
  protected $conf_name = 'adevelop';
  
  function __construct()
  {
    $this->conf = lmbToolkit::instance()->getConf($this->conf_name);
  }
  
  function run()
  {
    $backup = new YaBackup($this->conf);
    $backup->execute();
  }
  
}



Здесь как и везде выше используется стандартный механизм файлов конфигурации из Limb. В принципе класс можно сделать абстрактным, но это кому как удобно.
Остался вопрос запуска. Сама задача запускается при помощи скрипта cron_runner.php. Который подключает файл с классом задания, создает объект этого класса и следит, чтобы одновременно одно и то же задание не выполнялось двумя процессами (последнее реализовано на основе файловых локов).
cron_runner.php
set_time_limit(0);
require_once(dirname(__FILE__) . '/../setup.php');
lmb_require('limb/core/src/lmbBacktrace.class.php');
lmb_require('limb/fs/src/lmbFs.class.php');
lmb_require('ya/src/YaLogger.class.php');
new lmbBacktrace;
function write_error_in_log($errno, $errstr, $errfile, $errline)
{
  global $logger;
  $back_trace = new lmbBacktrace(10, 10);
  $error_str = " error: $errstr\nfile: $errfile\nline: $errline\nbacktrace:".$back_trace->toString();
  $logger->log($error_str,"ERROR",$errno);
}

set_error_handler('write_error_in_log');
error_reporting(E_ALL);
ini_set('display_errors', true);

if($argc < 2)
  die('Usage: php cron_runner.php cron_job_file_path(starting from include_file_path)' . PHP_EOL);

$cron_job_file_path = $argv[1];
$logger = YaLogger::instance();

$lock_dir = LIMB_VAR_DIR . '/cron_job_lock/';
if(!file_exists($lock_dir))
  lmbFs :: mkdir($lock_dir, 0777);

$name = array_shift(explode('.', basename($cron_job_file_path)));
$lock_file = $lock_dir . $name;
if(!file_exists($lock_file))
{
  file_put_contents($lock_file, '');
  chmod($lock_file, 0777);
}

$fp = fopen($lock_file, 'w');

if(!flock($fp, LOCK_EX + LOCK_NB))
{
  $logger->logConflict();
  return;
}

flock($fp, LOCK_EX + LOCK_NB);

  try {
    lmb_require($cron_job_file_path);
    $job  = new $name;

    if(!in_array('-ld', $argv))
      $logger->log('',"START");

    ob_start();
      echo $name . ' started' . PHP_EOL;
      $result = $job->run();
      $output = ob_get_contents();
    ob_end_clean();

    if(!in_array('-ld', $argv))
      $logger->log($output,"END",$result);
  }
  catch (lmbException $e)
  {
    $logger->logException($e->getNiceTraceAsString());
    throw $e;
  }

flock($fp, LOCK_UN);
fclose($fp);

if(in_array('-v', $argv))
{
  echo $output;
  var_dump($logger->getRecords());
}




В кронтаб прописывается команда:
  php /path/to/cron_runner.php ya/src/YaBackupJob.class.php

В качестве аргумента скрипту передаем путь относительно include_path до файла с классом. Имя самого класса с задачей скрипт определяет по имени файла.

Заключение


Буду рад, если кому пригодится этот код. Ссылки на полный работающий пример приведены ниже.
Конструктивная критика приветствуется. Жду ваших замечаний и отзывов.

Ссылки и источники


Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 23
  • +20
    И тут оно раз и удаляет все твои веб-проекты заодно с системой.
    • +1
      нет, проекты он оставляет себе… удаляет только систему
      • +2
        Основной целью было реализовать взаимодействие с Яндекс диском. Остальной код родился в творческом порыве.
        Использовать его или нет дело сугубо личное. Я постарался написать как пожно проще. Чтобы кажый смог разобраться в коде.
        Убийственные настройки можно выполнить в любом приложении. Особенно, если работать из под root.
      • +1
        Сделайте github репозиторий и composer пакет, пожалуйста!
        • +1
          Добавил на github. Ссылка в конце статьи. С composer посложнее, ни разу с ним не работал. Надо сначала разобраться.
        • 0
          В свой время, когда только-только появился gmail и получил инвайт, слал бекапы проекта на него, главное архив нормально побить на куски, а потом появились другие задачи.
          • +5
            Почему бы не решить эту задачу:
            обычным bash скриптом
            #!/bin/sh
            # Script name : backup.sh
            # Backup database and files
            DATE=`date +%Y-%m-%d_%s`
            BACKUP_TO_DIR='/mnt/yandex.disk'
            BACKUP_FROM_DIR='/home/www'
            SERVER_NAME='server_name'
            KEEP_DAYS=7
            BACKUP_DIR_NAME=$SERVER_NAME.backup
            SQL_USER=user
            SQL_PASSWORD=password
            
            cd $BACKUP_TO_DIR
            if [ ! -d $BACKUP_DIR_NAME ]; then
            mkdir $BACKUP_DIR_NAME
            fi
            
            cd $BACKUP_FROM_DIR
            /usr/bin/mysqldump -Q -q -p$SQL_PASSWORD -u$SQL_USER -hlocalhost --all-databases > sql_$DATE.sql
            if [ $?=0 ]; then
            tar czf backup_$DATE.tar.gz *
            rm -f sql_$DATE.sql
            mv backup_$DATE.tar.gz $BACKUP_TO_DIR/$BACKUP_DIR_NAME
            fi
            
            for del in $(find $BACKUP_TO_DIR/$BACKUP_DIR_NAME -name '*.tar.gz' -mtime $KEEP_DAYS)
            do
            rm $del
            done
            


            Синхронизировать с Яндекс.диском можно через webdav (к примеру по этой инструкции) или через их утилиту.
            • 0
              Можно и так, если у вас есть права устанавливать приложения и монтировать разделы. Проблема в том что далеко не каждый недорогой хостинг предоставляет такую возможность.
              За ссылку большое спасибо.
              • 0
                Мне вот всё интересно, зачем народ до сих пор использует хостинги? Тем более дешевые и с кучей ограничений, когда по 5$/мес виртуальные серваки раздают.
                • 0
                  Парадокс в том, что хостинг за 3$ зачастую быстрее, чем vps за 5-10$ (особенно когда несколько простых сайтов) и хлопот меньше. Использовал и то, и другое.
                  • 0
                    А где такое раздают?
            • 0
              Буквально недавно недореализовал точно такое же. Использую Sabre Dav и Carbon.
              Пример, как альтернативу решению топикастера: gist.github.com/SerafimArts/ad0b651cc23297db11fe

              Может что поможет, интересно =)
              • 0
                Давно хотел написать по теме, но все времени не было. Вообще куда проще все это делать через backupninja, которым можно бекапить базу и файлы полностью или инкрементально, все это дело еще шифровать в случае необходимости. Поддерживает разные протоколы, в том числе и webdav.
                • 0
                  Хм, а как c Dropbox'ом сделать нельзя?

                  1. Скачиваем клиент и устанавливаем в директорию ~/Dropbox
                  2. Создаем папку:
                  mkdir -p ~/Dropbox/git/project.git

                  3. Создаем локальный репозиторий (если нет):
                  mkdir -p project
                  cd $_
                  git init
                  

                  4. Добавляем адрес «удаленного» репозитория:
                  git remote add dropbox  ~/Dropbox/git/project.git

                  5. Отправляем данные:
                  git add .
                  git commit -m 'init comit'
                  git push dropbox master
                  


                  Иными словами, нужно лишь добавить удаленный репозиторий и делать в него пуш!
                  • 0
                    Так а тут без установки клиента )
                    • 0
                      Ну тут профит еще в том, что можно использовать полноценную коллективную разработку:

                      1. Расшариваем папку
                      2. Клонируем репозиторий:
                      git clone ~/Dropbox/git/project.git

                      3. Добавляем адрес «удаленного» репозитория:
                      git remote add dropbox ~/Dropbox/git/project.git

                      4. Отправляем данные:
                      git push dropbox master

                      5. Получаем данные:
                      git pull dropbox master
                      • +1
                        Это уже выходит за рамки бекапа, хотя безусловно интересно.

                        В чем прелесть решения автора статьи:
                        1. Не нужно тратиться на дополнительное хранилище под бекап, 10 Гб Яндекса хватит для большинства сайтов
                        2. Реализуется на любом хостинге, т.к. не требует
                        3. Позволяет делать копии на локальном компе установив клиент Яндекс диска в два клика

                        Если вы можете себе позволить ставить сторонний софт на сервер и вас есть необходимые время и квалификация, то вам такое решение даже близко не годится, есть варианты и по-лучше. А для владельцев пары сайтов на shared-хостингах вполне интересное решение.
                        • 0
                          Разумеется я бы тоже не стал ставить Dropbox на сервер, но для личных проектов на этапе разработки очень удобно так держать бекапы.
                  • 0
                    А я просто сделал ln -s /var/www ~/Yande.Disk/projects/www
                    Минус только один — клиент для Linux не синхронизирует папки .git и .svn. Так что локальные репозитории всё-таки лучше дополнительно периодически сжимать.
                    • 0
                      Из-за того что имена папок начинаются с точки?
                      • 0
                        Нет, так специально захардкожено в клиенте.
                    • +2
                      А вот есть Яндекс SDK на GitHub — github.com/nixsolutions/yandex-sdk-php — это официальная Я.SDK для PHP, если есть вопросы, пожелания, замечания и дополнения, то милости прошу, принимаем как issue так и pull-request'ы :)

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.