MysqlBackupOrRestore.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <?php
  2. /*
  3. * @Author: juneChen && junechen_0606@163.com
  4. * @Date: 2022-12-06 16:04:14
  5. * @LastEditors: juneChen && junechen_0606@163.com
  6. * @LastEditTime: 2022-12-15 15:48:54
  7. * @Description: 备份Mysql数据表到文件
  8. *
  9. * Copyright (c) 2022 by juneChen, All Rights Reserved.
  10. */
  11. namespace juneChen\dataTableBackup;
  12. use think\facade\Db;
  13. class MysqlBackupOrRestore
  14. {
  15. /**
  16. * 文件指针
  17. * @var resource
  18. */
  19. private $fp;
  20. /**
  21. * 备份文件信息 part - 卷号,name - 文件名
  22. * @var array
  23. */
  24. private array $file = [
  25. 'name' => "Backup",
  26. 'part' => 1
  27. ];
  28. /**
  29. * 当前打开文件大小
  30. * @var integer
  31. */
  32. private int $size = 0;
  33. /**
  34. * 备份配置
  35. * @var array
  36. */
  37. private array $config = [
  38. 'path' => "data/",
  39. //备份卷大小 默认 20M
  40. 'part' => 20971520,
  41. // 备份文件是否启用压缩
  42. "compress" => 1,
  43. // 备份文件压缩级别
  44. "level" => 9,
  45. ];
  46. /**
  47. * 数据库配置
  48. * @var string
  49. */
  50. private static string $database_config = "mysql";
  51. /**
  52. * 数据库备份构造方法
  53. * @param array $file 备份或还原的文件信息
  54. * @param array $config 备份配置信息
  55. */
  56. public function __construct(array $file = [], array $config = [])
  57. {
  58. if (!empty($file)) {
  59. $this->file = $file;
  60. }
  61. if (!empty($config)) {
  62. $this->config = $config;
  63. }
  64. if (isset($config['database_config'])) {
  65. self::$database_config = $config['database_config'];
  66. }
  67. }
  68. /**
  69. * 获取数据库所有表信息列表
  70. *
  71. * @return array
  72. * @author juneChen <junechen_0606@163.com>
  73. */
  74. public function getTableList(): array
  75. {
  76. $tableList = Db::connect(self::$database_config)->query("SHOW TABLE STATUS");
  77. if (empty($tableList)) {
  78. return [];
  79. }
  80. return array_map(function ($val) {
  81. return [
  82. "tableName" => $val['Name'],
  83. "lineNumber" => $val['Rows'],
  84. "size" => $this->format_bytes($val['Data_length']),
  85. "bredundancy" => $this->format_bytes($val['Data_free']),
  86. "comment" => $val['Comment'],
  87. ];
  88. }, $tableList);
  89. }
  90. /**
  91. * 写入初始数据
  92. * @return bool|int
  93. */
  94. public function create(): bool|int
  95. {
  96. $database_config = Db::connect(self::$database_config)->getConfig();
  97. $sql = "-- -----------------------------\n";
  98. $sql .= "-- MySQL Data Transfer\n";
  99. $sql .= "--\n";
  100. $sql .= "-- Host : " . $database_config['hostname'] . "\n";
  101. $sql .= "-- Port : " . $database_config['hostport'] . "\n";
  102. $sql .= "-- Database : " . $database_config['database'] . "\n";
  103. $sql .= "--\n";
  104. $sql .= "-- Part : #{$this->file['part']}\n";
  105. $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n";
  106. $sql .= "-- -----------------------------\n\n";
  107. $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
  108. return $this->write($sql);
  109. }
  110. /**
  111. * 备份表结构和数据
  112. * @param string $table 表名
  113. * @param integer $start 起始行数
  114. * @return array|bool|int false - 备份失败
  115. */
  116. public function backup(string $table = '', int $start = 0): int|bool|array
  117. {
  118. // 备份表结构
  119. if (0 == $start) {
  120. $result = Db::connect(self::$database_config)->query("SHOW CREATE TABLE `$table`");
  121. $result = array_map('array_change_key_case', $result);
  122. $sql = "\n";
  123. $sql .= "-- -----------------------------\n";
  124. $sql .= "-- Table structure for `$table`\n";
  125. $sql .= "-- -----------------------------\n";
  126. $sql .= "DROP TABLE IF EXISTS `$table`;\n";
  127. $sql .= trim($result[0]['create table']) . ";\n\n";
  128. if (false === $this->write($sql)) {
  129. return false;
  130. }
  131. }
  132. // 数据总数
  133. $result = Db::connect(self::$database_config)->query("SELECT COUNT(*) AS count FROM `$table`");
  134. $count = $result['0']['count'];
  135. //备份表数据
  136. if ($count) {
  137. // 写入数据注释
  138. if (0 == $start) {
  139. $sql = "-- -----------------------------\n";
  140. $sql .= "-- Records of `{$table}`\n";
  141. $sql .= "-- -----------------------------\n";
  142. $this->write($sql);
  143. }
  144. // 备份数据记录
  145. $result = Db::connect(self::$database_config)->query("SELECT * FROM `$table` LIMIT $start, 1000");
  146. foreach ($result as $row) {
  147. $row = array_map(function ($val) {
  148. // 解决数据值是 null 时,addslashes报错
  149. if (is_null($val)) {
  150. $val = 'NULL';
  151. } elseif (is_string($val)) {
  152. $val = "'" . addslashes($val) . "'";
  153. }
  154. return $val;
  155. }, $row);
  156. $sql = str_replace('%DATA%', implode(", ", $row), "INSERT INTO `$table` VALUES (%DATA%);\n");
  157. if (false === $this->write($sql)) {
  158. return false;
  159. }
  160. }
  161. //还有更多数据
  162. if ($count > $start + 1000) {
  163. return [$start + 1000, $count];
  164. }
  165. }
  166. // 备份下一表
  167. return 0;
  168. }
  169. /**
  170. * 恢复数据
  171. *
  172. * @param integer $start 起始位置
  173. * @return array|bool|int
  174. * @author juneChen <junechen_0606@163.com>
  175. */
  176. public function restore(int $start = 0): int|bool|array
  177. {
  178. if ($this->config['compress']) {
  179. $gz = gzopen($this->file['name'], 'r');
  180. $size = 0;
  181. } else {
  182. $size = filesize($this->file['name']);
  183. $gz = fopen($this->file['name'], 'r');
  184. }
  185. $sql = '';
  186. if ($start) {
  187. $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start);
  188. }
  189. for ($i = 0; $i < 1000; $i++) {
  190. $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz);
  191. if (preg_match('/.*;$/', trim($sql))) {
  192. if (false !== Db::connect(self::$database_config)->execute($sql)) {
  193. $start += strlen($sql);
  194. } else {
  195. return false;
  196. }
  197. $sql = '';
  198. } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) {
  199. return 0;
  200. }
  201. }
  202. return [$start, $size];
  203. }
  204. /**
  205. * 析构方法,用于关闭文件资源
  206. */
  207. public function __destruct()
  208. {
  209. if ($this->fp) $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
  210. }
  211. /**
  212. * 打开一个卷,用于写入数据
  213. *
  214. * @param float $size 写入数据的大小
  215. * @return void
  216. * @author juneChen <junechen_0606@163.com>
  217. */
  218. private function open(float $size = 0): void
  219. {
  220. if ($this->fp) {
  221. $this->size += $size;
  222. if ($this->size > $this->config['part']) {
  223. $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
  224. $this->fp = null;
  225. $this->file['part']++;
  226. $this->create();
  227. }
  228. } else {
  229. $backup_path = $this->config['path'];
  230. $filename = "{$backup_path}{$this->file['name']}-{$this->file['part']}.sql";
  231. if ($this->config['compress']) {
  232. $filename = "{$filename}.gz";
  233. $this->fp = @gzopen($filename, "a{$this->config['level']}");
  234. } else {
  235. $this->fp = @fopen($filename, 'a');
  236. }
  237. $this->size = filesize($filename) + $size;
  238. }
  239. }
  240. /**
  241. * 写入SQL语句
  242. * @param string $sql 要写入的SQL语句
  243. * @return bool|int
  244. */
  245. private function write(string $sql = ''): bool|int
  246. {
  247. $size = strlen($sql);
  248. // 由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%,
  249. // 一般情况压缩率都会高于50%;
  250. $size = $this->config['compress'] ? $size / 2 : $size;
  251. $this->open($size);
  252. return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql);
  253. }
  254. /**
  255. * 格式化字节大小
  256. *
  257. * @param int $size 字节数
  258. * @param string $delimiter 数字和单位分隔符
  259. * @return string 格式化后的带单位的大小
  260. * @author juneChen <junechen_0606@163.com>
  261. */
  262. private function format_bytes(int $size, string $delimiter = ''): string
  263. {
  264. $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
  265. for ($i = 0; $size >= 1024 && $i < 5; $i++) $size /= 1024;
  266. return round($size, 2) . $delimiter . $units[$i];
  267. }
  268. }