MysqlBackupService.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\services;
  12. use think\facade\Db;
  13. class MysqlBackupService
  14. {
  15. /**
  16. * 文件指针
  17. * @var resource
  18. */
  19. private $fp;
  20. /**
  21. * 备份文件信息 part - 卷号,name - 文件名
  22. * @var array
  23. */
  24. private $file;
  25. /**
  26. * 当前打开文件大小
  27. * @var integer
  28. */
  29. private $size = 0;
  30. /**
  31. * 数据库配置
  32. * @var integer
  33. */
  34. private $dbconfig = array();
  35. /**
  36. * 备份配置
  37. * @var integer
  38. */
  39. private $config = array(
  40. 'path' => './Data/',
  41. //数据库备份路径
  42. 'part' => 20971520,
  43. //数据库备份卷大小
  44. 'compress' => 0,
  45. //数据库备份文件是否启用压缩 0不压缩 1 压缩
  46. 'level' => 9,
  47. );
  48. /**
  49. * 数据库备份构造方法
  50. *
  51. * @param array $file 备份或还原的文件信息
  52. * @param array $config 备份配置信息
  53. */
  54. public function __construct($config = [])
  55. {
  56. $this->config['path'] = app()->getRootPath() . 'backup/';
  57. $this->config = array_merge($this->config, $config);
  58. //初始化文件名
  59. $this->setFile();
  60. //初始化数据库连接参数
  61. $this->setDbConn();
  62. //检查文件是否可写
  63. if (!$this->checkPath($this->config['path'])) {
  64. throw new \Exception("The current directory is not writable");
  65. }
  66. }
  67. /**
  68. * 设置脚本运行超时时间
  69. * 0表示不限制,支持连贯操作
  70. */
  71. public function setTimeout($time = null)
  72. {
  73. if (!is_null($time)) {
  74. set_time_limit($time) || ini_set("max_execution_time", $time);
  75. }
  76. return $this;
  77. }
  78. /**
  79. * 设置数据库连接必备参数
  80. *
  81. * @param array $dbconfig 数据库连接配置信息
  82. * @return $this
  83. */
  84. public function setDbConn($dbconfig = [])
  85. {
  86. if (empty($dbconfig)) {
  87. $this->dbconfig = config('database.connections.' . config('database.default'));
  88. //$this->dbconfig = Config::get('database');
  89. } else {
  90. $this->dbconfig = $dbconfig;
  91. }
  92. return $this;
  93. }
  94. /**
  95. * 设置备份文件名
  96. *
  97. * @param null $file
  98. * @return $this
  99. */
  100. public function setFile($file = null)
  101. {
  102. if (is_null($file)) {
  103. $this->file = ['name' => date('Ymd-His'), 'part' => 1];
  104. } else {
  105. if (!array_key_exists("name", $file) && !array_key_exists("part", $file)) {
  106. $this->file = $file['1'];
  107. } else {
  108. $this->file = $file;
  109. }
  110. }
  111. return $this;
  112. }
  113. //数据类连接
  114. public static function connect()
  115. {
  116. return Db::connect();
  117. }
  118. /**
  119. * 数据库表列表
  120. *
  121. * @param null $table
  122. * @param int $type
  123. * @return array
  124. * @throws \think\db\exception\BindParamException
  125. * @throws \think\exception\PDOException
  126. */
  127. public function dataList(?string $table = null, int $type = 1)
  128. {
  129. $key = 'mysql_backup_' . $table . '_' . $type;
  130. $data = CacheService::get($key);
  131. if (!$data) {
  132. $db = self::connect();
  133. if (is_null($table)) {
  134. $list = $db->query("SHOW TABLE STATUS");
  135. } else {
  136. if ($type) {
  137. $list = $db->query("SHOW FULL COLUMNS FROM {$table}");
  138. } else {
  139. $list = $db->query("show columns from {$table}");
  140. }
  141. }
  142. $data = array_map('array_change_key_case', $list);
  143. CacheService::set($key, $data, 7200);
  144. }
  145. return $data;
  146. }
  147. /**
  148. * 数据库备份文件列表
  149. *
  150. * @return array
  151. */
  152. public function fileList()
  153. {
  154. $list = [];
  155. if (!is_dir($this->config['path'])) {
  156. mkdir($this->config['path'], 0755, true);
  157. return $list;
  158. }
  159. $path = realpath($this->config['path']);
  160. $flag = \FilesystemIterator::KEY_AS_FILENAME;
  161. $glob = new \FilesystemIterator($path, $flag);
  162. foreach ($glob as $name => $file) {
  163. if (preg_match('/^\\d{8,8}-\\d{6,6}-\\d+\\.sql(?:\\.gz)?$/', $name)) {
  164. $info['filename'] = $name;
  165. $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
  166. $date = $name[0] . '-' . $name[1] . '-' . $name[2];
  167. $time = $name[3] . ':' . $name[4] . ':' . $name[5];
  168. $part = $name[6];
  169. if (isset($list[$date . $time])) {
  170. $info = $list[$date . $time];
  171. $info['part'] = max($info['part'], $part);
  172. $info['size'] = $info['size'] + $file->getSize();
  173. } else {
  174. $info['part'] = $part;
  175. $info['size'] = $file->getSize();
  176. }
  177. $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
  178. $info['compress'] = $extension === 'SQL' ? '-' : $extension;
  179. $info['time'] = strtotime($date . $time);
  180. $list[$date . $time] = $info;
  181. }
  182. }
  183. return $list;
  184. }
  185. /**
  186. * @param string $type
  187. * @param int $time
  188. * @return array|false|string
  189. * @throws \Exception
  190. */
  191. public function getFile($type = '', $time = 0)
  192. {
  193. //
  194. if (!is_numeric($time)) {
  195. throw new \Exception("{$time} Illegal data type");
  196. }
  197. switch ($type) {
  198. case 'time':
  199. $name = date('Ymd-His', $time) . '-*.sql*';
  200. $path = realpath($this->config['path']) . DIRECTORY_SEPARATOR . $name;
  201. return glob($path);
  202. break;
  203. case 'timeverif':
  204. $name = date('Ymd-His', $time) . '-*.sql*';
  205. $path = realpath($this->config['path']) . DIRECTORY_SEPARATOR . $name;
  206. $files = glob($path);
  207. $list = array();
  208. foreach ($files as $name) {
  209. $basename = basename($name);
  210. $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
  211. $gz = preg_match('/^\\d{8,8}-\\d{6,6}-\\d+\\.sql.gz$/', $basename);
  212. $list[$match[6]] = array($match[6], $name, $gz);
  213. }
  214. $last = end($list);
  215. if (count($list) === $last[0]) {
  216. return $list;
  217. } else {
  218. throw new \Exception("File {$files['0']} may be damaged, please check again");
  219. }
  220. break;
  221. case 'pathname':
  222. return "{$this->config['path']}{$this->file['name']}-{$this->file['part']}.sql";
  223. break;
  224. case 'filename':
  225. return "{$this->file['name']}-{$this->file['part']}.sql";
  226. break;
  227. case 'filepath':
  228. return $this->config['path'];
  229. break;
  230. default:
  231. $arr = array('pathname' => "{$this->config['path']}{$this->file['name']}-{$this->file['part']}.sql", 'filename' => "{$this->file['name']}-{$this->file['part']}.sql", 'filepath' => $this->config['path'], 'file' => $this->file);
  232. return $arr;
  233. }
  234. }
  235. /**
  236. * 删除备份文件
  237. * @param $time
  238. * @return mixed
  239. * @throws \Exception
  240. */
  241. public function delFile($time)
  242. {
  243. if ($time) {
  244. $file = $this->getFile('time', $time);
  245. array_map("unlink", $this->getFile('time', $time));
  246. if (count($this->getFile('time', $time))) {
  247. throw new \Exception("File {$time} deleted failed");
  248. } else {
  249. return $time;
  250. }
  251. } else {
  252. throw new \Exception("{$time} Time parameter is incorrect");
  253. }
  254. }
  255. /**
  256. * 下载备份
  257. *
  258. * @param $time
  259. * @param int $part
  260. * @throws \Exception
  261. */
  262. public function downloadFile($time, int $part = 0, bool $isFile = false)
  263. {
  264. $file = $this->getFile('time', $time);
  265. $fileName = $file[$part];
  266. if (file_exists($fileName)) {
  267. if ($isFile) {
  268. $key = password_hash(time() . $fileName, PASSWORD_DEFAULT);
  269. CacheService::set($key, ['path' => $fileName, 'fileName' => substr(strstr($fileName, 'backup'), 7)], 300);
  270. return $key;
  271. }
  272. ob_end_clean();
  273. header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
  274. header('Content-Description: File Transfer');
  275. header('Access-Control-Allow-Origin: ' . request()->domain());
  276. header('Content-Type: application/octet-stream');
  277. header('Content-Length: ' . filesize($fileName));
  278. header('Content-Disposition: attachment; filename=' . basename($fileName));
  279. return readfile($fileName);
  280. } else {
  281. throw new \Exception("{$time} File is abnormal");
  282. }
  283. }
  284. public function import($start)
  285. {
  286. //还原数据
  287. $db = self::connect();
  288. if ($this->config['compress']) {
  289. $gz = gzopen($this->file[1], 'r');
  290. $size = 0;
  291. } else {
  292. $size = filesize($this->file[1]);
  293. $gz = fopen($this->file[1], 'r');
  294. }
  295. $sql = '';
  296. if ($start) {
  297. $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start);
  298. }
  299. for ($i = 0; $i < 1000; $i++) {
  300. $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz);
  301. if (preg_match('/.*;$/', trim($sql))) {
  302. if (false !== $db->execute($sql)) {
  303. $start += strlen($sql);
  304. } else {
  305. return false;
  306. }
  307. $sql = '';
  308. } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) {
  309. return 0;
  310. }
  311. }
  312. return array($start, $size);
  313. }
  314. /**
  315. * 写入初始数据
  316. *
  317. * @return boolean true - 写入成功,false - 写入失败
  318. */
  319. public function Backup_Init()
  320. {
  321. $sql = "-- -----------------------------\n";
  322. $sql .= "-- Think MySQL Data Transfer \n";
  323. $sql .= "-- \n";
  324. $sql .= "-- Host : " . $this->dbconfig['hostname'] . "\n";
  325. $sql .= "-- Port : " . $this->dbconfig['hostport'] . "\n";
  326. $sql .= "-- Database : " . $this->dbconfig['database'] . "\n";
  327. $sql .= "-- \n";
  328. $sql .= "-- Part : #{$this->file['part']}\n";
  329. $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n";
  330. $sql .= "-- -----------------------------\n\n";
  331. $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
  332. return $this->write($sql);
  333. }
  334. /**
  335. * 备份表结构
  336. *
  337. * @param string $table
  338. * @param int $start
  339. * @return bool|int
  340. * @throws \think\db\exception\BindParamException
  341. * @throws \think\exception\PDOException
  342. */
  343. public function backup(string $table, int $start, $sql = '')
  344. {
  345. $db = self::connect();
  346. // 备份表结构
  347. if (0 == $start) {
  348. $result = $db->query("SHOW CREATE TABLE `{$table}`");
  349. $sql .= "\n";
  350. $sql .= "-- -----------------------------\n";
  351. $sql .= "-- Table structure for `{$table}`\n";
  352. $sql .= "-- -----------------------------\n";
  353. $sql .= "DROP TABLE IF EXISTS `{$table}`;\n";
  354. $sql .= trim($result[0]['Create Table']) . ";\n\n";
  355. }
  356. //数据总数
  357. $result = $db->query("SELECT COUNT(*) AS count FROM `{$table}`");
  358. $count = $result['0']['count'];
  359. //备份表数据
  360. if ($count) {
  361. //写入数据注释
  362. if (0 == $start) {
  363. $sql .= "-- -----------------------------\n";
  364. $sql .= "-- Records of `{$table}`\n";
  365. $sql .= "-- -----------------------------\n";
  366. }
  367. //备份数据记录
  368. $result = $db->query("SELECT * FROM `{$table}` LIMIT :MIN, 1000", ['MIN' => intval($start)]);
  369. foreach ($result as $row) {
  370. $row = array_map('addslashes', $row);
  371. $sql .= "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r", "\n"), array('\\r', '\\n'), implode("', '", $row)) . "');\n";
  372. }
  373. if (false === $this->write($sql)) {
  374. return false;
  375. }
  376. //还有更多数据
  377. if ($count > $start + 1000) {
  378. //return array($start + 1000, $count);
  379. return $this->backup($table, $start + 1000);
  380. }
  381. }
  382. //备份下一表
  383. return 0;
  384. }
  385. /**
  386. * 优化表
  387. *
  388. * @param array|string $tables
  389. * @throws \think\db\exception\BindParamException
  390. * @throws \think\exception\PDOException
  391. */
  392. public function optimize($tables)
  393. {
  394. if ($tables) {
  395. $db = self::connect();
  396. if (is_array($tables)) {
  397. $tables = implode('`,`', $tables);
  398. $list = $db->query("OPTIMIZE TABLE `{$tables}`");
  399. } else {
  400. $list = $db->query("OPTIMIZE TABLE {$tables}");
  401. }
  402. if (!$list) {
  403. throw new \Exception("data sheet'{$tables}'Repair mistakes please try again!");
  404. }
  405. return $list;
  406. } else {
  407. throw new \Exception("Please specify the table to be repaired!");
  408. }
  409. }
  410. /**
  411. * 修复表
  412. *
  413. * @param string|null $tables
  414. * @return array
  415. * @throws \think\db\exception\BindParamException
  416. * @throws \think\exception\PDOException
  417. */
  418. public function repair(?string $tables = null)
  419. {
  420. if ($tables) {
  421. $db = self::connect();
  422. if (is_array($tables)) {
  423. $tables = implode('`,`', $tables);
  424. $list = $db->query("REPAIR TABLE `{$tables}`");
  425. } else {
  426. $list = $db->query("REPAIR TABLE {$tables}");
  427. }
  428. if ($list) {
  429. return $list;
  430. } else {
  431. throw new \Exception("data sheet'{$tables}'Repair mistakes please try again!");
  432. }
  433. } else {
  434. throw new \Exception("Please specify the table to be repaired!");
  435. }
  436. }
  437. /**
  438. * 写入SQL语句
  439. *
  440. * @param string $sql 要写入的SQL语句
  441. * @return boolean true - 写入成功,false - 写入失败!
  442. */
  443. private function write(string $sql)
  444. {
  445. $size = strlen($sql);
  446. //由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%,
  447. //一般情况压缩率都会高于50%;
  448. $size = $this->config['compress'] ? $size / 2 : $size;
  449. $this->open($size);
  450. return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql);
  451. }
  452. /**
  453. * 打开一个卷,用于写入数据
  454. *
  455. * @param integer $size 写入数据的大小
  456. */
  457. private function open(int $size)
  458. {
  459. if ($this->fp) {
  460. $this->size += $size;
  461. if ($this->size > $this->config['part']) {
  462. $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
  463. $this->fp = null;
  464. $this->file['part']++;
  465. session('backup_file', $this->file);
  466. $this->Backup_Init();
  467. }
  468. } else {
  469. $backuppath = $this->config['path'];
  470. $filename = "{$backuppath}{$this->file['name']}-{$this->file['part']}.sql";
  471. if ($this->config['compress']) {
  472. $filename = "{$filename}.gz";
  473. $this->fp = @gzopen($filename, "a{$this->config['level']}");
  474. } else {
  475. $this->fp = @fopen($filename, 'a');
  476. }
  477. $this->size = filesize($filename) + $size;
  478. }
  479. }
  480. /**
  481. * 检查目录是否可写
  482. *
  483. * @param string $path
  484. * @return bool
  485. */
  486. protected function checkPath(string $path)
  487. {
  488. if (is_dir($path)) {
  489. return true;
  490. }
  491. if (mkdir($path, 0755, true)) {
  492. return true;
  493. } else {
  494. return false;
  495. }
  496. }
  497. /**
  498. * 析构方法,用于关闭文件资源
  499. */
  500. public function __destruct()
  501. {
  502. $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
  503. }
  504. }