Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 31.05.2014 | нет комментариев
В дальнии детские годы я не понимал значимость резервного копирования данных. Но, как говориться, осознавание приходит с навыком. Нередко навык бывает дюже горький. В моем случае хостинг два раза убивал базу сайта 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. 1-й исполняет cUrl запрос на приобретение токена, а 2-й проверяет не устарел ли токен.

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


Позже приобретения токена, дозволено исполнять операции с Яндекс диском.
Яндекс диск разрешает исполнять много разных запросов. Для моих целей нужны следующие операции:

  • Создание папки
  • Загрузка файла на Яндекс диск
  • Удаление файла с Яндекс диска
  • Скачивание файла с Яндекс диска
  • Приобретение списка объектов содержащихся в папке
  • Определение существования объекта на диска и его тип


Все операции исполняем с применение 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 до файла с классом. Имя самого класса с задачей скрипт определяет по имени файла.

Завершение


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

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

 Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB