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