You are here

backup_migrate.module in Backup and Migrate 5

Create (manually or scheduled) and restore backups of your Drupal MySQL database with an option to exclude table data (f.e. cache_*)

File

backup_migrate.module
View source
<?php

/**
 * @file
 * Create (manually or scheduled) and restore backups of your Drupal MySQL
 * database with an option to exclude table data (f.e. cache_*)
 */

/**
 * Implementation of hook_menu().
 */
function backup_migrate_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/content/backup_migrate',
      'title' => t('Backup and Migrate'),
      'description' => t('Backup/restore your database or migrate data to or from another Drupal site.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'backup_migrate_backup',
      ),
      'access' => user_access('perform backup'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/export',
      'title' => t('Backup/Export DB'),
      'description' => t('Backup the database.'),
      'callback' => '_backup_migrate_export',
      'access' => user_access('perform backup'),
      'weight' => 0,
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/restore',
      'title' => t('Restore/Import DB'),
      'description' => t('Restore the database from a previous backup'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'backup_migrate_restore',
      ),
      'access' => user_access('restore from backup'),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files',
      'title' => t('Saved Backups'),
      'description' => t('View existing backup files'),
      'callback' => '_backup_migrate_list_files',
      'access' => user_access('access backup files'),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files/manual',
      'title' => t('Manual Backups'),
      'callback' => '_backup_migrate_list_files',
      'callback arguments' => array(
        'manual',
      ),
      'access' => user_access('access backup files'),
      'weight' => 1,
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/files/scheduled',
      'title' => t('Scheduled Backups'),
      'callback' => '_backup_migrate_list_files',
      'callback arguments' => array(
        'scheduled',
      ),
      'access' => user_access('access backup files'),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/schedule',
      'title' => t('Backup Schedule'),
      'description' => t('View existing backup files'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'backup_migrate_schedule',
      ),
      'access' => user_access('access backup files'),
      'weight' => 3,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/restorefile',
      'title' => t('restore from backup'),
      'description' => t('Restore database from a backup file on the server'),
      'callback' => '_backup_migrate_restore_from_server',
      'access' => user_access('restore from backup'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/content/backup_migrate/delete',
      'title' => t('Delete File'),
      'description' => t('Delete a backup file'),
      'callback' => '_backup_migrate_delete',
      'access' => user_access('delete backup files'),
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_cron(),
 *
 * Takes care of scheduled backups.
 */
function backup_migrate_cron() {

  // Backing up requires a full bootstrap as it uses the file functionality in
  // files.inc. Running poormanscron with caching on can cause cron to run without
  // a full bootstrap so we manually finish bootstrapping here.
  require_once './includes/common.inc';
  _drupal_bootstrap_full();
  if ($backup_schedule_period = variable_get("backup_migrate_schedule_backup_period", 0)) {
    $last_backup = variable_get("backup_migrate_schedule_last_backup", 0);
    $now = time();
    if ($last_backup < $now - $backup_schedule_period * 60 * 60) {

      // time to run the backup
      _backup_migrate_dump_tables(variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()), variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()), variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()), 'sql', "save", variable_get("backup_migrate_compression", "none"), "scheduled", variable_get("backup_migrate_timestamp_format", 'Y-m-d\\TH-i-s'));

      // Set the timestamp to indecate last backup time.
      variable_set("backup_migrate_schedule_last_backup", $now);

      // Delete older backups if needed.
      _backup_migrate_remove_expired_backups();
    }
  }

  // Delete temp files abandoned for 6 or more hours.
  $dir = file_directory_temp();
  $expire = time() - variable_get('backup_migrate_cleanup_time', 21600);
  if (file_exists($dir) && is_dir($dir) && is_readable($dir) && ($handle = opendir($dir))) {
    while (FALSE !== ($file = @readdir($handle))) {

      // Delete 'backup_migrate_' files in the temp directory that are older than the expire time.
      // We should only attempt to delete writable files to prevent errors in shared environments.
      // This could still cause issues in shared environments with poorly configured file permissions.
      if (strpos($file, 'backup_migrate_') === 0 && is_writable("{$dir}/{$file}") && @filectime("{$dir}/{$file}") < $expire) {
        unlink("{$dir}/{$file}");
      }
    }
    closedir($handle);
  }
}

/**
 * Implementation of hook_perm().
 */
function backup_migrate_perm() {
  return array(
    'perform backup',
    'access backup files',
    'delete backup files',
    'restore from backup',
  );
}

/**
 * Implementation of hook_simpletest().
 */
function backup_migrate_simpletest() {
  $dir = drupal_get_path('module', 'backup_migrate') . '/tests';
  $tests = file_scan_directory($dir, '\\.test$');
  return array_keys($tests);
}

/**
 * Menu callback. Delete a previous backup.
 */
function _backup_migrate_delete() {

  // Merge remainder of arguments from GET['q'], into relative file path.
  $args = func_get_args();
  $path = implode('/', $args);
  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    return drupal_get_form('backup_migrate_delete_confirm', $path);
  }
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * Ask confirmation for file deletion.
 */
