1. 程式人生 > 資料庫 >基於 ThinkPHP6.0 的命令列備份恢復資料庫,可用於定時任務等!

基於 ThinkPHP6.0 的命令列備份恢復資料庫,可用於定時任務等!

很久沒有寫部落格了,最近弄個小專案,為了方便在不同電腦上做專案,把資料庫放在專案裡面一起帶走,參考了 海豚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);
    }
}