From 09eddda97f00a1d56767141874777e57339d1877 Mon Sep 17 00:00:00 2001 From: liyukun <1099438829@qq.com> Date: Sun, 31 Oct 2021 01:03:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=BD=91=E7=AB=99=E5=A4=87?= =?UTF-8?q?=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/Databases.php | 330 +++++++++++++++++++++++++++ app/admin/view/admin_role/index.html | 18 -- app/admin/view/databases/export.html | 251 ++++++++++++++++++++ app/admin/view/databases/import.html | 163 +++++++++++++ app/common/service/Database.php | 211 +++++++++++++++++ app/common/tpl/dispatch_jump.tpl | 86 +++++++ config/app.php | 4 +- extend/.gitignore | 2 - public/databack/backup.lock | 1 + 9 files changed, 1044 insertions(+), 22 deletions(-) create mode 100644 app/admin/controller/Databases.php create mode 100644 app/admin/view/databases/export.html create mode 100644 app/admin/view/databases/import.html create mode 100644 app/common/service/Database.php create mode 100644 app/common/tpl/dispatch_jump.tpl delete mode 100644 extend/.gitignore create mode 100644 public/databack/backup.lock diff --git a/app/admin/controller/Databases.php b/app/admin/controller/Databases.php new file mode 100644 index 0000000..7df5608 --- /dev/null +++ b/app/admin/controller/Databases.php @@ -0,0 +1,330 @@ +error('参数错误!'); + } + $this->assign('mate_title',$title); + return $this->fetch($type); + } + + /** + * 获取列表 + * @param Request $request + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 李玉坤 + * @date 2021-10-31 0:12 + */ + public function lst(Request $request) + { + $data = Util::postMore([ + ['type','export'], + ]); + + switch ($data['type']) { + /* 数据还原 */ + case 'import': + //列出备份文件列表 + $path = system_config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + $path = realpath($path); + $flag = \FilesystemIterator::KEY_AS_FILENAME; + $glob = new \FilesystemIterator($path, $flag); + + $list = array(); + 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($list["{$date} {$time}"])){ + $info = $list["{$date} {$time}"]; + $info['part'] = max($info['part'], $part); + $info['size'] = $info['size'] + $file->getSize(); + } else { + $info['part'] = $part; + $info['size'] = $file->getSize(); + } + $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + $info['compress'] = ($extension === 'SQL') ? '-' : $extension; + $info['time'] = strtotime("{$date} {$time}"); + + $list["{$date} {$time}"] = $info; + } + } + $title = '数据还原'; + break; + + /* 数据备份 */ + case 'export': + $list = Db::query('SHOW TABLE STATUS'); + $list = array_map('array_change_key_case', $list); + $title = '数据备份'; + break; + + default: + $this->error('参数错误!'); + } + return app("json")->layui($list); + } + + + /** + * 优化表 + * @author 李玉坤 + * @date 2021-10-30 12:46 + */ + public function optimize() + { + $data = Util::postMore([ + ['ids',''], + ]); + if ($data['ids'] == "") return app("json")->fail("请指定要优化的表"); + $ids = $data['ids']; + if(is_array($ids)){ + $ids = implode('`,`', $ids); + $list = Db::query("OPTIMIZE TABLE `{$ids}`"); + return $list ? app("json")->success("数据表优化完成",'code') : app("json")->fail("数据表优化出错请重试"); + } else { + $list = Db::query("OPTIMIZE TABLE `{$ids}`"); + return $list ? app("json")->success("数据表'{$ids}'优化完成",'code') : app("json")->fail("数据表'{$ids}'优化出错请重试"); + } + } + + /** + * 修复表 + * @author 李玉坤 + * @date 2021-10-31 0:12 + */ + public function repair() + { + $data = Util::postMore([ + ['ids',''], + ]); + if ($data['ids'] == "") return app("json")->fail("请指定要优化的表"); + $ids = $data['ids']; + if(is_array($ids)){ + $ids = implode('`,`', $ids); + $list = Db::query("REPAIR TABLE `{$ids}`"); + return $list ? app("json")->success("数据表修复完成",'code') : app("json")->fail("数据表修复出错请重试"); + } else { + $list = Db::query("REPAIR TABLE `{$ids}`"); + return $list ? app("json")->success("数据表'{$ids}'修复完成",'code') : app("json")->fail("数据表'{$ids}'修复出错请重试"); + } + } + + /** + * 删除备份文件 + * @return mixed|void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 李玉坤 + * @date 2021-10-30 13:31 + */ + public function delOne(){ + $data = Util::postMore([ + ['time',''], + ]); + if ($data['time'] == "") return app("json")->fail("参数错误"); + $name = date('Ymd-His', $data['time']) . '-*.sql*'; + $path = realpath(system_config('data_backup_path')) . DIRECTORY_SEPARATOR . $name; + array_map("unlink", glob($path)); + return !count(glob($path)) ? app("json")->success("备份文件删除成功",'code') : app("json")->fail("备份文件删除失败,请检查权限"); + } + + /** + * 备份数据库 + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 李玉坤 + * @date 2021-10-31 0:35 + */ + public function export(){ + $data = Util::postMore([ + ['ids',''], + ['id',''], + ['start',''], + ]); + if(request()->isPost() && !empty($data['ids'])){ //初始化 + if (!is_array($data['ids'])){ + $data['ids'] = explode(',',$data['ids']); + } + $path = system_config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + //读取备份配置 + $config = array( + 'path' => realpath($path) . DIRECTORY_SEPARATOR, + 'part' => system_config('data_backup_part_size'), + 'compress' => system_config('data_backup_compress'), + 'level' => system_config('data_backup_compress_level'), + ); + //检查备份目录是否可写 + if(!is_writeable($config['path'])) return app("json")->fail('备份目录不存在或不可写,请检查后重试'); + //检查是否有正在执行的任务 + $lock = "{$config['path']}backup.lock"; + if(is_file($lock)){ + return app("json")->fail('检测到有一个备份任务正在执行,请稍后再试'); + } else { + //创建锁文件 + file_put_contents($lock, time()); + } + session('backup_config', $config); + //生成备份文件信息 + $file = array( + 'name' => date('Ymd-His', time()), + 'part' => 1, + ); + session('backup_file', $file); + + //缓存要备份的表 + session('backup_tables', $data['ids']); + + //创建备份文件 + $Database = new Database($file, $config); + $res = $Database->create(); + return $res ? app("json")->success("初始化成功",'code') : app("json")->fail("初始化失败,备份文件创建失败!"); + } elseif (request()->isGet() && is_numeric($data['id']) && is_numeric($data['start'])) { //备份数据 + $data['ids'] = session('backup_tables'); + //备份指定表 + $Database = new Database(session('backup_file'), session('backup_config')); + $data['start'] = $Database->backup($data['ids'][$data['id']], $data['start']); + if(false === $data['start']){ //出错 + $this->error('备份出错!'); + } elseif (0 === $data['start']) { //下一表 + if(isset($data['ids'][++$data['id']])){ + return app("json")->success("备份完成",'code'); + } else { //备份完成,清空缓存 + unlink(session('backup_config.path') . 'backup.lock'); + session('backup_tables', null); + session('backup_file', null); + session('backup_config', null); + return app("json")->success("备份完成",'code'); + } + } else { + $rate = floor(100 * ($data['start'][0] / $data['start'][1])); + return app("json")->success("正在备份...({$rate}%)",'code'); + } + + } else { //出错 + return app("json")->fail("参数错误"); + } + } + + /** + * 还原数据库 + * @author 李玉坤 + * @date 2021-10-30 12:46 + */ + public function import(){ + $data = Util::postMore([ + ['time',''], + ['part',''], + ['start',''], + ]); + if(is_numeric($data['time']) && is_null($data['part']) && is_null($data['start'])){ //初始化 + //获取备份文件信息 + $name = date('Ymd-His', $data['time']) . '-*.sql*'; + $path = realpath(system_config('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]){ + session('backup_list', $list); //缓存备份列表 + return app("json")->success("初始化完成",'code'); + } else { + return app("json")->fail("备份文件可能已经损坏,请检查",'code'); + } + } elseif(is_numeric($data['part']) && is_numeric($data['start'])) { + $list = session('backup_list'); + $db = new Database($list[$data['part']], array( + 'path' => realpath(system_config('data_backup_path')) . DIRECTORY_SEPARATOR, + 'compress' => $list[$data['part']][2])); + + $data['start'] = $db->import($data['start']); + + if(false === $data['start']){ + $this->error('还原数据出错!'); + } elseif(0 === $data['start']) { //下一卷 + if(isset($list[++$data['part']])){ + return app("json")->success("正在还原...#{$data['part']}",'code'); + } else { + session('backup_list', null); + return app("json")->success("还原完成",'code'); + } + } else { + $data = array('part' => $data['part'], 'start' => $data['start'][0]); + if($data['start'][1]){ + $rate = floor(100 * ($data['start'][0] / $data['start'][1])); + return app("json")->success("正在还原...#{$data['part']} ({$rate}%)"); + } else { + $data['gz'] = 1; + return app("json")->success("正在还原...#{$data['part']}"); + } + } + + } else { + return app("json")->fail('参数错误!'); + } + } + +} diff --git a/app/admin/view/admin_role/index.html b/app/admin/view/admin_role/index.html index c4f8af1..c8ecc0c 100644 --- a/app/admin/view/admin_role/index.html +++ b/app/admin/view/admin_role/index.html @@ -202,24 +202,6 @@ } }); } - - function test() { - var selRows = $treeTable.bootstrapTable("getSelections"); - if(selRows.length == 0){ - alert("请至少选择一行"); - return; - } - console.log(selRows); - - var postData = ""; - $.each(selRows,function(i) { - postData += this.id; - if (i < selRows.length - 1) { - postData += ", "; - } - }); - alert("你选中行的 id 为:"+postData); - } \ No newline at end of file diff --git a/app/admin/view/databases/export.html b/app/admin/view/databases/export.html new file mode 100644 index 0000000..816f913 --- /dev/null +++ b/app/admin/view/databases/export.html @@ -0,0 +1,251 @@ + + + + {$mate_title} - {:system_config("title")} + {include file="public/header" /} + + + +
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+ +{include file="public/footer"/} + + + \ No newline at end of file diff --git a/app/admin/view/databases/import.html b/app/admin/view/databases/import.html new file mode 100644 index 0000000..9ee48c3 --- /dev/null +++ b/app/admin/view/databases/import.html @@ -0,0 +1,163 @@ + + + + {$mate_title} - {:system_config("title")} + {include file="public/header" /} + + + +
+
+
+ +
+
+
+
+
+
+
+
+ +{include file="public/footer"/} + + + \ No newline at end of file diff --git a/app/common/service/Database.php b/app/common/service/Database.php new file mode 100644 index 0000000..f8b22be --- /dev/null +++ b/app/common/service/Database.php @@ -0,0 +1,211 @@ +file = $file; + $this->config = $config; + } + + /** + * 打开一个卷,用于写入数据 + * @param integer $size 写入数据的大小 + */ + private function open($size){ + 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 { + $backuppath = $this->config['path']; + $filename = "{$backuppath}{$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 boolean true - 写入成功,false - 写入失败 + */ + public function create(){ + $type = config('database.default'); + $sql = "-- -----------------------------\n"; + $sql .= "-- Think MySQL Data Transfer \n"; + $sql .= "-- \n"; + $sql .= "-- Host : " . config('database.connections.'.$type.'.hostname') . "\n"; + $sql .= "-- Port : " . config('database.connections.'.$type.'.hostport'). "\n"; + $sql .= "-- Database : " . config('database.connections.'.$type.'.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 boolean true - 写入成功,false - 写入失败! + */ + 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 boolean false - 备份失败 + */ + public function backup($table, $start){ + //创建DB对象 + //备份表结构 + if(0 == $start){ + $result = Db::query("SHOW CREATE TABLE `{$table}`"); + $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) { + $sql = "INSERT INTO `{$table}` VALUES ("; + foreach ($row as $item){ + if($item===null){ + $sql=$sql.'null,'; + } + else{ + $item=addslashes($item); + $sql=$sql."'".str_replace(array("\r","\n"),array('\r','\n'),$item)."',"; + } + } + $sql=substr($sql,0,-1); + $sql=$sql. ");\n"; + if(false === $this->write($sql)){ + return false; + } + } + + //还有更多数据 + if($count > $start + 1000){ + return array($start + 1000, $count); + } + } + + //备份下一表 + return 0; + } + + public function import($start){ + //还原数据 + + 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::query($sql)){ + $start += strlen($sql); + } else { + return false; + } + $sql = ''; + } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) { + return 0; + } + } + + return array($start, $size); + } + + /** + * 析构方法,用于关闭文件资源 + */ + public function __destruct(){ + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + } +} \ No newline at end of file diff --git a/app/common/tpl/dispatch_jump.tpl b/app/common/tpl/dispatch_jump.tpl new file mode 100644 index 0000000..482755b --- /dev/null +++ b/app/common/tpl/dispatch_jump.tpl @@ -0,0 +1,86 @@ + + + + + + + 跳转提示 + + + +
+ + +

+

+ + +

+

+ + +

+

+ 页面自动 跳转 等待时间: +

+
+ + + + diff --git a/config/app.php b/config/app.php index 6c0a8c1..f3c26ae 100644 --- a/config/app.php +++ b/config/app.php @@ -34,8 +34,8 @@ return [ // 异常页面的模板文件 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', // 默认跳转页面对应的模板文件 - 'dispatch_success_tmpl' => root_path(). 'extend/tpl/dispatch_jump.tpl', - 'dispatch_error_tmpl' => root_path(). 'extend/tpl/dispatch_jump.tpl', + 'dispatch_success_tmpl' => root_path(). 'app/common/tpl/dispatch_jump.tpl', + 'dispatch_error_tmpl' => root_path(). 'app/common/tpl/dispatch_jump.tpl', // 错误显示信息,非调试模式有效 'error_message' => '页面错误!请稍后再试~', diff --git a/extend/.gitignore b/extend/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/extend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/public/databack/backup.lock b/public/databack/backup.lock new file mode 100644 index 0000000..7433a73 --- /dev/null +++ b/public/databack/backup.lock @@ -0,0 +1 @@ +1635613086 \ No newline at end of file