function backup_migrate_delete_confirm($path) {
  $form['path'] = array(
    '#type' => 'value',
    '#value' => $path,
  );
  return confirm_form($form, t('Are you sure you want to delete the backup file at %path?', array(
    '%path' => $path,
  )), 'admin/content/backup_migrate/files', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
function backup_migrate_delete_confirm_submit($form_id, $form_values) {
  $path = $form_values['path'];
  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    file_delete($path);
  }
  watchdog('backup_migrate', t('Database backup file deleted: %file', array(
    '%file' => $path,
  )));
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * The schedule form.
 */
function backup_migrate_schedule() {
  $form = array();
  $form['backup_migrate_schedule_backup_period'] = array(
    "#type" => "textfield",
    "#title" => t("Backup every"),
    "#field_suffix" => t("Hour(s)"),
    "#description" => t("Use 0 for no scheduled backup. Cron must be configured to run for backups to work."),
    "#default_value" => variable_get("backup_migrate_schedule_backup_period", 0),
  );
  $form['backup_migrate_schedule_backup_keep'] = array(
    "#type" => "textfield",
    "#title" => t("Number of Backup files to keep"),
    "#description" => t("The number of backup files to keep before deleting old ones. Use 0 to never delete backups"),
    "#default_value" => variable_get("backup_migrate_schedule_backup_keep", 0),
  );
  if (!_backup_migrate_check_destination_dir('scheduled')) {
    $form['backup_migrate_schedule_backup_period']['#disabled'] = TRUE;
    $form['backup_migrate_schedule_backup_keep']['#disabled'] = TRUE;
  }
  return system_settings_form($form);
}

/**
 * The backup/export form.
 */
function backup_migrate_backup() {
  $form = array();
  $tables = _backup_migrate_get_table_names();
  $form['backup_migrate_exclude_tables'] = array(
    "#type" => "select",
    "#multiple" => TRUE,
    "#title" => t("Exclude the following tables altogether"),
    "#options" => $tables,
    "#default_value" => variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()),
    "#description" => t("The selected tables will not be added to the backup file."),
  );
  $form['backup_migrate_nodata_tables'] = array(
    "#type" => "select",
    "#multiple" => TRUE,
    "#title" => t("Exclude the data from the following tables"),
    "#options" => $tables,
    "#default_value" => variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()),
    "#description" => t("The selected tables will have their structure backed up but not their contents. This is useful for excluding cache data to reduce file size."),
  );
  $form['backup_migrate_file_name'] = array(
    "#type" => "textfield",
    "#title" => t("Backup file name"),
    "#default_value" => variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()),
  );
  if (module_exists('token')) {
    $form['token_help'] = array(
      '#title' => t('Replacement patterns'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#description' => t('Prefer raw-text replacements for text to avoid problems with HTML entities!'),
    );
    $form['token_help']['help'] = array(
      '#value' => theme('token_help', ''),
    );
  }
  $compression_options = array(
    "none" => t("No Compression"),
  );
  if (@function_exists("gzencode")) {
    $compression_options['gzip'] = t("GZip");
  }
  if (@function_exists("bzcompress")) {
    $compression_options['bzip'] = t("BZip");
  }
  if (class_exists('ZipArchive')) {
    $compression_options['zip'] = t("Zip");
  }
  $form['backup_migrate_compression'] = array(
    "#type" => "radios",
    "#title" => t("Compression"),
    "#options" => $compression_options,
    "#default_value" => variable_get("backup_migrate_compression", "none"),
  );
  $destination_options = array(
    "download" => t("Download"),
  );
  if (_backup_migrate_check_destination_dir('manual')) {
    $destination_options['save'] = t("Save to Files Directory");
  }
  $form['backup_migrate_destination'] = array(
    "#type" => "radios",
    "#title" => t("Destination"),
    "#options" => $destination_options,
    "#default_value" => variable_get("backup_migrate_destination", "download"),
  );
  $form['backup_migrate_append_timestamp'] = array(
    "#type" => "checkbox",
    "#title" => t("Append a timestamp."),
    "#default_value" => variable_get("backup_migrate_append_timestamp", 1),
  );
  $form['backup_migrate_timestamp_format'] = array(
    "#type" => "textfield",
    "#title" => t("Timestamp format"),
    "#default_value" => variable_get("backup_migrate_timestamp_format", 'Y-m-d\\TH-i-s'),
    "#description" => t('Should be a PHP <a href="!url">date()</a> format string.', array(
      '!url' => 'http://www.php.net/date',
    )),
  );
  $form['backup_migrate_save_settings'] = array(
    "#type" => "checkbox",
    "#title" => t("Save these settings."),
    "#default_value" => 1,
  );
  $form[] = array(
    '#type' => 'submit',
    '#value' => t('Backup Database'),
  );
  return $form;
}

/**
 * Submit the form. Save the values as defaults if desired and output the backup file.
 */
function backup_migrate_backup_submit($form_id, $form_values) {
  if ($form_values['backup_migrate_save_settings']) {
    variable_set("backup_migrate_exclude_tables", $form_values['backup_migrate_exclude_tables']);
    variable_set("backup_migrate_nodata_tables", $form_values['backup_migrate_nodata_tables']);
    variable_set("backup_migrate_file_name", $form_values['backup_migrate_file_name']);
    variable_set("backup_migrate_destination", $form_values['backup_migrate_destination']);
    variable_set("backup_migrate_compression", $form_values['backup_migrate_compression']);
    variable_set("backup_migrate_append_timestamp", $form_values['backup_migrate_append_timestamp']);
    variable_set("backup_migrate_timestamp_format", $form_values['backup_migrate_timestamp_format']);
  }
  _backup_migrate_dump_tables($form_values['backup_migrate_file_name'], $form_values['backup_migrate_exclude_tables'], $form_values['backup_migrate_nodata_tables'], 'sql', $form_values['backup_migrate_destination'], $form_values['backup_migrate_compression'], "manual", $form_values['backup_migrate_append_timestamp'] ? $form_values['backup_migrate_timestamp_format'] : false);
  return "admin/content/backup_migrate";
}

