sources.db.mysql.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <?php
  2. /**
  3. * @file
  4. */
  5. backup_migrate_include('sources.db');
  6. /**
  7. * @file
  8. * Functions to handle the direct to database source.
  9. */
  10. /**
  11. * A source type for backing up from database server.
  12. *
  13. * @ingroup backup_migrate_destinations
  14. */
  15. class backup_migrate_source_db_mysql extends backup_migrate_source_db {
  16. public function type_name() {
  17. return t("MySQL Database");
  18. }
  19. /**
  20. * Return a list of backup filetypes.
  21. */
  22. public function file_types() {
  23. return array(
  24. "sql" => array(
  25. "extension" => "sql",
  26. "filemime" => "text/x-sql",
  27. "backup" => TRUE,
  28. "restore" => TRUE,
  29. ),
  30. "mysql" => array(
  31. "extension" => "mysql",
  32. "filemime" => "text/x-sql",
  33. "backup" => TRUE,
  34. "restore" => TRUE,
  35. ),
  36. );
  37. }
  38. /**
  39. * Return the scheme for this db type.
  40. */
  41. public function default_scheme() {
  42. return 'mysql';
  43. }
  44. /**
  45. * Declare any mysql databases defined in the settings.php file as a possible source.
  46. */
  47. public function sources() {
  48. $out = array();
  49. global $databases;
  50. foreach ((array) $databases as $db_key => $target) {
  51. foreach ((array) $target as $tgt_key => $info) {
  52. // Only mysql/mysqli supported by this source.
  53. $key = $db_key . ':' . $tgt_key;
  54. if ($info['driver'] === 'mysql') {
  55. // Compile the database connection string.
  56. $url = 'mysql://';
  57. $url .= urlencode($info['username']) . ':' . urlencode($info['password']);
  58. $url .= '@';
  59. $url .= urlencode($info['host']);
  60. if (!empty($info['port'])) {
  61. $url .= ':' . $info['port'];
  62. }
  63. $url .= '/' . urlencode($info['database']);
  64. if ($source = backup_migrate_create_destination('mysql', array('url' => $url))) {
  65. // Treat the default database differently because it is probably
  66. // the only one available.
  67. if ($key == 'default:default') {
  68. $source->set_id('db');
  69. $source->set_name(t('Default Database'));
  70. // Dissalow backing up to the default database because that's confusing and potentially dangerous.
  71. $source->remove_op('scheduled backup');
  72. $source->remove_op('manual backup');
  73. }
  74. else {
  75. $source->set_id('db:' . $key);
  76. $source->set_name($key . ": " . $source->get_display_location());
  77. }
  78. $out[$source->get_id()] = $source;
  79. }
  80. }
  81. }
  82. }
  83. return $out;
  84. }
  85. /**
  86. * Get the file type for to backup this source to.
  87. */
  88. public function get_file_type_id() {
  89. return 'mysql';
  90. }
  91. /**
  92. * Backup the databases to a file.
  93. *
  94. * Returns a list of sql commands, one command per line.
  95. * That makes it easier to import without loading the whole file into memory.
  96. * The files are a little harder to read, but human-readability is not a priority.
  97. */
  98. public function _backup_db_to_file($file, $settings) {
  99. if (!empty($settings->filters['use_cli']) && $this->_backup_db_to_file_mysqldump($file, $settings)) {
  100. return TRUE;
  101. }
  102. $lines = 0;
  103. $exclude = !empty($settings->filters['exclude_tables']) ? $settings->filters['exclude_tables'] : array();
  104. $nodata = !empty($settings->filters['nodata_tables']) ? $settings->filters['nodata_tables'] : array();
  105. if ($file->open(TRUE)) {
  106. $file->write($this->_get_sql_file_header());
  107. $alltables = $this->_get_tables();
  108. $allviews = $this->_get_views();
  109. foreach ($alltables as $table) {
  110. if (_backup_migrate_check_timeout()) {
  111. return FALSE;
  112. }
  113. if ($table['name'] && !isset($exclude[$table['name']])) {
  114. $file->write($this->_get_table_structure_sql($table));
  115. $lines++;
  116. if (!in_array($table['name'], $nodata)) {
  117. $lines += $this->_dump_table_data_sql_to_file($file, $table);
  118. }
  119. }
  120. }
  121. foreach ($allviews as $view) {
  122. if (_backup_migrate_check_timeout()) {
  123. return FALSE;
  124. }
  125. if ($view['name'] && !isset($exclude[$view['name']])) {
  126. $file->write($this->_get_view_create_sql($view));
  127. }
  128. }
  129. $file->write($this->_get_sql_file_footer());
  130. $file->close();
  131. return $lines;
  132. }
  133. else {
  134. return FALSE;
  135. }
  136. }
  137. /**
  138. * Backup the databases to a file using the mysqldump command.
  139. */
  140. public function _backup_db_to_file_mysqldump($file, $settings) {
  141. $success = FALSE;
  142. $nodata_tables = array();
  143. $alltables = $this->_get_tables();
  144. $command = 'mysqldump --result-file=%file --opt -Q --host=%host --port=%port --user=%user --password=%pass %db';
  145. $args = array(
  146. '%file' => $file->filepath(),
  147. '%host' => $this->dest_url['host'],
  148. '%port' => isset($this->dest_url['port']) ? $this->dest_url['port'] : '3306',
  149. '%user' => $this->dest_url['user'],
  150. '%pass' => $this->dest_url['pass'],
  151. '%db' => $this->dest_url['path'],
  152. );
  153. // Ignore the excluded and no-data tables.
  154. if (!empty($settings->filters['exclude_tables'])) {
  155. $db = $this->dest_url['path'];
  156. foreach ((array) $settings->filters['exclude_tables'] as $table) {
  157. if (isset($alltables[$table])) {
  158. $command .= ' --ignore-table=' . $db . '.' . $table;
  159. }
  160. }
  161. foreach ((array) $settings->filters['nodata_tables'] as $table) {
  162. if (isset($alltables[$table])) {
  163. $nodata_tables[] = $table;
  164. $command .= ' --ignore-table=' . $db . '.' . $table;
  165. }
  166. }
  167. }
  168. $success = backup_migrate_exec($command, $args);
  169. // Get the nodata tables.
  170. if ($success && !empty($nodata_tables)) {
  171. $tables = implode(' ', array_unique($nodata_tables));
  172. $command = "mysqldump --no-data --opt -Q --host=%host --port=%port --user=%user --password=%pass %db $tables >> %file";
  173. $success = backup_migrate_exec($command, $args);
  174. }
  175. return $success;
  176. }
  177. /**
  178. * Backup the databases to a file.
  179. */
  180. public function _restore_db_from_file($file, $settings) {
  181. $num = 0;
  182. if ($file->open() && $conn = $this->_get_db_connection()) {
  183. // Optionally drop all existing tables.
  184. if (!empty($settings->filters['utils_drop_all_tables'])) {
  185. $all_tables = $this->_get_tables();
  186. $table_names = array_map('backup_migrate_array_name_value', $all_tables);
  187. $table_list = join(', ', $table_names);
  188. $stmt = $conn->prepare("DROP TABLE IF EXISTS $table_list;\n");
  189. $stmt->execute();
  190. }
  191. // Read one line at a time and run the query.
  192. while ($line = $this->_read_sql_command_from_file($file)) {
  193. if (_backup_migrate_check_timeout()) {
  194. return FALSE;
  195. }
  196. if ($line) {
  197. // Prepeare and exexute the statement instead of the api function to avoid substitution of '{' etc.
  198. $stmt = $conn->prepare($line);
  199. $stmt->execute();
  200. $num++;
  201. }
  202. }
  203. // Close the file with fclose/gzclose.
  204. $file->close();
  205. }
  206. else {
  207. drupal_set_message(t("Unable to open file %file to restore database", array("%file" => $file->filepath())), 'error');
  208. $num = FALSE;
  209. }
  210. return $num;
  211. }
  212. /**
  213. * Read a multiline sql command from a file.
  214. *
  215. * Supports the formatting created by mysqldump, but won't handle multiline comments.
  216. */
  217. public function _read_sql_command_from_file($file) {
  218. $out = '';
  219. while ($line = $file->read()) {
  220. $first2 = substr($line, 0, 2);
  221. $first3 = substr($line, 0, 3);
  222. // Ignore single line comments. This function doesn't support multiline comments or inline comments.
  223. if ($first2 != '--' && ($first2 != '/*' || $first3 == '/*!')) {
  224. $out .= ' ' . trim($line);
  225. // If a line ends in ; or */ it is a sql command.
  226. if (substr($out, strlen($out) - 1, 1) == ';') {
  227. return trim($out);
  228. }
  229. }
  230. }
  231. return trim($out);
  232. }
  233. /**
  234. * Get a list of tables in the database.
  235. */
  236. public function _get_table_names() {
  237. $out = array();
  238. foreach ($this->_get_tables() as $table) {
  239. $out[$table['name']] = $table['name'];
  240. }
  241. return $out;
  242. }
  243. /**
  244. * Get a list of views in the database.
  245. */
  246. public function _get_view_names() {
  247. $out = array();
  248. foreach ($this->_get_views() as $view) {
  249. $out[$view['name']] = $view['name'];
  250. }
  251. return $out;
  252. }
  253. /**
  254. * Lock the list of given tables in the database.
  255. */
  256. public function _lock_tables($tables) {
  257. if ($tables) {
  258. $tables_escaped = array();
  259. foreach ($tables as $table) {
  260. $tables_escaped[] = '`' . db_escape_table($table) . '` WRITE';
  261. }
  262. $this->query('LOCK TABLES ' . implode(', ', $tables_escaped));
  263. }
  264. }
  265. /**
  266. * Unlock all tables in the database.
  267. */
  268. public function _unlock_tables($settings) {
  269. $this->query('UNLOCK TABLES');
  270. }
  271. /**
  272. * Get a list of tables in the db.
  273. */
  274. public function _get_tables() {
  275. $out = array();
  276. // get auto_increment values and names of all tables.
  277. $tables = $this->query("show table status", array(), array('fetch' => PDO::FETCH_ASSOC));
  278. foreach ($tables as $table) {
  279. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  280. // See: http://drupal.org/node/1171866
  281. $table = array_change_key_case($table);
  282. if (!empty($table['engine'])) {
  283. $out[$table['name']] = $table;
  284. }
  285. }
  286. return $out;
  287. }
  288. /**
  289. * Get a list of views in the db.
  290. */
  291. public function _get_views() {
  292. $out = array();
  293. // get auto_increment values and names of all tables.
  294. $tables = $this->query("show table status", array(), array('fetch' => PDO::FETCH_ASSOC));
  295. foreach ($tables as $table) {
  296. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  297. // See: http://drupal.org/node/1171866
  298. $table = array_change_key_case($table);
  299. if (empty($table['engine'])) {
  300. $out[$table['name']] = $table;
  301. }
  302. }
  303. return $out;
  304. }
  305. /**
  306. * Get the sql for the structure of the given view.
  307. */
  308. public function _get_view_create_sql($view) {
  309. $out = "";
  310. // Switch SQL mode to get rid of "CREATE ALGORITHM..." what requires more permissions + troubles with the DEFINER user.
  311. $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
  312. $this->query("SET sql_mode = 'ANSI'");
  313. $result = $this->query("SHOW CREATE VIEW `" . $view['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
  314. $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
  315. foreach ($result as $create) {
  316. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  317. // See: http://drupal.org/node/1171866
  318. $create = array_change_key_case($create);
  319. $out .= "DROP VIEW IF EXISTS `" . $view['name'] . "`;\n";
  320. $out .= "SET sql_mode = 'ANSI';\n";
  321. $out .= strtr($create['create view'], "\n", " ") . ";\n";
  322. $out .= "SET sql_mode = '$sql_mode';\n";
  323. }
  324. return $out;
  325. }
  326. /**
  327. * Get the sql for the structure of the given table.
  328. */
  329. public function _get_table_structure_sql($table) {
  330. $out = "";
  331. $result = $this->query("SHOW CREATE TABLE `" . $table['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
  332. foreach ($result as $create) {
  333. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  334. // See: http://drupal.org/node/1171866
  335. $create = array_change_key_case($create);
  336. $out .= "DROP TABLE IF EXISTS `" . $table['name'] . "`;\n";
  337. // Remove newlines and convert " to ` because PDO seems to convert those for some reason.
  338. $out .= strtr($create['create table'], array("\n" => ' ', '"' => '`'));
  339. if ($table['auto_increment']) {
  340. $out .= " AUTO_INCREMENT=" . $table['auto_increment'];
  341. }
  342. $out .= ";\n";
  343. }
  344. return $out;
  345. }
  346. /**
  347. * Get the sql to insert the data for a given table.
  348. */
  349. public function _dump_table_data_sql_to_file($file, $table) {
  350. $rows_per_query = variable_get('backup_migrate_data_rows_per_query', 1000);
  351. $rows_per_line = variable_get('backup_migrate_data_rows_per_line', 30);
  352. $bytes_per_line = variable_get('backup_migrate_data_bytes_per_line', 2000);
  353. if (variable_get('backup_migrate_verbose')) {
  354. _backup_migrate_message('Table: %table', array('%table' => $table['name']), 'success');
  355. }
  356. // Escape backslashes, PHP code, special chars.
  357. $search = array('\\', "'", "\x00", "\x0a", "\x0d", "\x1a");
  358. $replace = array('\\\\', "''", '\0', '\n', '\r', '\Z');
  359. $lines = 0;
  360. $from = 0;
  361. $args = array('fetch' => PDO::FETCH_ASSOC);
  362. while ($data = $this->query("SELECT * FROM `" . $table['name'] . "`", array(), $args, $from, $rows_per_query)) {
  363. if ($data->rowCount() == 0) {
  364. break;
  365. }
  366. $rows = $bytes = 0;
  367. $line = array();
  368. foreach ($data as $row) {
  369. $from++;
  370. // DB Escape the values.
  371. $items = array();
  372. foreach ($row as $key => $value) {
  373. $items[] = is_null($value) ? "null" : "'" . str_replace($search, $replace, $value) . "'";
  374. }
  375. // If there is a row to be added.
  376. if ($items) {
  377. // Start a new line if we need to.
  378. if ($rows == 0) {
  379. $file->write("INSERT INTO `" . $table['name'] . "` VALUES ");
  380. $bytes = $rows = 0;
  381. }
  382. // Otherwise add a comma to end the previous entry.
  383. else {
  384. $file->write(",");
  385. }
  386. // Write the data itself.
  387. $sql = implode(',', $items);
  388. $file->write('(' . $sql . ')');
  389. $bytes += strlen($sql);
  390. $rows++;
  391. // Finish the last line if we've added enough items.
  392. if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) {
  393. $file->write(";\n");
  394. $lines++;
  395. $bytes = $rows = 0;
  396. }
  397. }
  398. }
  399. // Finish any unfinished insert statements.
  400. if ($rows > 0) {
  401. $file->write(";\n");
  402. $lines++;
  403. }
  404. }
  405. if (variable_get('backup_migrate_verbose')) {
  406. _backup_migrate_message('Peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
  407. }
  408. return $lines;
  409. }
  410. /**
  411. * Get the db connection for the specified db.
  412. */
  413. public function _get_db_connection() {
  414. if (!$this->connection) {
  415. $this->connection = parent::_get_db_connection();
  416. // Set the sql mode because the default is ANSI,TRADITIONAL which is not aware of collation or storage engine.
  417. $this->connection->exec("SET sql_mode=''");
  418. }
  419. return $this->connection;
  420. }
  421. /**
  422. * Run a query on this source's database using Drupal's MySQL engine.
  423. *
  424. * @param string $query
  425. * The query string.
  426. * @param array $args
  427. * Arguments for the query.
  428. * @param array $options
  429. * Options to pass to the query.
  430. * @param int|null $from
  431. * The starting point for the query; when passed will perform a queryRange()
  432. * method instead of a regular query().
  433. * @param int|null $count
  434. * The number of records to obtain from this query. Will be ignored if the
  435. * $from argument is empty.
  436. *
  437. * @see DatabaseConnection_mysql::query()
  438. * @see DatabaseConnection_mysql::queryRange()
  439. */
  440. public function query($query, array $args = array(), array $options = array(), $from = NULL, $count = NULL) {
  441. if ($conn = $this->_get_db_connection()) {
  442. // If no $from is passed in, just do a basic query.
  443. if (is_null($from)) {
  444. return $conn->query($query, $args, $options);
  445. }
  446. // The $from variable was passed in, so do a ranged query.
  447. else {
  448. return $conn->queryRange($query, $from, $count, $args, $options);
  449. }
  450. }
  451. }
  452. /**
  453. * The header for the top of the sql dump file. These commands set the connection
  454. * character encoding to help prevent encoding conversion issues.
  455. */
  456. public function _get_sql_file_header() {
  457. $info = $this->_db_info();
  458. return "-- Backup and Migrate (Drupal) MySQL Dump
  459. -- Backup and Migrate Version: " . BACKUP_MIGRATE_VERSION . "
  460. -- http://drupal.org/project/backup_migrate
  461. -- Drupal Version: " . VERSION . "
  462. -- http://drupal.org/
  463. --
  464. -- Host: " . url('', array('absolute' => TRUE)) . "
  465. -- Site Name: " . url('', array('absolute' => TRUE)) . "
  466. -- Generation Time: " . format_date(time(), 'custom', 'r') . "
  467. -- MySQL Version: " . $info['version'] . "
  468. /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
  469. /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
  470. /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
  471. /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
  472. /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
  473. /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;
  474. SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";
  475. SET NAMES utf8;
  476. ";
  477. }
  478. /**
  479. * The footer of the sql dump file.
  480. */
  481. public function _get_sql_file_footer() {
  482. return "
  483. /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
  484. /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
  485. /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
  486. /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
  487. /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
  488. /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
  489. ";
  490. }
  491. /**
  492. * Get the version info for the given DB.
  493. */
  494. public function _db_info() {
  495. $db = $this->_get_db_connection();
  496. return array(
  497. 'type' => 'mysql',
  498. 'version' => $db ? $db->version() : t('Unknown'),
  499. );
  500. }
  501. }