基於 ThinkPHP6.0 的命令列備份恢復資料庫,可用於定時任務等!
阿新 • • 發佈:2021-01-15
很久沒有寫部落格了,最近弄個小專案,為了方便在不同電腦上做專案,把資料庫放在專案裡面一起帶走,參考了 海豚PHP內的資料庫備份,拿出來改了一下,做成命令列的方式,方便通過命令列或者定時任務來備份資料庫。此命令列基於 ThinkPHP 6.0
新增方式:1、把檔案放在專案的 command內;2、在 console.php 內註冊命令列
使用方式:php think databases ['export', 'import', 'optimize', 'repair', 'delete', 'list]
import 和 delete 需要提供 儲存時間戳 ,時間戳通過 list 裡面的 time 獲取
示例:
下面是完整程式碼:databases.php
<?php declare(strict_types=1); namespace app\command; use Console_Table; use think\console\Command; use think\console\Input; use think\console\input\Argument; use think\console\Output; use think\facade\Db; /** * 資料庫管理 */ class databases extends Command { //輸出物件 protected $output; //資料庫備份根路徑 路徑必須以 / 結尾 protected $data_backup_path = '../database/back/'; //資料庫備份卷大小 該值用於限制壓縮後的分卷最大長度。單位:B;建議設定20M protected $data_backup_part_size = 20971520; //資料庫備份檔案是否啟用壓縮 '0:否1:是', '壓縮備份檔案需要PHP環境支援 <code>gzopen</code>, <code>gzwrite</code>函式' protected $data_backup_compress = 1; //資料庫備份檔案壓縮級別 1:最低4:一般9:最高', '資料庫備份檔案的壓縮級別,該配置在開啟壓縮時生效 protected $data_backup_compress_level = 9; protected function configure() { // 指令配置 $this->setName('databases manager') ->addArgument('options', Argument::OPTIONAL, "options:'export', 'import', 'optimize', 'repair', 'delete', 'list'") ->addArgument('time', Argument::OPTIONAL, "import back_data_file time, run: php think databases list|delete ") ->setDescription("the databases manager command options :'export', 'import', 'optimize', 'repair', 'delete', 'list'"); } protected function execute(Input $input, Output $output) { //檢查是否安裝 Console_Table 元件 ,此元件用於在命令列中輸出表格 $composer = \json_decode(file_get_contents($this->app->getRootPath() . 'composer.json'), true); if (!isset($composer['require']['pear/console_table'])) { fwrite(STDOUT, 'it found that you have not install [pear/console_table]?Y/N' . PHP_EOL); $answer = strtolower(trim(fread(STDIN, 1024), PHP_EOL)); if ($answer == 'y' || $answer == 'yes') { exec('composer require pear/console_table'); } } //設定輸出物件 $this->output = $output; //設定備份目錄 $this->data_backup_path = root_path() . 'database/back/'; //獲取引數陣列 $Argument = $input->getArguments(); //獲取第一個引數 $Argument_options = $Argument['options']; // 指令輸出 switch ($Argument_options) { //備份資料庫 case 'export': $this->export(); break; case 'import': //恢復資料庫 $this->import($Argument); break; case 'optimize': //優化所有表 $this->optimize(); break; case 'repair': //修復所有表 $this->repair(); break; case 'delete': //刪除指定時間備份檔案 $this->delete($Argument); break; case 'list': default: $this->list(); } } /** * 獲取資料庫所有表名稱 */ protected function getTableNames() { $tables = Db::query("SHOW TABLE STATUS"); $tables = array_map('array_change_key_case', $tables); foreach ($tables as $key => &$table) { $table = $table['name']; } unset($table); return $tables; } /** * 輸出備份檔案列表 */ public function list() { // 列出備份檔案列表 $path = $this->data_backup_path; if (!is_dir($path)) { mkdir($path, 0755, true); } $path = realpath($path); $flag = \FilesystemIterator::KEY_AS_FILENAME; $glob = new \FilesystemIterator($path, $flag); $data_list = []; foreach ($glob as $name => $file) { if (preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)) { $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d'); $date = "{$name[0]}-{$name[1]}-{$name[2]}"; $time = "{$name[3]}:{$name[4]}:{$name[5]}"; $part = $name[6]; if (isset($data_list["{$date} {$time}"])) { $info = $data_list["{$date} {$time}"]; $part = max($info['part'], $part); $size = $info['size'] + $file->getSize(); } else { $part = $part; $size = $file->getSize(); } $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); $compress = ($extension === 'SQL') ? '-' : $extension; $info['time'] = strtotime("{$date} {$time}"); $info['part'] = $part; $info['size'] = $size; $info['compress'] = $compress; $info['back_time'] = $date . $time; $data_list[] = $info; } } $table = new Console_Table(); $table->setHeaders(array_keys($data_list[0])); $table->addData($data_list); $this->output->writeln($table->getTable()); } /** * 備份資料庫 */ public function export() { // 初始化 $path = $this->data_backup_path; if (!is_dir($path)) { mkdir($path, 0755, true); } // 讀取備份配置 $config = array( 'path' => realpath($path) . DIRECTORY_SEPARATOR, 'part' => $this->data_backup_part_size, 'compress' => $this->data_backup_compress, 'level' => $this->data_backup_compress_level, ); // 檢查是否有正在執行的任務 $lock = "{$config['path']}backup.lock"; if (is_file($lock)) { $this->output->writeln('檢測到有一個備份任務正在執行,請稍後再試!'); } else { // 建立鎖檔案 file_put_contents($lock, time()); } // 生成備份檔案資訊 $file = array( 'name' => date('Ymd-His', time()), 'part' => 1, ); // 建立備份檔案 $Database = new DatabaseModel($file, $config); if (false !== $Database->create()) { // 備份所有表 $tables = $this->getTableNames(); foreach ($tables as $table) { $start = $Database->backup($table, 0); while (0 !== $start) { if (false === $start) { // 出錯 $this->output->writeln('備份出錯!'); unlink($lock); die; } $start = $Database->backup($table, $start[0]); } } // 備份完成,刪除鎖定檔案 unlink($lock); $this->output->writeln('備份完成!'); } else { $this->output->writeln('初始化失敗,備份檔案建立失敗!'); } } /** * 還原資料庫 * @param array $Argument 檔案時間戳 */ public function import($Argument) { $time = (int)$Argument['time']; if ($time === 0) { $this->output->writeln('引數錯誤!'); die; } // 初始化 $name = date('Ymd-His', $time) . '-*.sql*'; $path = realpath($this->data_backup_path) . DIRECTORY_SEPARATOR . $name; $files = glob($path); $list = array(); foreach ($files as $name) { $basename = basename($name); $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d'); $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename); $list[$match[6]] = array($match[6], $name, $gz); } ksort($list); // 檢測檔案正確性 $last = end($list); if (count($list) === $last[0]) { foreach ($list as $item) { $config = [ 'path' => realpath($this->data_backup_path) . DIRECTORY_SEPARATOR, 'compress' => $item[2] ]; $Database = new DatabaseModel($item, $config); $start = $Database->import(0); // 迴圈匯入資料 while (0 !== $start) { if (false === $start) { // 出錯 $this->output->writeln('還原資料出錯!'); } $start = $Database->import($start[0]); } } $this->output->writeln('還原完成!'); } else { $this->output->writeln('備份檔案可能已經損壞,請檢查!'); } } /** * 優化表 */ public function optimize() { $tables = $this->getTableNames(); foreach ($tables as $table) { $list = Db::query("OPTIMIZE TABLE `{$table}`"); if ($list) { $this->output->writeln("資料表'{$table}'優化完成!"); } else { $this->output->writeln("資料表'{$table}'優化出錯請重試!"); } } } /** * 修復表 */ public function repair() { $tables = $this->getTableNames(); foreach ($tables as $table) { $list = Db::query("REPAIR TABLE `{$table}`"); if ($list) { $this->output->writeln("資料表'{$table}'修復完成!"); } else { $this->output->writeln("資料表'{$table}'修復出錯請重試!"); } } } /** * 刪除備份檔案 * @param array $Argument 備份時間 */ public function delete($Argument) { $time = (int)$Argument['time']; if ($time === 0) { $this->output->writeln('引數錯誤!'); die; } $name = date('Ymd-His', $time) . '-*.sql*'; $path = realpath($this->data_backup_path) . DIRECTORY_SEPARATOR . $name; array_map("unlink", glob($path)); if (count(glob($path))) { $this->output->writeln('備份檔案刪除失敗,請檢查許可權!'); } else { $this->output->writeln('備份檔案刪除成功!'); } } } //資料操作類 class DatabaseModel { /** * 檔案指標 * @var resource */ private $fp; /** * 備份檔案資訊 part - 卷號,name - 檔名 * @var array */ private $file; /** * 當前開啟檔案大小 * @var integer */ private $size = 0; /** * 備份配置 * @var integer */ private $config; /** * 資料庫備份構造方法 * @param array $file 備份或還原的檔案資訊 * @param array $config 備份配置資訊 * @param string $type 執行型別,export - 備份資料, import - 還原資料 */ public function __construct($file, $config, $type = 'export') { $this->file = $file; $this->config = $config; } /** * 開啟一個卷,用於寫入資料 * @param integer $size 寫入資料的大小 */ private function open($size = 0) { if ($this->fp) { $this->size += $size; if ($this->size > $this->config['part']) { $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); $this->fp = null; $this->file['part']++; session('backup_file', $this->file); $this->create(); } } else { $backup_path = $this->config['path']; $filename = "{$backup_path}{$this->file['name']}-{$this->file['part']}.sql"; if ($this->config['compress']) { $filename = "{$filename}.gz"; $this->fp = @gzopen($filename, "a{$this->config['level']}"); } else { $this->fp = @fopen($filename, 'a'); } $this->size = filesize($filename) + $size; } } /** * 寫入初始資料 * @return mixed */ public function create() { $sql = "-- -----------------------------\n"; $sql .= "-- MySQL Data Transfer\n"; $sql .= "--\n"; $sql .= "-- Host : " . config('database.hostname') . "\n"; $sql .= "-- Port : " . config('database.hostport') . "\n"; $sql .= "-- Database : " . config('database.database') . "\n"; $sql .= "--\n"; $sql .= "-- Part : #{$this->file['part']}\n"; $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n"; $sql .= "-- -----------------------------\n\n"; $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n"; return $this->write($sql); } /** * 寫入SQL語句 * @param string $sql 要寫入的SQL語句 * @return int */ private function write($sql = '') { $size = strlen($sql); // 由於壓縮原因,無法計算出壓縮後的長度,這裡假設壓縮率為50%, // 一般情況壓縮率都會高於50%; $size = $this->config['compress'] ? $size / 2 : $size; $this->open($size); return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql); } /** * 備份表結構 * @param string $table 表名 * @param integer $start 起始行數 * @return array|bool|int false - 備份失敗 */ public function backup($table = '', $start = 0) { // 備份表結構 if (0 == $start) { $result = Db::query("SHOW CREATE TABLE `{$table}`"); $result = array_map('array_change_key_case', $result); $sql = "\n"; $sql .= "-- -----------------------------\n"; $sql .= "-- Table structure for `{$table}`\n"; $sql .= "-- -----------------------------\n"; $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; $sql .= trim($result[0]['create table']) . ";\n\n"; if (false === $this->write($sql)) { return false; } } // 資料總數 $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); $count = $result['0']['count']; //備份表資料 if ($count) { // 寫入資料註釋 if (0 == $start) { $sql = "-- -----------------------------\n"; $sql .= "-- Records of `{$table}`\n"; $sql .= "-- -----------------------------\n"; $this->write($sql); } // 備份資料記錄 $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); foreach ($result as $row) { $row = array_map('addslashes', $row); $sql = "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r", "\n"), array('\r', '\n'), implode("', '", $row)) . "');\n"; if (false === $this->write($sql)) { return false; } } //還有更多資料 if ($count > $start + 1000) { return array($start + 1000, $count); } } // 備份下一表 return 0; } /** * 匯入資料 * @param integer $start 起始位置 * @return array|bool|int */ public function import($start = 0) { if ($this->config['compress']) { $gz = gzopen($this->file[1], 'r'); $size = 0; } else { $size = filesize($this->file[1]); $gz = fopen($this->file[1], 'r'); } $sql = ''; if ($start) { $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start); } for ($i = 0; $i < 1000; $i++) { $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz); if (preg_match('/.*;$/', trim($sql))) { if (false !== Db::execute($sql)) { $start += strlen($sql); } else { return false; } $sql = ''; } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) { return 0; } } return array($start, $size); } /** * 匯出 * @param array $tables 表名 * @param string $path 匯出路徑 * @param string $prefix 表字首 * @param integer $export_data 是否匯出資料 * @author 蔡偉明 <[email protected]> * @return bool */ public static function export($tables = [], $path = '', $prefix = '', $export_data = 1) { $tables = is_array($tables) ? $tables : explode(',', $tables); $datetime = date('Y-m-d H:i:s', time()); $sql = "-- -----------------------------\n"; $sql .= "-- 匯出時間 `{$datetime}`\n"; $sql .= "-- -----------------------------\n"; if (!empty($tables)) { foreach ($tables as $table) { $sql .= self::getSql($prefix . $table, $export_data); } // 寫入檔案 if (file_put_contents($path, $sql)) { return true; }; } return false; } /** * 匯出解除安裝檔案 * @param array $tables 表名 * @param string $path 匯出路徑 * @param string $prefix 表字首 * @author 蔡偉明 <[email protected]> * @return bool */ public static function exportUninstall($tables = [], $path = '', $prefix = '') { $tables = is_array($tables) ? $tables : explode(',', $tables); $datetime = date('Y-m-d H:i:s', time()); $sql = "-- -----------------------------\n"; $sql .= "-- 匯出時間 `{$datetime}`\n"; $sql .= "-- -----------------------------\n"; if (!empty($tables)) { foreach ($tables as $table) { $sql .= "DROP TABLE IF EXISTS `{$prefix}{$table}`;\n"; } // 寫入檔案 if (file_put_contents($path, $sql)) { return true; }; } return false; } /** * 獲取表結構和資料 * @param string $table 表名 * @param integer $export_data 是否匯出資料 * @param integer $start 起始行數 * @author 蔡偉明 <[email protected]> * @return string */ private static function getSql($table = '', $export_data = 0, $start = 0) { $sql = ""; if (Db::query("SHOW TABLES LIKE '%{$table}%'")) { // 表結構 if ($start == 0) { $result = Db::query("SHOW CREATE TABLE `{$table}`"); $sql .= "\n-- -----------------------------\n"; $sql .= "-- 表結構 `{$table}`\n"; $sql .= "-- -----------------------------\n"; $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; $sql .= trim($result[0]['Create Table']) . ";\n\n"; } // 表資料 if ($export_data) { $sql .= "-- -----------------------------\n"; $sql .= "-- 表資料 `{$table}`\n"; $sql .= "-- -----------------------------\n"; // 資料總數 $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); $count = $result['0']['count']; // 備份資料記錄 $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); foreach ($result as $row) { $row = array_map('addslashes', $row); $sql .= "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r", "\n"), array('\r', '\n'), implode("', '", $row)) . "');\n"; } // 還有更多資料 if ($count > $start + 1000) { $sql .= self::getSql($table, $export_data, $start + 1000); } } } return $sql; } /** * 析構方法,用於關閉檔案資源 */ public function __destruct() { $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); } }