/**
 * Restore a backup file (from the server).
 */
function _backup_migrate_restore_from_server() {

  // Merge remainder of arguments from GET['q'], into relative file path.
  $args = func_get_args();
  $path = implode('/', $args);
  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    return drupal_get_form('backup_migrate_restore_confirm', $path);
  }
  drupal_goto(user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate");
}

/**
 * Ask confirmation for file restore.
 */
function backup_migrate_restore_confirm($path) {
  $form['path'] = array(
    '#type' => 'value',
    '#value' => $path,
  );
  return confirm_form($form, t('Are you sure you want to restore the database from the backup at %path?', array(
    '%path' => $path,
  )), 'admin/content/backup_migrate/files', t('This will delete some or all of your data and cannot be undone. <strong>Always test your backups on a non-production server!</strong>'), t('Restore'), t('Cancel'));
}
function backup_migrate_restore_confirm_submit($form_id, $form_values) {
  $path = $form_values['path'];
  if ($path && _backup_migrate_path_is_in_save_dir($path)) {
    _backup_migrate_restore_file($path);
    watchdog('backup_migrate', t('Database restored from %file', array(
      '%file' => $path,
    )));
  }
  return user_access('access backup files') ? "admin/content/backup_migrate/files" : "admin/content/backup_migrate";
}

/**
 * The restore/import upload form.
 */
function backup_migrate_restore() {
  $form = array();
  $form['backup_migrate_restore_upload'] = array(
    '#title' => t('Upload a Backup File'),
    '#type' => 'file',
    '#description' => t("Upload a backup file created by this version of this module. For other database backups please use another tool for import. Max file size: %size", array(
      "%size" => format_size(file_upload_max_size()),
    )),
  );
  $form[] = array(
    '#type' => 'markup',
    '#value' => t('<p>This will delete some or all of your data and cannot be undone. If there is a sessions table in the backup file, you and all other currently logged in users will be logged out. <strong>Always test your backups on a non-production server!</strong></p>'),
  );
  $form[] = array(
    '#type' => 'submit',
    '#value' => t('Restore Database'),
  );
  if (user_access('access backup files')) {
    $form[] = array(
      '#type' => 'markup',
      '#value' => t('<p>Or you can restore one of the files in the <a href="!url">saved backup directory.</a></p>', array(
        "!url" => url("admin/content/backup_migrate/files"),
      )),
    );
  }
  $form['#attributes'] = array(
    'enctype' => 'multipart/form-data',
  );
  return $form;
}

/**
 * The restore submit. Do the restore.
 */
function backup_migrate_restore_submit($form_id, $form_values) {
  if ($file = file_check_upload('backup_migrate_restore_upload')) {
    _backup_migrate_restore_file($file->filepath, $file->filename, true);
    watchdog('backup_migrate', t('Database restored from upload %file', array(
      '%file' => $file->filename,
    )));
  }
  return 'admin/content/backup_migrate/restore';
}

/**
 * Action to backup the drupal site. Requires actions.module.
 */
function action_backup_migrate_backup($op, $edit = array()) {
  switch ($op) {
    case 'do':
      _backup_migrate_backup_with_defaults();
      watchdog('action', t('Backed up database'));
      break;
    case 'metadata':
      return array(
        'description' => t('Backup the database with the default settings'),
        'type' => t('Backup and Migrate'),
        'batchable' => TRUE,
        'configurable' => FALSE,
      );

    // Return an HTML config form for the action.
    case 'form':
      return '';

    // Validate the HTML form.
    case 'validate':
      return TRUE;

    // Process the HTML form to store configuration.
    case 'submit':
      return '';
  }
}

/*
 * Implementation of hook_action_info().
 */
function backup_migrate_action_info() {
  return array(
    'backup_migrate_action_backup' => array(
      '#label' => t('Backup the database'),
      '#module' => t('Backup and Migrate'),
      '#description' => t('Backup the database with the default settings.'),
    ),
  );
}

/*
 * Action callback.
 */
function backup_migrate_action_backup() {
  _backup_migrate_backup_with_defaults();
}

/**
 * Backup the database with the default settings.
 */
function _backup_migrate_backup_with_defaults($mode = "manual") {
  _backup_migrate_dump_tables(variable_get("backup_migrate_file_name", _backup_migrate_default_file_name()), variable_get("backup_migrate_exclude_tables", _backup_migrate_default_exclude_tables()), variable_get("backup_migrate_nodata_tables", _backup_migrate_default_structure_only_tables()), 'sql', 'save', variable_get("backup_migrate_compression", "none"), $mode, variable_get("backup_migrate_append_timestamp", 1) ? variable_get("backup_migrate_timestamp_format", 'Y-m-d\\TH-i-s') : FALSE);
}

/**
 * Build the database dump file. Takes a list of tables to exclude and some formatting options.
 */
