You are here

hosting_site_backup_manager.module in Hosting Site Backup Manager 6

Hosting site backup manager module.

Adds a backup tab to the site node.

File

hosting_site_backup_manager.module
View source
<?php

/**
 * @file
 * Hosting site backup manager module.
 *
 * Adds a backup tab to the site node.
 */

/**
 * Implements of hook_menu().
 *
 * @return array
 *   An array of menu items.
 */
function hosting_site_backup_manager_menu() {
  $items = array();
  $items['node/%hosting_site_node/backups'] = array(
    'title' => 'Backups',
    'description' => 'List of backups of this website',
    'page callback' => 'hosting_site_backup_manager_page',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['node/%hosting_site_node/ajax/backups'] = array(
    'title' => 'Backup list',
    'description' => 'AJAX callback for refreshing backups list',
    'page callback' => 'hosting_site_backup_manager_ajax_list',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['node/%hosting_site_node/backup/download/%'] = array(
    'title' => 'Download backup',
    'description' => 'Download the selected backup',
    'page callback' => 'hosting_site_backup_manager_download',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['node/%hosting_site_node/backup/delete/%'] = array(
    'title' => 'Delete backup',
    'description' => 'Delete the selected backup',
    'page callback' => 'hosting_site_backup_manager_delete',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['node/%hosting_site_node/backup/restore/%'] = array(
    'title' => 'Restore backup',
    'description' => 'Restore the selected backup',
    'page callback' => 'hosting_site_backup_manager_restore',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['node/%hosting_site_node/backup/export/%'] = array(
    'title' => 'Export backup',
    'description' => 'Export the selected backup',
    'page callback' => 'hosting_site_backup_manager_export',
    'page arguments' => array(
      1,
      4,
    ),
    'access callback' => 'node_access',
    'access arguments' => array(
      'view',
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Placeholder function for additional delete checks.
 *
 * @param node $site
 *   The site node object.
 * @param int $bid
 *   The backup id.
 *
 * @return string
 *   A confirmation form or a error string.
 */
function hosting_site_backup_manager_delete($site, $bid) {

  // Get the filename.
  $source = db_fetch_object(db_query("SELECT filename FROM {hosting_site_backups} WHERE site=%d AND bid=%d ORDER BY timestamp DESC", $site->nid, $bid));
  if ($source) {

    // Return a confirmation form.
    $output = drupal_get_form('hosting_site_backup_manager_confirm_delete', $source->filename, $site->nid, $bid);
  }
  else {
    $output = t('A valid backup could not be found.');
  }
  return $output;
}

/**
 * Function that renders a confirmation form for the selected deletetion.
 *
 * @param arrays $form_state
 *   The form state array. Changes made to this variable will have no effect.
 * @param string $filename
 *   The backup filename to delete.
 * @param int $sitenid
 *   The selected site node id.
 * @param int $bid
 *   The backup id.
 *
 * @return string
 *   A confirmation form.
 */
function hosting_site_backup_manager_confirm_delete($form_state, $filename, $sitenid, $bid) {

  // Add the hosting_task javascript,
  // so we can use the confirm form functionality.
  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');

  // Get the backup data.
  $source = db_fetch_object(db_query("SELECT * FROM {hosting_site_backups} WHERE site=%d AND bid=%d", $sitenid, $bid));

  // Build the form.
  $form = array();
  $form['#filename'] = $filename;
  $form['#sitenid'] = $sitenid;
  $form['#bid'] = $bid;

  // Not the best formatted code, but suffices for now.
  $form['explanation'] = array(
    '#type' => 'markup',
    '#value' => '<h2>' . t('Backup information') . '</h2>' . t('Backup description:<br /> %description', array(
      '%description' => filter_xss($source->description),
    )) . '<br />',
    '#weight' => -10,
  );
  $form['date'] = array(
    '#type' => 'markup',
    '#value' => t('Backup was created on:<br /> %date', array(
      '%date' => format_date($source->timestamp, 'short'),
    )) . '<br /><br />',
    '#weight' => -8,
  );

  // Build the confirmation form.
  $form = confirm_form($form, t('Are you sure you want to delete this backup?', array()), 'node/' . $sitenid . '/backup', t('This action cannot be undone.'), t('Delete'), t('Cancel'), 'hosting_site_backup_manager_confirm_delete');

  // Copied from hosting_task.module
  // Add an extra class to the actions to allow us to disable
  // the cancel link via javascript for the modal dialog.
  $form['actions']['#prefix'] = '<div id="hosting-task-confirm-form-actions" class="container-inline">';
  return $form;
}

/**
 * The form submit function.
 */
function hosting_site_backup_manager_confirm_delete_submit($form, &$form_state) {

  // The deletion has been confirmed, process the result.
  $filename = $form['#filename'];
  $sitenid = $form['#sitenid'];
  $bid = $form['#bid'];

  // Add the hosting task.
  hosting_add_task($sitenid, 'backup_delete', array(
    $bid => $filename,
  ));
  $form_state['redirect'] = 'node/' . $sitenid . '/backup';
  modalframe_close_dialog();
}

/**
 * Placeholder  function for additional restore checks.
 *
 * @param node $site
 *   The site node object.
 * @param int $bid
 *   The backup id.
 *
 * @return string
 *   A confirmation form.
 */
function hosting_site_backup_manager_restore($site, $bid) {

  // @todo: Build in extra checks.
  $output = drupal_get_form('hosting_site_backup_manager_confirm_restore', $site->nid, $bid);
  return $output;
}

/**
 * Function that renders a confirmation form for the selected deletetion.
 *
 * @param array $form_state
 *   The form state array. Changes made to this variable will have no effect.
 * @param int $sitenid
 *   The selected site node id.
 * @param int $bid
 *   The backup id
 *
 * @return string
 *   A confirmation form.
 */
function hosting_site_backup_manager_confirm_restore($form_state, $sitenid, $bid) {

  // Add the hosting_task javascript,
  // so we can use the confirm form functionality.
  drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js');

  // Build the form.
  $form = array();
  $form['#sitenid'] = $sitenid;
  $form['#bid'] = $bid;

  // Get the backup data.
  $source = db_fetch_object(db_query("SELECT * FROM {hosting_site_backups} WHERE site=%d AND bid=%d", $sitenid, $bid));

  // Not the best formatted code, but suffices for now.
  $form['explanation'] = array(
    '#type' => 'markup',
    '#value' => '<h2>' . t('Backup information') . '</h2>' . t('Backup description:<br /> %description', array(
      '%description' => filter_xss($source->description),
    )) . '<br />',
    '#weight' => -10,
  );
  $form['date'] = array(
    '#type' => 'markup',
    '#value' => t('Backup was created on:<br /> %date', array(
      '%date' => format_date($source->timestamp, 'short'),
    )) . '<br /><br />',
    '#weight' => -8,
  );

  // Build the confirmation form.
  $form = confirm_form($form, t('Are you sure you want to restore this backup?', array()), 'node/' . $sitenid . '/backup', t('This action cannot be undone.'), t('Restore'), t('Cancel'), 'hosting_site_backup_manager_confirm_restore');

  // Copied from hosting_task.module
  // add an extra class to the actions to allow us to
  // disable the cancel link via javascript for the modal dialog.
  $form['actions']['#prefix'] = '<div id="hosting-task-confirm-form-actions" class="container-inline">';
  return $form;
}

/**
 * The form submit function.
 */
function hosting_site_backup_manager_confirm_restore_submit($form, &$form_state) {

  // The restoration has been confirmed, process the result.
  $sitenid = $form['#sitenid'];
  $bid = $form['#bid'];
  hosting_add_task($sitenid, 'restore', array(
    'bid' => $bid,
  ));
  $form_state['redirect'] = 'node/' . $sitenid . '/backup';
  modalframe_close_dialog();
}

/**
 * The form submit function.
 */
function hosting_site_backup_manager_export($site, $bid) {

  // Get the filename.
  $source = db_fetch_object(db_query("SELECT filename FROM {hosting_site_backups} WHERE site=%d AND bid=%d ORDER BY timestamp DESC", $site->nid, $bid));

  // Start the task.
  hosting_add_task($site->nid, 'export_backup', array(
    'backup' => $source->filename,
  ));

  // Redirect to the backups page.
  drupal_goto('node/' . $site->nid . '/backups');
}

/**
 * Function to download a backup file.
 *
 * @param node $site
 *   The site node object.
 * @param int $bid
 *   The backup id.
 *
 * @return file
 *   A file or a 404 page.
 */
function hosting_site_backup_manager_download($site, $bid) {

  // @todo: check if the bid is not in a task.
  $source = db_fetch_object(db_query("SELECT filename FROM {hosting_site_backups} WHERE site=%d AND bid=%d ORDER BY timestamp DESC", $site->nid, $bid));
  if ($source) {

    // Determine client name.
    $client = hosting_client_node_load($site->client);

    // Get Document Root.
    $docroot = _hosting_site_backup_manager_getaegirroot();
    $symlink = $docroot . '/backup-exports/' . $client->uname . '/' . basename($source->filename);
    if ($fd = @fopen($symlink, 'rb')) {

      // Construct filename.
      $filename = basename($source->filename);

      // Set headers.
      header("Cache-Control: public");
      header("Content-Description: File Transfer");
      header("Content-Disposition: attachment; filename=\"{$filename}\"");
      header("Content-Type: application/octet-stream");
      header("Content-Transfer-Encoding: binary");
      while (!feof($fd)) {
        print fread($fd, 1024);
      }
      fclose($fd);

      // File downloaded so delete the file.
      // Start the task.
      hosting_add_task($site->nid, 'remove_export_backup', array(
        'export' => $symlink,
      ));
      exit;
    }
    else {
      drupal_not_found();
    }
  }
  else {
    drupal_not_found();
  }
}

/**
 * Show a list of backups for a website.
 *
 * @param node $site
 *   The site node.
 *
 * @return string
 *   A theme table with a list of backups.
 */
function hosting_site_backup_manager_page($site) {

  // Add the js.
  drupal_add_js(drupal_get_path('module', 'hosting_site_backup_manager') . '/hosting_site_backup_manager.js');
  $settings['hostingSiteBackupManager'] = array(
    'nid' => $site->nid,
  );
  drupal_add_js($settings, 'setting');
  $output = '<div id="hosting-site-backup-manager-backupstable">';
  $output .= hosting_site_backup_manager_backups_table($site);
  $output .= '</div>';
  return $output;
}

/**
 * Page callback for backups table via AJAX.
 */
function hosting_site_backup_manager_ajax_list($site) {
  $return['markup'] = hosting_site_backup_manager_backups_table($site);
  drupal_json($return);
  exit;
}

/**
 * Prepare a table of available backups.
 */
function hosting_site_backup_manager_backups_table($site) {
  global $user;

  // Determine client name.
  $client = hosting_client_node_load($site->client);
  $headers[] = t('Backup');
  $headers[] = array(
    'data' => t('Actions'),
    'class' => 'hosting-actions',
  );
  $rows = array();

  // Only allow actions when there's no backup delete or
  // restore task running for this node.
  $buttonstatus = !(hosting_task_outstanding($site->nid, 'backup_delete') || hosting_task_outstanding($site->nid, 'restore') || hosting_task_outstanding($site->nid, 'export_backup') || hosting_task_outstanding($site->nid, 'remove_export_backup'));

  // TODO Make the table reload automatically.
  $result = db_query("SELECT bid, description, filename, size, timestamp FROM {hosting_site_backups} WHERE site=%d ORDER BY timestamp DESC", $site->nid);
  if (db_affected_rows($result) == 0) {
    $output = t("No backups available.");
    $options = array(
      'query' => array(
        'token' => drupal_get_token($user->uid),
      ),
      'attributes' => array(
        'class' => 'hosting-button-dialog',
      ),
    );
    $output .= '&nbsp;' . l(t('Create backup'), 'node/' . $site->nid . '/site_backup', $options);
    return $output;
  }
  while ($object = db_fetch_object($result)) {
    $row = array();
    $row['description'] = filter_xss($object->description) . "<br />" . format_size($object->size) . " - " . format_date($object->timestamp, 'short');
    $actions = array();

    // @todo: Add check if the backup can be restored to current platform?

    /* Add download button */
    $downloadstatus = _hosting_site_backup_manager_isfileavailable($site, $object->bid) && !hosting_task_outstanding($site->nid, 'remove_export_backup');
    $actions['download'] = _hosting_task_button(t('Get'), 'node/' . $site->nid . '/backup/download/' . $object->bid, t('Download the backup'), '', $downloadstatus, FALSE, FALSE);

    /* Add export backup button */
    $exportstatus = $buttonstatus && !$downloadstatus;
    $actions['export_backup'] = _hosting_task_button(t('Export'), 'node/' . $site->nid . '/backup/export/' . $object->bid, t('Make the backup exportable'), '', $exportstatus, FALSE, FALSE);

    /* Add delete button */
    $actions['delete'] = _hosting_task_button(t('Delete'), 'node/' . $site->nid . '/backup/delete/' . $object->bid, t('Delete the backup'), '', $buttonstatus, TRUE, FALSE);

    /* Add restore button */
    $actions['restore'] = _hosting_task_button(t('Restore'), 'node/' . $site->nid . '/backup/restore/' . $object->bid, t('Restore the backup'), '', $buttonstatus, TRUE, FALSE);
    $row['actions'] = array(
      'data' => implode('', $actions),
      'class' => 'hosting-actions',
    );
    $rows[] = array(
      'data' => $row,
      'class' => $info['class'],
    );
  }
  $output .= theme('table', $headers, $rows, array(
    'class' => 'hosting-table',
  ));
  return $output;
}

/**
 * Implements hook_hosting_tasks().
 */
function hosting_site_backup_manager_hosting_tasks() {
  $tasks = array();
  $tasks['site']['export_backup'] = array(
    'title' => t('Export Backup'),
    'description' => t('Make a backup available for download.'),
    'dialog' => TRUE,
    'hidden' => TRUE,
  );
  $tasks['site']['remove_export_backup'] = array(
    'title' => t('Remove Export Backup'),
    'description' => t('Remove an exported backup, making it unavailable for download.'),
    'dialog' => FALSE,
    'hidden' => TRUE,
  );
  return $tasks;
}

/**
 * Helper function to get the Aegir root directory.
 */
function _hosting_site_backup_manager_getaegirroot() {

  // TODO: Make it a variable or determine it from $_SERVER['DOCUMENT_ROOT']
  return '/var/aegir';
}

/**
 * Helper function to check if a backup file is available.
 */
function _hosting_site_backup_manager_isfileavailable($site, $bid) {
  $result = FALSE;

  // @todo: check if the bid is not in a task.
  $source = db_fetch_object(db_query("SELECT filename FROM {hosting_site_backups} WHERE site=%d AND bid=%d ORDER BY timestamp DESC", $site->nid, $bid));
  if ($source) {

    // Determine client name.
    $client = hosting_client_node_load($site->client);

    // Get Document Root.
    $docroot = _hosting_site_backup_manager_getaegirroot();
    $file = $docroot . '/backup-exports/' . $client->uname . '/' . basename($source->filename);
    if (file_exists($file)) {
      return TRUE;
    }
  }
  return $result;
}

/**
 * Remove backup links if they persist beyond a timeout.
 *
 * They should normally be cleared directly after download.
 *
 * Implements hook_cron().
 */
function hosting_site_backup_manager_cron() {

  // Get Document Root.
  $docroot = _hosting_site_backup_manager_getaegirroot();
  $backup_export_expire_timeout = 60 * 10;
  $backup_root = $file = $docroot . '/backup-exports/';
  if (!is_dir($backup_root)) {
    return;
  }
  try {

    // Loop the client names that have a dir under the backup root.
    $iterator = new DirectoryIterator($backup_root);
    foreach ($iterator as $path) {
      if ($path
        ->isDot() || !$path
        ->isDir()) {
        continue;
      }

      // Loop all files in the client's backup dir.
      $iterator2 = new DirectoryIterator($path
        ->getPathname());
      foreach ($iterator2 as $path2) {
        if ($path2
          ->isDot() || !$path2
          ->isFile()) {
          continue;
        }

        // Get the inode Change time, not to be confused with the modified time.
        // When the inode changed, not the content. e.g. when the extra hardlink was added to the inode.
        if ($path2
          ->getCTime() < time() - $backup_export_expire_timeout) {
          echo "wanna unlink: " . $path2
            ->getPathname();
          watchdog('hosting_site_backup_manager', "Unlinking leftover exported backup: @pathname ", array(
            '@pathname' => $path2
              ->getPathname(),
          ));
          unlink($path2
            ->getPathname());
        }

        // Were now using hardlinks, but incase we ever want to switch:
        // For symlinks, $path2->getCTime() returns that of the link target not the link itself.
        // $link_stats = lstat($path2->getPathname());
        // if ($link_stats['mtime'] < (time() - $backup_export_expire_timeout)) {
      }
    }
  } catch (UnexpectedValueException $e) {

    // TODO: convert to watchdog_exception for D7
    watchdog('hosting_site_backup_manager', nl2br(check_plain($e
      ->getMessage())), NULL, WATCHDOG_WARNING);
  }
}

Functions

Namesort descending Description
hosting_site_backup_manager_ajax_list Page callback for backups table via AJAX.
hosting_site_backup_manager_backups_table Prepare a table of available backups.
hosting_site_backup_manager_confirm_delete Function that renders a confirmation form for the selected deletetion.
hosting_site_backup_manager_confirm_delete_submit The form submit function.
hosting_site_backup_manager_confirm_restore Function that renders a confirmation form for the selected deletetion.
hosting_site_backup_manager_confirm_restore_submit The form submit function.
hosting_site_backup_manager_cron Remove backup links if they persist beyond a timeout.
hosting_site_backup_manager_delete Placeholder function for additional delete checks.
hosting_site_backup_manager_download Function to download a backup file.
hosting_site_backup_manager_export The form submit function.
hosting_site_backup_manager_hosting_tasks Implements hook_hosting_tasks().
hosting_site_backup_manager_menu Implements of hook_menu().
hosting_site_backup_manager_page Show a list of backups for a website.
hosting_site_backup_manager_restore Placeholder function for additional restore checks.
_hosting_site_backup_manager_getaegirroot Helper function to get the Aegir root directory.
_hosting_site_backup_manager_isfileavailable Helper function to check if a backup file is available.