Yandex.DiskへのWebプロジェクトのバックアップ

幼少期には、データをバックアップすることの重要性を理解していませんでした。 しかし、彼らが言うように、理解には経験が伴います。 多くの場合、経験は非常に苦いです。 私の場合、2度ホスティングすると、学生時代に作成されたMathInfinity Webサイトの基盤が破壊されました。



大規模プロジェクトでは、サーバー全体をバックアップ用に確保できます。 しかし、あなたの熱意だけで機能する小さなプロジェクトが膨大にあります。 これらのプロジェクトにもバックアップが必要です。



Dropbox、Ubuntu One、Yandex Disk、Google Driveなどのサービスでアーカイブを作成するというアイデアは、長い間私の注目を集めていました。 理論的にはデータの予約に使用できる数十ギガバイトの空き領域。



今、このアイデアは私の最初の実施形態を受け取りました。 Yandex Diskは、アーカイブを作成するためのサービスとして選択されました。



私はこのアイデアの天才のふりをしません。 そしてもちろん、自転車の発明はインターネット上のターンキーソリューションの検索から始まりました。 見つかったコードはすべて機能しなくなったか、見た目が完全に判読できませんでした。 私のアプリケーションがどのように機能するかを理解することを好みます。



YandexサービスAPIに優れたドキュメントがあるとは言いません。 ただし、特定の標準への例と参照があります。 それで十分でした。



問題を調査した後、データバックアップタスクは次のポイントに分類されました。



  1. アプリケーション登録
  2. OAuthを使用したYandexでの承認
  3. Yandex.Diskの操作
  4. バックアップを作成してYandexディスクに送信する
  5. クラウンコピーを実行する




最後の2つのポイントは技術の問題ですが、それでも説明に含めることにしました。



Limbフレームワークを長い間使用しています。 そして、あなたの自転車に車輪を再発明しないために、クラスコードは以下に与えられます

このフレームワークを使用します。 lmb接頭辞を持つすべてのクラスと関数は、標準のLimbクラスと関数です。



アプリケーション登録



まず、アプリケーションを登録する必要があります。 アプリケーションの登録プロセスは非常に簡単です。 この手順は、 Yandex Documentationで説明されています。

簡単なフォームに記入する必要があります。このフォームでは、特にYandexドライブアプリケーションの使用許可を与える必要があります。 フォームフィールドに入力した結果、アプリケーションIDとアプリケーションパスワードが提供されます。 トークンを取得するために使用する必要があります。 このプロセスには3分かかりました。



OAuthを使用したYandexでの承認



ディスク操作を実行するには、OAuthトークンを指定する必要があります。 OAuth標準には、トークンを取得するためのいくつかのオプションが記載されています。 Tuは最も簡単な方法を採用することにしました。 OAuth標準p。4.3.2に従って、トークンは、Yandexアカウントからのユーザー名とパスワードを使用してサービスに直接要求することで取得できます(どのアカウントでも可能です)。

ドキュメント内の小さな検索により、次のクラスを記述できました。



トークン受信クラスコード
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の2つのメインメソッドがあります。 1つ目はトークンのcUrlリクエストを実行し、2つ目はトークンが古いかどうかを確認します。



Yandex.Diskの操作



トークンを受け取った後、Yandexディスクで操作を実行できます。

Yandexディスクを使用すると、さまざまな要求を実行できます。 私の目的には次の操作が必要です。



すべての操作はcUrlを使用して実行されます。 もちろん、これはすべてソケットを使用して実行できますが、コードのシンプルさは私にとって重要です。 Yandexディスクを使用したすべての操作は、WebDavプロトコルに準拠しています。 Yandex Disk 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を使用してクエリを作成および実行することです。 リクエストごとに、上記で取得したトークンを追加する必要があります。

正直なところ、答えを完全に分析することは怠lazでした。 したがって、応答は単に要求のステータスをチェックし、予想されるものと一致する場合、操作が正常に完了したと見なします。 それ以外の場合は、エラーをログに書き込みます。

メソッドis_dir、is_file、existesの実装はひどいですが、10個以上のファイルがあるフォルダーを扱うつもりはありません。 そのため、lsメソッドを使用して実装されています。

これで、自由にディスク管理ツールを使用できます。 少し欠陥がありますが、それでもツールです。



バックアップを作成してYandexディスクに送信する



次のアルゴリズムを使用してバックアップを作成します。

  1. Yandexドライブから余分なバックアップを削除します。 ディスクにn個を超えるバックアップが蓄積されている場合は、古いものを削除します。構成から番号nを取得します。
  2. 一時フォルダーに、Mysqlデータベースのダンプを作成します。 私のコードでは、これはmysqldumpコマンドを呼び出すことで行われます。
  3. 同じフォルダに、保存するファイルをコピーします。
  4. 作成したファイルを含むフォルダーをアーカイブします。
  5. 結果のアーカイブはYandex Diskにコピーされます
  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("Ymd",$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) { //    Ymd-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メソッドを実行することです。



クラウンコピーを実行する



クラスの相続人として王冠のすべてのタスクを実装することがたまたま起こりました。



クロンジョブ
 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スクリプトを使用して起動されます。 ファイルをタスククラスに接続し、このクラスのオブジェクトを作成し、同時に同じタスクが2つのプロセスによって実行されないようにします(後者はファイルロックに基づいて実装されます)。

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









コマンドはcrontabで作成されます。

  php /path/to/cron_runner.php ya/src/YaBackupJob.class.php
      
      





スクリプトへの引数として、include_pathからの相対パスをクラスを持つファイルに渡します。 スクリプト自体は、ファイル名によってタスクを持つクラスの名前を決定します。



おわりに



このコードが役に立つと嬉しいです。 完全な動作例へのリンクを以下に示します。

建設的な批判は大歓迎です。 コメントとフィードバックを待っています。



参照とソース






All Articles