function _backup_migrate_dump_tables($filename, $exclude_tables, $nodata_tables, $type = "sql", $destination = "download", $compression = "none", $mode = "manual", $append_timestamp = FALSE) {
  $filemime = "text/plain";
  $success = FALSE;
  if ($append_timestamp) {
    $filename .= "-" . date($append_timestamp);
  }
  $filename = _backup_migrate_clean_filename($filename);

  // Dump the database.
  $temp_file = _backup_migrate_temp_file();
  switch ($type) {
    case "sql":
      $success = _backup_migrate_get_dump_sql($temp_file, $exclude_tables, $nodata_tables);
      $filename .= ".sql";
      $filemime = 'text/x-sql';
      break;
  }

  // Compress the results.
  if ($success) {
    switch ($compression) {
      case "gzip":
        $temp_gz = _backup_migrate_temp_file('gz');
        if ($success = _backup_migrate_gzip_encode($temp_file, $temp_gz, 9)) {
          $temp_file = $temp_gz;
          $filename .= ".gz";
          $filemime = 'application/x-gzip';
        }
        break;
      case "bzip":
        $temp_bz = _backup_migrate_temp_file('bz');
        if ($success = _backup_migrate_bzip_encode($temp_file, $temp_bz)) {
          $temp_file = $temp_bz;
          $filename .= ".bz";
          $filemime = 'application/x-bzip';
        }
        break;
      case "zip":
        $temp_zip = _backup_migrate_temp_file('zip');
        if ($success = _backup_migrate_zip_encode($temp_file, $temp_zip, $filename)) {
          $temp_file = $temp_zip;
          $filename .= ".zip";
          $filemime = 'application/zip';
        }
        break;
    }
  }

  // Save or download the results.
  if ($success) {
    switch ($destination) {
      case "save":
        _backup_migrate_save_to_disk($temp_file, $filename, $mode);
        break;
      case "download":
        _backup_migrate_send_file_to_download($filename, $filemime, $temp_file);
        break;
    }
  }

  // Delete any temporary files we've created.
  _backup_migrate_temp_file("", TRUE);
}

/**
 *  Force a browser download for the file.
 */
function _backup_migrate_send_file_to_download($filename, $filetype, $file_path) {
  header('Content-Type: ' . $filetype);
  header('Expires: ' . gmdate('D, d M Y H:i:s') . ' GMT');
  header('Content-Length: ' . filesize($file_path));
  header('Content-Disposition: attachment; filename="' . $filename . '"');

  // Transfer file in 1024 byte chunks to save memory usage.
  if ($fd = fopen($file_path, 'rb')) {
    while (!feof($fd)) {
      print fread($fd, 1024);
    }
    fclose($fd);
  }

  // Delete any temporary files we've created.
  _backup_migrate_temp_file("", TRUE);
  watchdog('backup_migrate', 'Database backup downloaded');
  module_invoke_all('exit');
  exit;
}

/**
 * Get the sql dump file. Returns a list of sql commands, one command per line.
 *  That makes it easier to import without loading the whole file into memory.
 *  The files are a little harder to read, but human-readability is not a priority
 */
