Database.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 海豚PHP框架 [ DolphinPHP ]
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2016~2017 河源市卓锐科技有限公司 [ http://www.zrthink.com ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://dolphinphp.com
  8. // +----------------------------------------------------------------------
  9. // | 开源协议 ( http://www.apache.org/licenses/LICENSE-2.0 )
  10. // +----------------------------------------------------------------------
  11. namespace app\admin\controller;
  12. use app\common\builder\ZBuilder;
  13. use think\Db;
  14. use util\Database as DatabaseModel;
  15. /**
  16. * 数据库管理
  17. * @package app\admin\controller
  18. */
  19. class Database extends Admin
  20. {
  21. /**
  22. * 数据库管理
  23. * @param string $group 分组
  24. * @author 蔡伟明 <314013107@qq.com>
  25. * @return mixed
  26. */
  27. public function index($group = 'export')
  28. {
  29. // 配置分组信息
  30. $list_group = ['export' =>'备份数据库', 'import' => '还原数据库'];
  31. $tab_list = [];
  32. foreach ($list_group as $key => $value) {
  33. $tab_list[$key]['title'] = $value;
  34. $tab_list[$key]['url'] = url('index', ['group' => $key]);
  35. }
  36. switch ($group) {
  37. case 'export':
  38. $data_list = Db::query("SHOW TABLE STATUS");
  39. $data_list = array_map('array_change_key_case', $data_list);
  40. // 自定义按钮
  41. $btn_export = [
  42. 'title' => '立即备份',
  43. 'icon' => 'fa fa-fw fa-copy',
  44. 'class' => 'btn btn-primary ajax-post confirm',
  45. 'href' => url('export')
  46. ];
  47. $btn_optimize_all = [
  48. 'title' => '优化表',
  49. 'icon' => 'fa fa-fw fa-cogs',
  50. 'class' => 'btn btn-success ajax-post',
  51. 'href' => url('optimize')
  52. ];
  53. $btn_repair_all = [
  54. 'title' => '修复表',
  55. 'icon' => 'fa fa-fw fa-wrench',
  56. 'class' => 'btn btn-success ajax-post',
  57. 'href' => url('repair')
  58. ];
  59. $btn_optimize = [
  60. 'title' => '优化表',
  61. 'icon' => 'fa fa-fw fa-cogs',
  62. 'class' => 'btn btn-xs btn-default ajax-get',
  63. 'href' => url('optimize', ['ids' => '__id__'])
  64. ];
  65. $btn_repair = [
  66. 'title' => '修复表',
  67. 'icon' => 'fa fa-fw fa-wrench',
  68. 'class' => 'btn btn-xs btn-default ajax-get',
  69. 'href' => url('repair', ['ids' => '__id__'])
  70. ];
  71. // 使用ZBuilder快速创建数据表格
  72. return ZBuilder::make('table')
  73. ->setPageTitle('数据库管理') // 设置页面标题
  74. ->setPrimaryKey('name')
  75. ->setTabNav($tab_list, $group) // 设置tab分页
  76. ->addColumns([ // 批量添加数据列
  77. ['name', '表名'],
  78. ['rows', '行数'],
  79. ['data_length', '大小', 'byte'],
  80. ['data_free', '冗余', 'byte'],
  81. ['comment', '备注'],
  82. ['right_button', '操作', 'btn']
  83. ])
  84. ->addTopButton('custom', $btn_export) // 添加单个顶部按钮
  85. ->addTopButton('custom', $btn_optimize_all) // 添加单个顶部按钮
  86. ->addTopButton('custom', $btn_repair_all) // 添加单个顶部按钮
  87. ->addRightButton('custom', $btn_optimize) // 添加右侧按钮
  88. ->addRightButton('custom', $btn_repair) // 添加右侧按钮
  89. ->setRowList($data_list) // 设置表格数据
  90. ->fetch(); // 渲染模板
  91. break;
  92. case 'import':
  93. // 列出备份文件列表
  94. $path = config('data_backup_path');
  95. if(!is_dir($path)){
  96. mkdir($path, 0755, true);
  97. }
  98. $path = realpath($path);
  99. $flag = \FilesystemIterator::KEY_AS_FILENAME;
  100. $glob = new \FilesystemIterator($path, $flag);
  101. $data_list = [];
  102. foreach ($glob as $name => $file) {
  103. if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){
  104. $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
  105. $date = "{$name[0]}-{$name[1]}-{$name[2]}";
  106. $time = "{$name[3]}:{$name[4]}:{$name[5]}";
  107. $part = $name[6];
  108. if(isset($data_list["{$date} {$time}"])){
  109. $info = $data_list["{$date} {$time}"];
  110. $info['part'] = max($info['part'], $part);
  111. $info['size'] = $info['size'] + $file->getSize();
  112. } else {
  113. $info['part'] = $part;
  114. $info['size'] = $file->getSize();
  115. }
  116. $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
  117. $info['compress'] = ($extension === 'SQL') ? '-' : $extension;
  118. $info['time'] = strtotime("{$date} {$time}");
  119. $info['name'] = $info['time'];
  120. $data_list["{$date} {$time}"] = $info;
  121. }
  122. }
  123. $data_list = !empty($data_list) ? array_values($data_list) : $data_list;
  124. // 自定义按钮
  125. $btn_import = [
  126. 'title' => '还原',
  127. 'icon' => 'fa fa-fw fa-reply',
  128. 'class' => 'btn btn-xs btn-default ajax-get confirm',
  129. 'href' => url('import', ['time' => '__id__'])
  130. ];
  131. // 使用ZBuilder快速创建数据表格
  132. return ZBuilder::make('table')
  133. ->setPageTitle('数据库管理') // 设置页面标题
  134. ->setPrimaryKey('time')
  135. ->hideCheckbox()
  136. ->setTabNav($tab_list, $group) // 设置tab分页
  137. ->addColumns([ // 批量添加数据列
  138. ['name', '备份名称', 'datetime', '', 'Ymd-His'],
  139. ['part', '卷数'],
  140. ['compress', '压缩'],
  141. ['size', '数据大小', 'byte'],
  142. ['time', '备份时间', 'datetime', '', 'Y-m-d H:i:s'],
  143. ['right_button', '操作', 'btn']
  144. ])
  145. ->addRightButton('custom', $btn_import) // 添加右侧按钮
  146. ->addRightButton('delete') // 添加右侧按钮
  147. ->setRowList($data_list) // 设置表格数据
  148. ->fetch(); // 渲染模板
  149. break;
  150. }
  151. }
  152. /**
  153. * 备份数据库(参考onthink 麦当苗儿 <zuojiazi@vip.qq.com>)
  154. * @param null|array $ids 表名
  155. * @param integer $start 起始行数
  156. * @author 蔡伟明 <314013107@qq.com>
  157. */
  158. public function export($ids = null, $start = 0)
  159. {
  160. $tables = $ids;
  161. if ($this->request->isPost() && !empty($tables) && is_array($tables)) {
  162. // 初始化
  163. $path = config('data_backup_path');
  164. if(!is_dir($path)){
  165. mkdir($path, 0755, true);
  166. }
  167. // 读取备份配置
  168. $config = array(
  169. 'path' => realpath($path) . DIRECTORY_SEPARATOR,
  170. 'part' => config('data_backup_part_size'),
  171. 'compress' => config('data_backup_compress'),
  172. 'level' => config('data_backup_compress_level'),
  173. );
  174. // 检查是否有正在执行的任务
  175. $lock = "{$config['path']}backup.lock";
  176. if(is_file($lock)){
  177. $this->error('检测到有一个备份任务正在执行,请稍后再试!');
  178. } else {
  179. // 创建锁文件
  180. file_put_contents($lock, $this->request->time());
  181. }
  182. // 检查备份目录是否可写
  183. is_writeable($config['path']) || $this->error('备份目录不存在或不可写,请检查后重试!');
  184. // 生成备份文件信息
  185. $file = array(
  186. 'name' => date('Ymd-His', $this->request->time()),
  187. 'part' => 1,
  188. );
  189. // 创建备份文件
  190. $Database = new DatabaseModel($file, $config);
  191. if(false !== $Database->create()){
  192. // 备份指定表
  193. foreach ($tables as $table) {
  194. $start = $Database->backup($table, $start);
  195. while (0 !== $start) {
  196. if (false === $start) { // 出错
  197. $this->error('备份出错!');
  198. }
  199. $start = $Database->backup($table, $start[0]);
  200. }
  201. }
  202. // 备份完成,删除锁定文件
  203. unlink($lock);
  204. // 记录行为
  205. action_log('database_export', 'database', 0, UID, implode(',', $tables));
  206. $this->success('备份完成!');
  207. } else {
  208. $this->error('初始化失败,备份文件创建失败!');
  209. }
  210. } else {
  211. $this->error('参数错误!');
  212. }
  213. }
  214. /**
  215. * 还原数据库(参考onthink 麦当苗儿 <zuojiazi@vip.qq.com>)
  216. * @param int $time 文件时间戳
  217. * @author 蔡伟明 <314013107@qq.com>
  218. */
  219. public function import($time = 0)
  220. {
  221. if ($time === 0) $this->error('参数错误!');
  222. // 初始化
  223. $name = date('Ymd-His', $time) . '-*.sql*';
  224. $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
  225. $files = glob($path);
  226. $list = array();
  227. foreach($files as $name){
  228. $basename = basename($name);
  229. $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
  230. $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename);
  231. $list[$match[6]] = array($match[6], $name, $gz);
  232. }
  233. ksort($list);
  234. // 检测文件正确性
  235. $last = end($list);
  236. if(count($list) === $last[0]){
  237. foreach ($list as $item) {
  238. $config = [
  239. 'path' => realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR,
  240. 'compress' => $item[2]
  241. ];
  242. $Database = new DatabaseModel($item, $config);
  243. $start = $Database->import(0);
  244. // 循环导入数据
  245. while (0 !== $start) {
  246. if (false === $start) { // 出错
  247. $this->error('还原数据出错!');
  248. }
  249. $start = $Database->import($start[0]);
  250. }
  251. }
  252. // 记录行为
  253. action_log('database_import', 'database', 0, UID, date('Ymd-His', $time));
  254. $this->success('还原完成!');
  255. } else {
  256. $this->error('备份文件可能已经损坏,请检查!');
  257. }
  258. }
  259. /**
  260. * 优化表
  261. * @param null|string|array $ids 表名
  262. * @author 蔡伟明 <314013107@qq.com>
  263. */
  264. public function optimize($ids = null)
  265. {
  266. $tables = $ids;
  267. if($tables) {
  268. if(is_array($tables)){
  269. $tables = implode('`,`', $tables);
  270. $list = Db::query("OPTIMIZE TABLE `{$tables}`");
  271. if($list){
  272. // 记录行为
  273. action_log('database_optimize', 'database', 0, UID, "`{$tables}`");
  274. $this->success("数据表优化完成!");
  275. } else {
  276. $this->error("数据表优化出错请重试!");
  277. }
  278. } else {
  279. $list = Db::query("OPTIMIZE TABLE `{$tables}`");
  280. if($list){
  281. // 记录行为
  282. action_log('database_optimize', 'database', 0, UID, $tables);
  283. $this->success("数据表'{$tables}'优化完成!");
  284. } else {
  285. $this->error("数据表'{$tables}'优化出错请重试!");
  286. }
  287. }
  288. } else {
  289. $this->error("请选择要优化的表!");
  290. }
  291. }
  292. /**
  293. * 修复表
  294. * @param null|string|array $ids 表名
  295. * @author 蔡伟明 <314013107@qq.com>
  296. */
  297. public function repair($ids = null)
  298. {
  299. $tables = $ids;
  300. if($tables) {
  301. if(is_array($tables)){
  302. $tables = implode('`,`', $tables);
  303. $list = Db::query("REPAIR TABLE `{$tables}`");
  304. if($list){
  305. // 记录行为
  306. action_log('database_repair', 'database', 0, UID, "`{$tables}`");
  307. $this->success("数据表修复完成!");
  308. } else {
  309. $this->error("数据表修复出错请重试!");
  310. }
  311. } else {
  312. $list = Db::query("REPAIR TABLE `{$tables}`");
  313. if($list){
  314. // 记录行为
  315. action_log('database_repair', 'database', 0, UID, $tables);
  316. $this->success("数据表'{$tables}'修复完成!");
  317. } else {
  318. $this->error("数据表'{$tables}'修复出错请重试!");
  319. }
  320. }
  321. } else {
  322. $this->error("请指定要修复的表!");
  323. }
  324. }
  325. /**
  326. * 删除备份文件
  327. * @param int $ids 备份时间
  328. * @author 蔡伟明 <314013107@qq.com>
  329. * @return mixed
  330. */
  331. public function delete($ids = 0)
  332. {
  333. if ($ids == 0) $this->error('参数错误!');
  334. $name = date('Ymd-His', $ids) . '-*.sql*';
  335. $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name;
  336. array_map("unlink", glob($path));
  337. if(count(glob($path))){
  338. $this->error('备份文件删除失败,请检查权限!');
  339. } else {
  340. // 记录行为
  341. action_log('database_backup_delete', 'database', 0, UID, date('Ymd-His', $ids));
  342. $this->success('备份文件删除成功!');
  343. }
  344. }
  345. }