'admin/settings/db_maintenance', 'title' => t('DB maintenance'), 'description' => t('Executes a cron-based OPTIMIZE TABLE query on MyISAM, InnoDB, or BerkeleyDB database tables.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('db_maintenance_admin_settings'), 'access' => user_access('administer site configuration'), 'type' => MENU_NORMAL_ITEM, // optional ); $items[] = array( 'path' => 'db_maintenance/optimize', 'title' => t('Optimize tables'), 'callback' => 'db_maintenance_optimize_tables_page', 'access' => user_access('administer site configuration'), 'type' => MENU_CALLBACK, ); } return $items; } /** * Callback page for manually optimizing tables. * */ function db_maintenance_optimize_tables_page() { db_maintenance_optimize_tables(); drupal_set_message(t('Database tables optimized')); drupal_goto('admin/settings/db_maintenance'); } /** * Get a list of all the tables in a database. * * @param $db The name of the database connection to query for tables. * @return An array representing the tables in the specified database. */ function _db_maintenance_list_mysql_tables() { $table_names = array(); // Set the database to query. $previous = db_set_active($db); $result = db_query('SHOW TABLES'); // Return to the previously set database. db_set_active($previous); while ($table_name = db_fetch_array($result)) { $table_name = current($table_name); $table_names[$table_name] = $table_name; } return $table_names; } /** * Implementation of hook_cron(). * */ function db_maintenance_cron() { $last_run = variable_get('db_maintenance_cron_last', 0); $now = time(); $optimize_frequency = variable_get('db_maintenance_cron_frequency', 86400); $email = array(); $interval = $now - $optimize_frequency; // Only run cron if enough time has elapsed if ($optimize_frequencty != 0 && $interval > $last_run) { db_maintenance_optimize_tables(); } $last_db_backup = variable_get('db_maintenance_last_db_backup', 0); $db_backup_freq = variable_get('db_maintenance_db_backup_frequency', 0); $files_backup_freq = variable_get('db_maintenance_files_backup_frequency', 0); $last_files_backup = variable_get('db_maintenance_last_files_backup', 0); if ($db_backup_freq != 0 && $last_db_backup <= $now - $db_backup_freq) { $email['db_backup'] = db_maintenance_do_db_backup(); variable_set('db_maintenance_last_db_backup', $now); } if ($files_backup_freq != 0 && $last_files_backup <= $now - $files_backup_freq) { $email['files_backup'] = db_maintanence_do_files_backup(); variable_set('db_maintenance_last_files_backup', $now); } db_maintenance_email($email); } /** * Perform the maintenance. * */ function db_maintenance_optimize_tables() { global $db_url; // Set the databases array if not already set in $db_url. if (is_array(($db_url))) { $databases = $db_url; } else { $databases['default'] = $db_url; } // Loop through each database optimizing any selected tables. foreach ($databases as $db => $connection) { $config_tables = variable_get('db_maintenance_table_list_'. $db, NULL); // Only proceed if tables are selected for this database. if (is_array($config_tables) && sizeof($config_tables) > 0) { $db_name = $db == 'default' ? 'Drupal' : $db; // Only proceed if it's a MySQL database. if (strpos($connection, 'mysql://') !== 0 && strpos($connection, 'mysqli://') !== 0) { watchdog('db_maintenance', t('Tables are configured to be optimized, but the !db database is not using MySQL', array('!db' => $db_name))); } else { $tables = implode(', ', $config_tables); // Set the database to query. $previous = db_set_active($db); $result = db_query('OPTIMIZE TABLE %s', $tables); $status = db_fetch_array($result); while ($status[] = db_fetch_array($result)) { // get all of the rows } db_maintenance_check_status($status, 'OPTIMIZE'); // Return to the previously set database. db_set_active($previous); } } } variable_set('db_maintenance_cron_last', time()); } /** * Administration settings * * options: log each optimization * multi-select list of tables to optimize * * @return array */ function db_maintenance_admin_settings() { global $db_url; $form = array(); $form['db_maintenance_log'] = array( '#type' => 'checkbox', '#title' => 'Log OPTIMIZE queries', '#default_value' => variable_get('db_maintenance_log', 0), '#description' => t('If enabled, a watchdog entry will be made each time tables are optimized, containing information which tables were involved.') ); $form['db_maintenance_repair'] = array( '#type' => 'checkbox', '#title' => 'Attempt REPAIR of table if OPTIMIZE is problematic', '#default_value' => variable_get('db_maintenance_repair', 0), '#description' => t('If enabled and a table receives a non-okay status from the OPTIMIZE then a repair of that table will be attempted. In the case of REPAIR all resulting status are logged via watchdog.') ); // array keyed by time (in seconds) with 0 indicating never $frequency = array( 0 => t('Never'), 3600 => t('Hourly'), 86400 => t('Daily'), 604800 => t('Weekly'), 2592000 => t('Monthly'), ); $form['db_maintenance_cron_frequency'] = array( '#type' => 'select', '#title' => t('Optimize tables'), '#options' => $frequency, '#default_value' => variable_get('db_maintenance_cron_frequency', 86400), '#description' => t('Select how often database tables should be optimized.') .' '. l(t('Optimize now.'), 'db_maintenance/optimize'), ); // Set the databases array if not already set in $db_url. if (is_array(($db_url))) { $databases = $db_url; } else { $databases['default'] = $db_url; } // Loop through each database and list the possible tables to optimize. foreach ($databases as $db => $connection) { $options = _db_maintenance_list_mysql_tables($db); $form['db_maintenance_table_list_'. $db] = array( '#type' => 'select', '#title' => t('Tables in the !db database', array('!db' => $db == 'default' ? 'Drupal' : $db)), '#options' => $options, '#default_value' => variable_get('db_maintenance_table_list_'. $db, ''), '#description' => t('Selected tables will be optimized during cron runs.'), '#multiple' => true, '#attributes' => array('size' => count($options)), ); } $form['db_maintenance_db_backup_frequency'] = array( '#type' => 'select', '#title' => t('Database Backup frequency'), '#description' => t('How often to backup the database. This is the most frequent this will run, but no more often than the frequency of the drupal cron script.'), '#options' => $frequency, '#default_value' => variable_get('db_maintenance_db_backup_frequency', 0), ); $form['db_maintenance_files_backup_frequency'] = array( '#type' => 'select', '#title' => t('Files Backup frequency'), '#description' => t('How often to backup the files directory. This is the most frequent this will run, but no more often than the frequency of the drupal cron script.'), '#options' => $frequency, '#default_value' => variable_get('db_maintenance_files_backup_frequency', 0), ); $form['db_maintenance_backup_directory'] = array( '#type' => 'textfield', '#title' => t('Backup directory'), '#description' => t('Directory to store backup files in'), '#default_value' => variable_get('db_maintenance_backup_directory', '/tmp'), ); $form['db_maintenance_path_to_tar'] = array( '#type' => 'textfield', '#title' => t('Path to tar'), '#default_value' => variable_get('db_maintenance_path_to_tar', '/bin/tar'), ); $form['db_maintenance_path_to_mysqldump'] = array( '#type' => 'textfield', '#title' => t('Path to mysqldump'), '#default_value' => variable_get('db_maintenance_path_to_mysqldump', '/usr/bin/mysqldump'), ); $form['db_maintenance_email_notify'] = array( '#type' => 'textfield', '#title' => t('Email address to notify'), '#default_value' => variable_get('db_maintenance_email_notify', ''), '#description' => t('List of email addresses to notify when tasks run. Seperate multiple addressses with a comma. Leave empty for no notification') ); return system_settings_form($form); } function db_maintenance_admin_settings_validate($formid, &$form) { if (!is_dir($form['db_maintenance_backup_directory'])) { form_set_error('db_maintenance_backup_directory', t('Backup directory does not exist or is not a directory.')); } elseif (!is_writable($form['db_maintenance_backup_directory'])) { form_set_error('db_maintenance_backup_directory', t('Backup directory is not writable.')); } if (!is_file($form['db_maintenance_path_to_tar'])) { form_set_error('db_maintenance_path_to_tar', t('Path to tar is incorrect.')); } elseif(!is_executable($form['db_maintenance_path_to_tar'])) { form_set_error('db_maintenance_path_to_tar', t('tar is not executable.')); } if (!is_file($form['db_maintenance_path_to_mysqldump'])) { form_set_error('db_maintenance_path_to_mysqldump', t('Path to mysqldump is incorrect.')); } elseif(!is_executable($form['db_maintenance_path_to_mysqldump'])) { form_set_error('db_maintenance_path_to_mysqldump', t('mysqldump is not executable.')); } $emails = explode(',', $form['db_maintenance_email_notify']); if (!empty($emails)) { $bad = array(); foreach($emails as $email) { $email = trim($email); if (!valid_email_address($email)) { $bad[] = $email; } } if (!empty($bad)) { form_set_error('db_maintenance_email_notify', t('The following email address(es) were invalid; @badmail', array('@badmail' => implode(', ', $bad)))); } } } function db_maintenance_check_status($status, $op) { // mysql return codes indicating okay/success all others assumed to be "bad" $DB_MAINTENANCE_OKAY = array('Table is already up to date', 'OK'); foreach ($status as $key => $return) { if (is_numeric($key) && !empty($return)) { if (in_array($return['Msg_text'], $DB_MAINTENANCE_OKAY)) { // everything okay only log if explicitly set or we did a repair if (variable_get('db_maintenance_log', 0) || $op == 'REPAIR') { watchdog('db_maintenance', t('Success: !op table !table, type: !type, message: !message', array('!op' => $op, '!table' => $return['Table'], '!type' => $return['Msg_type'], '!message' => $return['Msg_text']))); } } else { // problems encountered watchdog('db_maintenance', t('Failure: !op table !table type: !type, message: !message', array('!op' => $op, '!table' => $return['Table'], '!type' => $return['Msg_type'], '!message' => $return['Msg_text'])), WATCHDOG_ERROR); // attempt repair if config is set and makes sense if (variable_get('db_maintenance_repair', 0) && $op == 'OPTIMIZE' && !empty($return['Table'])) { $result = db_query('REPAIR TABLE %s', $return['Table']); $status = array(); while ($status[] = db_fetch_array($result)) { // get all of the rows } db_maintenance_check_status($status, 'REPAIR'); } } } } // check non-numeric entries (mysql has such a lovely return setup) if (isset($status['Table']) && isset($status['Op']) && isset($status['Msg_type']) && isset($status['Msg_text'])) { $new_status = array(); $new_status[] = array('Table' => $status['Table'], 'Op' => $status['Op'], 'Msg_type' => $status['Msg_type'], 'Msg_text' => $status['Msg_text']); db_maintenance_check_status($new_status, $op); } } function db_maintenance_email($email) { global $base_url; $email_list = variable_get('db_maintenance_email_notify', ''); if (!empty($email) && !empty($email_list)) { $message = array(); $backupdir = variable_get('db_maintenance_backup_directory', '/tmp'); foreach ($email as $task => $status) { switch ($task) { case 'db_backup': // db_backup returns filename if (is_file($status)) { $message[] = t('Database backed up. mysqldump is available at \'!STATUS\' when connected to \'!URL\'', array('!STATUS' => $status, '!URL' => $base_url)); } else { $message[] = t('Database backup failed. Please review watchdog for important messages'); } break; case 'files_backup': if (is_file($status)) { $message[] = t('Site files backed up. Files tarball are available at \'!STATUS\' when connected to \'!URL\'', array('!STATUS' => $status, '!URL' => $base_url)); } else { $message[] = t('Files directory backup failed. Please review watchdog for important messages'); } break; case 'db_maintenance': $badstatus = db_maintenance_return_non_okay($status); if (empty($badstatus)) { $message[] = t('DB maintenance tasks ran successfully.'); } else { $message[] = t('DB maintenance tasks encountered at least one issue. These may have been automatically been recovered from, but at least one table was in a non-okay state. Please review the messages below, watchdog, and manually review the database table status to ensure the system is working properly.'); $message[] = db_maintenance_format_status($badstatus); } break; } } $body = implode("\n", $message); $subject = t('[drupal db_maintenance] site: !URL', array('!URL' => $base_url)); drupal_mail('db_maintenance', $email_list, $subject, $body); } } function db_maintenance_do_db_backup() { global $db_url; $mysqldump = variable_get('db_maintenance_path_to_mysqldump', '/usr/bin/mysqldump'); $backupdir = variable_get('db_maintenance_backup_directory', '/tmp'); $dateformat = 'Ymd_H-i-s'; $dbname = db_maintenance_get_db_info('dbname'); $now = time(); $date = date($dateformat, $now); $filename = "{$backupdir}/{$date}_{$dbname}_db.sql"; $mysqloptions = db_maintenance_get_mysql_options(); $mysqldumpexec = "$mysqldump $mysqloptions > $filename"; if (is_dir($backupdir)) { exec($mysqldumpexec, $output, $return); // $return is return value of exec'd command (0 is okay status) if (!$return && !is_file($filename)) { watchdog('db_maintenance', $output, WATCHDOG_ERROR); return FALSE; } else { return $filename; } } else { watchdog('db_maintenance', t('backup directory does not exist'), WATCHDOG_ERROR); return FALSE; } } function db_maintanence_do_files_backup() { $filespath = file_directory_path(); $tarpath = variable_get('db_maintenance_path_to_tar', '/bin/tar'); $backupdir = variable_get('db_maintenance_backup_directory', '/tmp'); $dateformat = 'Ymd_H-i-s'; $dbname = db_maintenance_get_db_info('dbname'); $now = time(); $date = date($dateformat, $now); if (is_file($tarpath) && is_dir($filespath) && is_dir($backupdir)) { $backupname = "{$date}_{$dbname}_files.tar.gz"; $command = "tar -cvzf {$backupdir}/{$backupname} {$filespath}"; exec($command, $output, $return); // $return is return value of exec'd command (0 is okay status) if ($return) { watchdog('db_maintenance', $output, WATCHDOG_ERROR); return FALSE; } else { if (!is_file("{$backupdir}/{$backupname}")) { watchdog('db_maintenance', t('failed to create files backup file') .': '. $output, WATCHDOG_ERROR); return FALSE; } return "{$backupdir}/{$backupname}"; } } else { watchdog('db_maintenance', t('Files dir not present, backup dir not present or path to tar incorrect'), WATCHDOG_ERROR); return FALSE; } } function db_maintenance_get_db_info($param) { global $db_url; $url = parse_url($db_url); switch (strtolower($param)) { case 'pass': case 'password': if (isset($url['pass'])) { $return = urldecode($url['pass']); } else { $return = ''; } break; case 'user': case 'username': $return = urldecode($url['user']); break; case 'host': case 'hostname': $return = urldecode($url['host']); break; case 'db': case 'database': case 'dbname': case 'name': $url['path'] = urldecode($url['path']); $return = substr($url['path'], 1); break; case 'port': if (isset($url['port'])) { $return = urldecode($url['port']); } else { $return = ''; } break; default: $return = ''; break; } return $return; } function db_maintenance_get_mysql_options() { global $db_url; $url = parse_url($db_url); $url['host'] = urldecode($url['host']); $url['path'] = urldecode($url['path']); $url['user'] = urldecode($url['user']); if (isset($url['pass'])) { $url['pass'] = urldecode($url['pass']); } else { $url['pass'] = ''; } if ($url['host']) { $options[] = '--host='. $url['host']; } if (!empty($url['port'])) { $options[] = '--port='. $url['port']; } if ($url['pass'] != '') { $options[] = '--password='. $url['pass']; } if ($url['user']) { $options[] = '--user='. $url['user']; } $return = implode(' ', $options); $return .= ' '. substr($url['path'], 1); // db name is 'path' with '/' pre-pended return $return; } /** * Implementation of hook_block(). * * @param string $op * @param integer $delta * @return string or array */ function db_maintenance_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list': $blocks[0]['info'] = 'Database size'; return $blocks; break; case 'view': if (user_access('access content')) { return array('content' => theme('db_maintenance_block')); } break; } } /** * MySQL size status by pcs305 */ function theme_db_maintenance_block() { // Database size = table size + index size: $rows = db_query("SHOW TABLE STATUS"); $dbssize = 0; while ($row = mysql_fetch_array($rows)) { $dbssize += $row['Data_length'] + $row['Index_length']; } $dbssize = db_maintenance_db_size_info($dbssize); return "{$dbssize['size']} {$dbssize['type']}"; } /** * Turn bytes into something more readable like KB, MB, etc. * * @return array */ function db_maintenance_db_size_info($dbsize) { $bytes = array('KB', 'KB', 'MB', 'GB', 'TB'); if ($dbsize < 1024) { $dbsize = 1; } for ($i = 0; $dbsize > 1024; $i++) { $dbsize /= 1024; } $db_size_info['size'] = ceil($dbsize); $db_size_info['type'] = $bytes[$i]; return $db_size_info; } /** * Implementation of hook_simpletest(). */ function db_maintenance_simpletest() { // Scan through mymodule/tests directory for any .test files to tell SimpleTest module. $tests = file_scan_directory(drupal_get_path('module', 'db_maintenance') .'/tests', '\.test'); return array_keys($tests); }