function _backup_migrate_get_dump_sql($file, $exclude_tables, $nodata_tables) {
  if ($dst = fopen($file, "w")) {
    fwrite($dst, _backup_migrate_get_sql_file_header());
    $alltables = _backup_migrate_get_tables();
    foreach ($alltables as $table) {
      if ($table['Name'] && !isset($exclude_tables[$table['Name']])) {
        fwrite($dst, _backup_migrate_get_table_structure_sql($table));
        if (!in_array($table['Name'], $nodata_tables)) {
          _backup_migrate_dump_table_data_sql_to_handle($dst, $table);
        }
      }
    }
    fwrite($dst, _backup_migrate_get_sql_file_footer());
    fclose($dst);
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Get the sql for the structure of the given table.
 */
function _backup_migrate_get_table_structure_sql($table) {
  $out = "";
  $result = db_query("SHOW CREATE TABLE `" . $table['Name'] . "`");
  if ($create = db_fetch_array($result)) {
    $out .= "DROP TABLE IF EXISTS `" . $table['Name'] . "`;\n";
    $out .= strtr($create['Create Table'], "\n", " ");
    if ($table['Auto_increment']) {
      $out .= " AUTO_INCREMENT=" . $table['Auto_increment'];
    }
    $out .= ";\n";
  }
  return $out;
}

/**
 *  Get the sql to insert the data for a given table
 */
function _backup_migrate_dump_table_data_sql_to_handle($dst, $table) {
  $data = db_query("SELECT * FROM `" . $table['Name'] . "`");
  while ($row = db_fetch_array($data)) {
    $items = array();
    foreach ($row as $key => $value) {
      $items[] = is_null($value) ? "null" : "'" . db_escape_string($value) . "'";
    }
    if ($items) {
      fwrite($dst, "INSERT INTO `" . $table['Name'] . "` VALUES (" . implode(",", $items) . ");\n");
    }
  }
}

/**
 * The header for the top of the sql dump file. These commands set the connection
 *  character encoding to help prevent encoding conversion issues.
 */
function _backup_migrate_get_sql_file_header() {
  return "\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;\n\nSET NAMES utf8;\n";
}

/**
 * The footer of the sql dump file.
 */
function _backup_migrate_get_sql_file_footer() {
  return "\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n";
}

/**
 * Get a list of tables in the db. Works with MySQL, Postgres not tested.
 */
function _backup_migrate_get_tables() {
  $out = "";

  // get auto_increment values and names of all tables
  $tables = db_query("show table status");
  while ($table = db_fetch_array($tables)) {
    $out[$table['Name']] = $table;
  }
  return $out;
}

/**
 * Get the list of table names.
 */
function _backup_migrate_get_table_names() {
  $out = "";

  // Get auto_increment values and names of all tables.
  $tables = db_query("show table status");
  while ($table = db_fetch_array($tables)) {
    $out[$table['Name']] = $table['Name'];
  }
  return $out;
}

/**
 * Restore from a previously backed up files. Accepts any file created by the backup function.
 */
function _backup_migrate_restore_file($filepath, $filename = "", $delete = FALSE) {
  if (!$filename) {
    $filename = $filepath;
  }
  $file_is_temp = $delete;
  $open_func = "fopen";
  $read_func = "fgets";
  $close_func = "fclose";

  // figure out if the file is compressed by the file extention
  if (drupal_substr($filename, -4, 4) == ".sql") {

    // No compression.
  }
  if (drupal_substr($filename, -3, 3) == ".gz") {
    if (function_exists("gzopen")) {
      $open_func = "gzopen";
      $read_func = "gzgets";
      $close_func = "gzclose";
    }
    else {

      // GZip compression... not supported.
      drupal_set_message(t("This version of PHP does not support gzip comressed files. Please try using an uncompressed sql backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }

  // BZip compression.
  if (drupal_substr($filename, -3, 3) == ".bz") {
    if (function_exists("bzopen")) {
      $open_func = "fopen";
      $read_func = "fgets";

      // Decompress the file to a temp file.
      $tmp = tempnam(file_directory_temp(), 'tmp_');
      if (($dst = fopen($tmp, "w")) && ($src = bzopen($filepath, "r"))) {
        while ($data = bzread($src)) {
          fwrite($dst, $data);
        }
        fclose($dst);
        bzclose($src);
        if ($delete) {
          unlink($filepath);
        }
        $filepath = $tmp;
        $delete = TRUE;
      }
      else {
        drupal_set_message(t("Unable to decompress bzip file. Please try using an uncompressed backup."), 'error');
        drupal_goto("admin/content/backup_migrate/restore");
      }
    }
    else {

      // BZip compression... not supported.
      drupal_set_message(t("This version of PHP does not support bzip compressed files. Please try using an uncompressed backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }

  // Zip compression.
  if (drupal_substr($filename, -4, 4) == ".zip") {
    if (class_exists('ZipArchive')) {
      if ($filepath != $filename) {
        rename($filepath, $filepath . ".zip");
        $filepath .= ".zip";
      }
      $tmp = tempnam(file_directory_temp(), 'tmp_');
      $zip = new ZipArchive();
      if (($dst = fopen($tmp, "w")) && ($src = $zip
        ->open($filepath))) {
        if ($data = $zip
          ->getFromIndex(0)) {
          fwrite($dst, $data);
        }
        fclose($dst);
        $zip
          ->close();
        if ($delete) {
          unlink($filepath);
        }
        $filepath = $tmp;
        $delete = TRUE;
      }
      else {
        drupal_set_message(t("Unable to decompress zip file. Please try using an uncompressed backup."), 'error');
        drupal_goto("admin/content/backup_migrate/restore");
      }
    }
    else {

      // Zip compression... not supported.
      drupal_set_message(t("This version of PHP does not support zip comressed files. Please try using an uncompressed backup."), 'error');
      drupal_goto("admin/content/backup_migrate/restore");
    }
  }

  // Open the file (with fopen or gzopen depending on file format).
  if ($handle = @$open_func($filepath, "r")) {
    $num = 0;

    // Read one line at a time and run the query.
    while ($line = $read_func($handle)) {
      $line = trim($line);
      if ($line) {

        // Use the helper instead of the api function to avoid substitution of '{' etc.
        _db_query($line);
        $num++;
      }
    }

    // Close the file with fclose/gzclose.
    $close_func($handle);

    // Delete the file if it is temporary.
    if ($delete) {
      unlink($filepath);
    }
    $message = t("Restore complete. %num SQL commands executed.", array(
      "%num" => $num,
    ));
    $message .= $file_is_temp ? "" : "(" . l(t("Restore Again..."), "admin/content/backup_migrate/restorefile/" . $filepath) . ")";
    drupal_set_message($message);
  }
  else {
    drupal_set_message(t("Unable to open file %file to restore database", array(
      "%file" => $filepath,
    )), 'error');
  }

  // Release cron semaphore that was set during scheduled backup.
  variable_del('cron_semaphore');

  // Delete any temp files we've created.
  _backup_migrate_temp_file("", TRUE);
}

/*
 Backup File Management
*/

/**
 * Return a list of backup filetypes.
 */
function _backup_migrate_filetypes() {
  return array(
    "sql" => array(
      "extension" => ".sql",
      "filemime" => "text/x-sql",
    ),
    "gzip" => array(
      "extension" => ".gz",
      "filemime" => "application/x-gzip",
    ),
    "bzip" => array(
      "extension" => ".bz",
      "filemime" => "application/x-bzip",
    ),
    "zip" => array(
      "extension" => ".zip",
      "filemime" => "application/zip",
    ),
  );
}

/**
 * Get the basic info for a backup file on the server.
 */
function _backup_migrate_file_info($path) {
  $types = _backup_migrate_filetypes();
  foreach ($types as $type) {
    $extlen = drupal_strlen($type['extension']);
    if (drupal_substr($path, -$extlen, $extlen) === $type['extension']) {
      $out = $type;
      $out['filesize'] = filesize($path);
      $out['filename'] = basename($path);
      $out['filemtime'] = filemtime($path);
      $out['filectime'] = filectime($path);
      $out['filepath'] = $path;
      return $out;
    }
  }
  return NULL;
}

/**
 * Save the backup file to the appropriete folder on the server.
 */
function _backup_migrate_save_to_disk($temp_file, $filename, $mode = "manual") {
  if ($dir = _backup_migrate_check_destination_dir($mode)) {
    $filepath = $dir . "/" . $filename;
    rename($temp_file, $filepath);
    $message = t('Database backup saved to %file. ', array(
      '%file' => $filepath,
    ));
    watchdog('backup_migrate', $message);
    if ($mode == "manual" && user_access('perform backup')) {
      $message .= user_access("access backup files") ? "(" . l(t("Download"), "system/files/" . $filepath) . ") " : "";
      $message .= user_access("delete backup files") ? "(" . l(t("Delete..."), "admin/content/backup_migrate/delete/" . $filepath) . ") " : "";
      $message .= user_access("restore from backup") ? "(" . l(t("Restore..."), "admin/content/backup_migrate/restorefile/" . $filepath) . ")" : "";
      drupal_set_message($message);
    }
  }
}

/**
 * Implementation of hook_file_download.()
 *
 * Allow users with the appropriate permissions to download backup files.
 */
function backup_migrate_file_download($path) {
  if (_backup_migrate_path_is_in_save_dir($path)) {
    if (user_access('access backup files') && ($info = _backup_migrate_file_info($path))) {
      return array(
        'Content-Type: ' . $info['filemime'],
        'Content-Length: ' . $info['filesize'],
        'Content-Disposition: attachment; filename="' . $info['filename'] . '"',
      );
    }
    else {
      return -1;
    }
  }
  return NULL;
}

/**
 * Get a temp file. Store it in the normal save path for slightly better security
 *   in shared environments.
 */
function _backup_migrate_temp_file($extension = "", $delete_all = FALSE) {
  static $files = array();
  if ($delete_all) {
    _backup_migrate_temp_files_delete($files);
  }
  else {
    $file = tempnam(file_directory_temp(), 'backup_migrate_');
    if (!empty($extension)) {
      unlink($file);
      $file .= '.' . $extension;
    }
    $files[] = $file;
    return $file;
  }
}

/**
 * Delete all temporary files.
 */
function _backup_migrate_temp_files_delete($files) {
  foreach ($files as $file) {
    if (file_exists($file)) {
      @unlink($file);
    }
  }
}

/**
 * List the previously created backup files.
 */
function _backup_migrate_list_files($mode = "manual") {
  $files = array();
  if ($dir = _backup_migrate_check_destination_dir($mode)) {
    if ($handle = opendir($dir)) {
      while (FALSE !== ($file = readdir($handle))) {
        $filepath = $dir . "/" . $file;
        if ($info = _backup_migrate_file_info($filepath)) {
          $files[$file] = array(
            $file,
            format_date($info['filemtime']),
            format_size($info['filesize']),
            l(t("Download"), "system/files/" . $filepath),
            user_access('restore from backup') ? l(t("Restore"), "admin/content/backup_migrate/restorefile/" . $filepath) : '',
            user_access('delete backup files') ? l(t("Delete"), "admin/content/backup_migrate/delete/" . $filepath) : '',
          );
        }
      }
    }
  }
  krsort($files);
  return theme("table", array(), $files);
}

/**
 * Remove older backups keeping only the number specified by the aministrator.
 */
function _backup_migrate_remove_expired_backups() {
  $num_to_keep = variable_get("backup_migrate_schedule_backup_keep", 0);

  // If num to keep is not 0 (0 is infinity).
  if ($num_to_keep && ($dir = _backup_migrate_check_destination_dir("scheduled"))) {

    // Create a list of backup files indexed by time (with an aditional index to
    // prevent two files with the same create time from overwriting each other).
    $res = opendir($dir);
    $files = array();
    $i = 0;
    if ($res) {

      // Read all files and sort them by modified time.
      while ($file = readdir($res)) {
        $filepath = $dir . "/" . $file;
        if ($info = _backup_migrate_file_info($filepath)) {
          $files[str_pad($info['filemtime'], 10, "0", STR_PAD_LEFT) . "-" . $i++] = $filepath;
        }
      }
    }

    // If we are beyond our limit, remove as many as we need.
    $num_files = count($files);
    if ($num_files > $num_to_keep) {
      $num_to_delete = $num_files - $num_to_keep;

      // Sort by date.
      ksort($files);

      // Delete from the start of the list (earliest).
      for ($i = 0; $i < $num_to_delete; $i++) {
        $filepath = array_shift($files);
        file_delete($filepath);
      }
    }
  }
}

/**
 * Return the path on the server to save the dump files.
 */
function _backup_migrate_path_is_in_save_dir($path, $mode = "") {
  $backup_dir = _backup_migrate_get_save_path($mode);
  return file_exists($backup_dir) && file_check_location($path, $backup_dir);
}

/**
 * Return the path on the server to save the dump files.
 */
function _backup_migrate_get_save_path($mode = "") {
  $dir = file_directory_path() . "/backup_migrate";
  if ($mode) {
    $dir .= $mode == "manual" ? "/manual" : "/scheduled";
  }
  return $dir;
}

/**
 * Prepare the destination directory for the backups.
 */
function _backup_migrate_check_destination_dir($mode = "") {
  $directory = _backup_migrate_get_save_path();
  $out = $subdir = rtrim($directory . "/" . $mode, "/");

  // Check for the main directory.
  if (!file_check_directory($directory, TRUE)) {

    // unable to create main directory
    $message = t("Unable to create or write to the save directory '%directory'. Please check the file permissions on your files directory.", array(
      '%directory' => $directory,
    ));
    drupal_set_message($message, "error");
    return FALSE;
  }

  // Create subdir if needed.
  if ($subdir != $directory && !file_check_directory($subdir, TRUE)) {

    // Unable to create sub directory.
    $message = t("Unable to create or write to the save directory '%directory'. Please check the file permissions on your files directory.", array(
      '%directory' => $subdir,
    ));
    drupal_set_message($message, "error");
    return FALSE;
  }

  // If the files are saved inside the webroot (ie: may be web-accesible).
  if (strtolower(substr(realpath($directory), 0, strlen($_SERVER['DOCUMENT_ROOT']))) === strtolower($_SERVER['DOCUMENT_ROOT'])) {

    // Check for a htaccess file which adequately protects the backup files.
    $htaccess_lines = "order allow,deny\ndeny from all\n";
    if (!is_file($directory . '/.htaccess') || strpos(file_get_contents($directory . '/.htaccess'), $htaccess_lines) === FALSE) {

      // Attempt to protect the backup files from public access using htaccess.
      if (($fp = @fopen($directory . '/.htaccess', 'w')) && @fputs($fp, $htaccess_lines)) {
        fclose($fp);
        chmod($directory . '/.htaccess', 0664);
      }
      else {
        $message = "Security warning: Couldn't modify .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code> or add them to the existing .htaccess file";
        $replace = array(
          '%directory' => $directory,
          '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)),
        );
        drupal_set_message(t($message, $replace), "error");
        watchdog('security', t($message, $replace), WATCHDOG_ERROR);
        return FALSE;
      }
    }

    // Check the user agent to make sure we're not responding to a request from drupal itself.
    // That should prevent infinite loops which could be caused by poormanscron in some circumstances.
    if (strpos($_SERVER['HTTP_USER_AGENT'], 'Drupal') !== FALSE) {
      return FALSE;
    }

    // Check to see if the destination is publicly accessible
    $test_contents = "this file should not be publicly accesible";

    // Create the the text.txt file if it's not already there.
    if (!is_file($subdir . '/test.txt') || file_get_contents($subdir . '/test.txt') != $test_contents) {
      if ($fp = fopen($subdir . '/test.txt', 'w')) {
        @fputs($fp, $test_contents);
        fclose($fp);
      }
      else {
        $message = t("Security notice: Backup and Migrate was unable to write a test text file to the destination directory %directory, and is therefore unable to check the security of the backup destination. Backups to the server will be disabled until the destination becomes writable and secure.", array(
          '%directory' => $directory,
        ));
        drupal_set_message($message, "error");
        return FALSE;
      }
    }

    // Attempt to read the test file via http. This may fail for other reasons, so it's not a bullet-proof check.
    $path = trim(substr($subdir . '/test.txt', strlen(file_directory_path())), '\\/');
    if (_backup_migrate_test_file_readable_remotely($path, $test_contents)) {
      $message = t("Security notice: Backup and Migrate will not save backup files to the server because the destination directory is publicly accessible. If you want to save files to the server, please secure the '%directory' directory", array(
        '%directory' => $directory,
      ));
      drupal_set_message($message, "error");
      return FALSE;
    }
  }
  return $out;
}

/**
 * Check if a file can be read remotely via http.
 */
function _backup_migrate_test_file_readable_remotely($path, $contents) {
  $url = $GLOBALS['base_url'] . '/' . file_directory_path() . '/' . str_replace('\\', '/', $path);
  $result = drupal_http_request($url);
  if (strpos($result->data, $contents) !== FALSE) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Gzip encode a file.
 */
function _backup_migrate_gzip_encode($source, $dest, $level = 9) {
  $success = FALSE;
  if (@function_exists("gzopen")) {
    if (($fp_out = gzopen($dest, 'wb' . $level)) && ($fp_in = fopen($source, 'rb'))) {
      while (!feof($fp_in)) {
        gzwrite($fp_out, fread($fp_in, 1024 * 512));
      }
      $success = TRUE;
    }
    @fclose($fp_in);
    @gzclose($fp_out);
  }
  return $success;
}

/**
 * Bzip encode a file.
 */
function _backup_migrate_bzip_encode($source, $dest) {
  $success = FALSE;
  if (@function_exists("bzopen")) {
    if (($fp_out = bzopen($dest, 'w')) && ($fp_in = fopen($source, 'rb'))) {
      while (!feof($fp_in)) {
        bzwrite($fp_out, fread($fp_in, 1024 * 512));
      }
      $success = TRUE;
    }
    else {
      $error = TRUE;
    }
    @fclose($fp_in);
    @bzclose($fp_out);
  }
  return $success;
}

/**
 * Zip encode a file.
 */
function _backup_migrate_zip_encode($source, $dest, $filename) {
  $success = FALSE;
  if (class_exists('ZipArchive')) {
    $zip = new ZipArchive();
    $res = $zip
      ->open($dest, constant("ZipArchive::CREATE"));
    if ($res === TRUE) {
      $zip
        ->addFile($source, $filename);
      $success = $zip
        ->close();
    }
  }
  return $success;
}

/*
Defaults
*/

/**
 * Construct a default filename using the site's name.
 */
function _backup_migrate_default_file_name() {
  if (module_exists('token')) {
    return '[site-name]';
  }
  else {
    return _backup_migrate_clean_filename(variable_get('site_name', "backup_migrate"));
  }
}

/**
 * Construct a default filename using token and some cleaning.
 */
function _backup_migrate_clean_filename($filename) {
  if (module_exists('token') && function_exists('token_replace')) {
    $filename = token_replace($filename, 'global');
  }
  $filename = preg_replace("/[^a-zA-Z0-9\\.\\-_]/", "", $filename);
  if (strlen($filename) > 50) {
    $filename = drupal_substr($filename, 0, 50);
  }
  if (strlen($filename) == 0) {
    $filename = 'untitled';
  }
  return $filename;
}

/**
 * Tables to ingore altogether. None by default.
 */
function _backup_migrate_default_exclude_tables() {
  return array();
}

/**
 * Return the default tables whose data can be ignored. These tables mostly contain
 *  info which can be easily reproducted (such as cache or search index)
 *  but also tables which can become quite bloated but are not necessarily extremely
 *  important to back up or migrate during development (such ass access log and watchdog)
 */
function _backup_migrate_default_structure_only_tables() {
  $core = array(
    'cache',
    'cache_filter',
    'cache_calendar_ical',
    'cache_menu',
    'cache_page',
    'cache_views',
    'sessions',
    'search_dataset',
    'search_index',
    'search_keywords_log',
    'search_total',
    'watchdog',
    'accesslog',
    'devel_queries',
    'devel_times',
  );
  $alltables = array_merge($core, module_invoke_all('devel_caches'));
  global $db_prefix;
  foreach ($alltables as $table) {
    $prefixed_tables[] = $db_prefix . $table;
  }
  return $prefixed_tables;
}

Functions

Namesort descending Description
action_backup_migrate_backup Action to backup the drupal site. Requires actions.module.
backup_migrate_action_backup
backup_migrate_action_info
backup_migrate_backup The backup/export form.
backup_migrate_backup_submit Submit the form. Save the values as defaults if desired and output the backup file.
backup_migrate_cron Implementation of hook_cron(),
backup_migrate_delete_confirm Ask confirmation for file deletion.
backup_migrate_delete_confirm_submit
backup_migrate_file_download Implementation of hook_file_download.()
backup_migrate_menu Implementation of hook_menu().
backup_migrate_perm Implementation of hook_perm().
backup_migrate_restore The restore/import upload form.
backup_migrate_restore_confirm Ask confirmation for file restore.
backup_migrate_restore_confirm_submit
backup_migrate_restore_submit The restore submit. Do the restore.
backup_migrate_schedule The schedule form.
backup_migrate_simpletest Implementation of hook_simpletest().
_backup_migrate_backup_with_defaults Backup the database with the default settings.
_backup_migrate_bzip_encode Bzip encode a file.
_backup_migrate_check_destination_dir Prepare the destination directory for the backups.
_backup_migrate_clean_filename Construct a default filename using token and some cleaning.
_backup_migrate_default_exclude_tables Tables to ingore altogether. None by default.
_backup_migrate_default_file_name Construct a default filename using the site's name.
_backup_migrate_default_structure_only_tables Return the default tables whose data can be ignored. These tables mostly contain info which can be easily reproducted (such as cache or search index) but also tables which can become quite bloated but are not necessarily extremely important to back up…
_backup_migrate_delete Menu callback. Delete a previous backup.
_backup_migrate_dump_tables Build the database dump file. Takes a list of tables to exclude and some formatting options.
_backup_migrate_dump_table_data_sql_to_handle Get the sql to insert the data for a given table
_backup_migrate_filetypes Return a list of backup filetypes.
_backup_migrate_file_info Get the basic info for a backup file on the server.
_backup_migrate_get_dump_sql Get the sql dump file. Returns a list of sql commands, one command per line. That makes it easier to import without loading the whole file into memory. The files are a little harder to read, but human-readability is not a priority
_backup_migrate_get_save_path Return the path on the server to save the dump files.
_backup_migrate_get_sql_file_footer The footer of the sql dump file.
_backup_migrate_get_sql_file_header The header for the top of the sql dump file. These commands set the connection character encoding to help prevent encoding conversion issues.
_backup_migrate_get_tables Get a list of tables in the db. Works with MySQL, Postgres not tested.
_backup_migrate_get_table_names Get the list of table names.
_backup_migrate_get_table_structure_sql Get the sql for the structure of the given table.
_backup_migrate_gzip_encode Gzip encode a file.
_backup_migrate_list_files List the previously created backup files.
_backup_migrate_path_is_in_save_dir Return the path on the server to save the dump files.
_backup_migrate_remove_expired_backups Remove older backups keeping only the number specified by the aministrator.
_backup_migrate_restore_file Restore from a previously backed up files. Accepts any file created by the backup function.
_backup_migrate_restore_from_server Restore a backup file (from the server).
_backup_migrate_save_to_disk Save the backup file to the appropriete folder on the server.
_backup_migrate_send_file_to_download Force a browser download for the file.
_backup_migrate_temp_file Get a temp file. Store it in the normal save path for slightly better security in shared environments.
_backup_migrate_temp_files_delete Delete all temporary files.
_backup_migrate_test_file_readable_remotely Check if a file can be read remotely via http.
_backup_migrate_zip_encode Zip encode a file.