You are here

class backup_migrate_destination_dropbox in Backup and Migrate Dropbox 7.2

Same name and namespace in other branches
  1. 6.2 destinations.dropbox.inc \backup_migrate_destination_dropbox
  2. 6 destinations.dropbox.inc \backup_migrate_destination_dropbox
  3. 7.3 destinations.dropbox.inc \backup_migrate_destination_dropbox
  4. 7 destinations.dropbox.inc \backup_migrate_destination_dropbox

A destination for sending database backups to a Dropbox account.

@property array $dest_url

Hierarchy

Expanded class hierarchy of backup_migrate_destination_dropbox

1 string reference to 'backup_migrate_destination_dropbox'
backup_migrate_dropbox_backup_migrate_destination_subtypes in ./backup_migrate_dropbox.module
Implements hook_backup_migrate_destination_subtypes().

File

./destinations.dropbox.inc, line 15
destinations.dropbox.inc

View source
class backup_migrate_destination_dropbox extends backup_migrate_destination_remote {
  var $supported_ops = array(
    'scheduled backup',
    'manual backup',
    'remote backup',
    'restore',
    'configure',
    'delete',
    'list files',
  );

  // Caches the list of remote files.
  public $cache_files = TRUE;

  // 32 days, instead of 1 day, because we update the cache smartly.
  public $cache_expire = 2764800;

  // Own property to prevent clearing the cache on save or delete
  protected $prevent_cache_clear = FALSE;

  /**
   * Lists all files from the Dropbox destination.
   */
  public function _list_files() {
    try {
      $path = $this
        ->remote_path();
      $response = $this
        ->getDropboxApi()
        ->list_folder($path);
    } catch (Exception $e) {
      watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), array(), WATCHDOG_ERROR);
      drupal_set_message('Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), 'error');
      return NULL;
    }
    $files = array();
    foreach ($response as $entry) {
      if ($entry->{'.tag'} === 'file') {
        $info = array(
          'filename' => $this
            ->local_path($entry->path_display),
          'filesize' => $entry->size,
          'filetime' => DateTime::createFromFormat('Y-m-d\\TH:i:s\\Z', $entry->server_modified)
            ->getTimestamp(),
        );
        $files[$info['filename']] = new backup_file($info);
      }
    }
    return $files;
  }

  /**
   * Loads the file with the given destination specific id.
   *
   * @param string $file_id
   *
   * @return \backup_file
   *   The loaded file.
   */
  public function load_file($file_id) {
    $file = new backup_file(array(
      'filename' => $file_id,
    ));
    try {
      $path = $this
        ->remote_path($file_id);
      $contents = $this
        ->getDropboxApi()
        ->file_download($path);
      file_put_contents($file
        ->filepath(), $contents);
    } catch (Exception $e) {
      watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), array(), WATCHDOG_ERROR);
      drupal_set_message('Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), 'error');
    }
    return $file;
  }

  /**
   * Saves a file to the Dropbox destination.
   *
   * @param \backup_file $file
   * @param \backup_migrate_profile $settings
   *
   * @return \backup_file|null
   */
  function _save_file($file, $settings) {
    try {
      $filename = $file->name . '.' . implode('.', $file->ext);
      $path = $this
        ->remote_path($filename);
      $this
        ->getDropboxApi()
        ->file_upload($file
        ->filepath(), $path);
    } catch (Exception $e) {
      watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), array(), WATCHDOG_ERROR);
      drupal_set_message('Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), 'error');
      return NULL;
    }
    return $file;
  }

  /**
   * Deletes a file on Dropbox.
   *
   * @param string $file_id
   */
  public function _delete_file($file_id) {
    try {
      $path = $this
        ->remote_path($file_id);
      $this
        ->getDropboxApi()
        ->file_delete($path);
    } catch (Exception $e) {

      // Ignore file not found errors
      if (strpos($e
        ->getMessage(), 'path_lookup/not_found') !== FALSE) {
        watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
          ->getMessage(), array(), WATCHDOG_ERROR);
        drupal_set_message('Backup Migrate Dropbox Error: ' . $e
          ->getMessage(), 'error');
      }
    }
  }

  /**
   * Generate a remote file path for the given path.
   *
   * @param string $path
   *
   * @return string
   *   The remote file path, not ending with a /.
   */
  public function remote_path($path = '') {
    return '/' . implode('/', array_filter([
      $this->dest_url['path'],
      $path,
    ]));
  }

  /**
   * Generate a local file path with the correct prefix.
   *
   * The local path will be $path without the root / and defined sub path part
   * and will be relative, i.e. not start with a /.
   *
   * @param string $path
   *
   * @return string
   *   A relative local path based on the relative path in the Dropbox app
   *   folder.
   */
  public function local_path($path) {
    $base_path = '/';
    if (!empty($this->dest_url['path'])) {
      $base_path .= $this->dest_url['path'] . '/';
    }

    // Strip base path.
    if (substr($path, 0, strlen($base_path)) === $base_path) {
      $path = substr($path, strlen($base_path));
    }
    return $path;
  }

  /**
   * Get the form for the settings for this filter.
   */
  function edit_form() {
    $form = parent::edit_form();
    $summary = t('Defining a Dropbox destination ...');
    $dropboxHeader = t('Dropbox part:');
    $dropboxList = t('<ol><li>Create or Login to your Dropbox account.</li>
	<li>Go to <a href="https://www.dropbox.com/developers" target="_blank">https://www.dropbox.com/developers</a>.</li>
	<li>Click on the “Create apps” button.</li>
	<li>Select “Dropbox API”.</li>
	<li>Select “App folder”. This will protect the rest of your Dropbox in case your site is compromised.</li>
	<li>Enter an app name and create it. Note that the app name must be globally unique, so you may have to retry it a few times. Using your website name may help.</li>
	<li>Set “Allow implicit grant” to “Disallow”.</li>
	<li>Click op “Generate” below “Generated access token”.</li>
	<li>Copy the token to your clipboard.</li>
</ol>');
    $drupalHeader = t('Drupal part:');
    $drupalList = t('<ol><li>Go to “Configuration - System - Backup and Migrate - Settings - Destinations - Add destination” (this page).</li>
	<li>Select “Dropbox” from the off-site destinations list.</li>
	<li>Fill in a “Destination name”.</li>
	<li>Optionally fill in the name of a subdirectory to place the backups to this destination in. E.g. use this if you have multiple destinations for this site (daily, weekly, ...) or multiple sites that backup to this app (mysite1, mysite2/daily, mysite2/weekly, ...). Upon saving, the module will create the subdirectory in your Dropbox folder.</li>
	<li>Paste the token into the “Dropbox Access Token” field.</li>
	<li>Click on “Save destination”.</li>
	<li>Test if everything works fine by doing a manual backup to the newly created destination.</li>
</ol>');
    $form['description'] = array(
      '#type' => 'markup',
      '#weight' => -999,
      '#markup' => "<details open style='border:1px solid #ccc;border-top-color:#999;padding:0.3em;margin-bottom:1em;'><summary style='font-size:larger;font-weight:bold;cursor:pointer;'>{$summary}</summary>\n<h3>{$dropboxHeader}</h3>\n{$dropboxList}\n<h3>{$drupalHeader}</h3>\n{$drupalList}</details>",
    );
    $form['name']['#description'] = t('Enter a "friendly" name for this destination. Only appears as a descriptor in the Backup and Migrate administration screens.');
    $form['scheme'] = array(
      '#type' => 'value',
      '#value' => 'https',
    );
    $form['host'] = array(
      '#type' => 'value',
      '#value' => 'www.dropbox.com',
    );
    $form['path']['#description'] = t('A relative folder inside your Dropbox App folder. Upon saving, the module will create the path in your Dropbox App folder. You should not use slashes before or after the path. For example, you can use this if you have multiple destinations for this site (daily, weekly, ...) or multiple sites that backup to this account (mysite1, mysite2/daily, mysite2/weekly, ...).');
    $form['path']['#required'] = FALSE;
    $form['user'] = array(
      '#type' => 'value',
      '#value' => '',
    );
    $form['old_password'] = array(
      '#type' => 'value',
      '#value' => '',
    );
    $form['pass'] = array(
      '#type' => 'value',
      '#value' => '',
    );
    $form['settings']['token'] = array(
      '#type' => 'textfield',
      '#title' => 'Dropbox Access Token',
      '#description' => 'Generated access token from your app. <b>Do not</b> use the secret key.',
      '#required' => TRUE,
      "#default_value" => $this
        ->settings('token'),
    );

    // How can we list the complete path to the Dropbox folder
    // (https://www.dropbox.com/home/Apps/{App name}/path) on the destinations
    // overview page? See
    // https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/dropbox-programmatically-get-quot-app-folder-quot-path/td-p/201627
    // Conclusion: only when we let the user fill in the App name, as we cannot
    // retrieve it ourselves.
    $destinations = url($this
      ->get_settings_path());
    $form['settings']['app_name'] = array(
      '#type' => 'textfield',
      '#title' => 'Dropbox App name (optional)',
      '#description' => "The name of the app. This is only used to show you a link to your Dropbox folder on the <a href='{$destinations}'>Backup and Migrate destinations list</a> page.",
      '#required' => FALSE,
      "#default_value" => $this
        ->settings('app_name'),
    );

    // Position the settings part on the form.
    $form['settings']['#weight'] = 60;
    return $form;
  }

  /**
   * Submit the form for the settings for the files destination.
   *
   * @param array $form
   * @param array $form_state
   */
  function edit_form_submit($form, &$form_state) {

    // Add the token.
    $form_state['values']['settings']['token'] = $form_state['values']['token'];
    if ($form_state['values']['token'] !== $this
      ->settings('token')) {
      $form_state['values']['settings']['token'] = $form_state['values']['token'];

      // Different token may mean different Dropbox folder: clear the file list.
      $this
        ->file_cache_clear();

      // We need this new token right away to create the folder.

      /** @noinspection PhpUndefinedFieldInspection */
      $this->settings['token'] = $form_state['values']['token'];
    }

    // Strip any / at begin and end.
    $form_state['values']['path'] = trim($form_state['values']['path'], '/');
    if (!empty($form_state['values']['path'])) {

      // Create folder and clear the file list cache if it was changed.
      try {
        $this
          ->getDropboxApi()
          ->create_folder($form_state['values']['path']);
        drupal_set_message(t("The folder '%path' has been created in your Dropbox app folder", array(
          '%path' => $form_state['values']['path'],
        )), 'status');
        $this
          ->file_cache_clear();
      } catch (Exception $e) {
        if (strpos($e
          ->getMessage(), 'path/conflict/folder') !== FALSE) {

          // Do not display "Folder already exists" messages, as these are not
          // errors. However, check if the path was changed, so we can clear the
          // file list cache.
          if ($form_state['values']['path'] !== $this->dest_url['path']) {
            $this
              ->file_cache_clear();
          }
        }
        else {
          watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
            ->getMessage(), array(), WATCHDOG_ERROR);
          drupal_set_message('Backup Migrate Dropbox Error: ' . $e
            ->getMessage(), 'error');
        }
      }
    }

    // Store the app name.
    $form_state['values']['settings']['app_name'] = $form_state['values']['app_name'];
    parent::edit_form_submit($form, $form_state);
  }

  /**
   * {@inheritdoc}
   *
   * We override to insert /home/Apps/{App name} between host and path.
   */
  public function get_display_location() {
    $result = parent::get_display_location();
    $host = $this->dest_url['host'];
    if (($pos = strpos($result, $host)) !== FALSE) {
      $pos += strlen($host);
      $app_name = $this
        ->settings('app_name') ? $this
        ->settings('app_name') : '{App name}';
      $result = substr($result, 0, $pos) . '/home/Apps/' . $app_name . substr($result, $pos);
    }
    return $result;
  }
  public function save_file($file, $settings) {

    // Smart caching: update the cache instead of clearing it.
    $this->prevent_cache_clear = true;
    $result = parent::save_file($file, $settings);

    // Add to the cache. The cache normally only contains the backup file
    // entries, not the info entries as these are deleted form the files list
    // when their info is merged into the info of the backup file itself.
    // So we only add the backup file itself. (Thus this code is correctly
    // placed here in save_file() and not in _save_file().)
    $files = $this
      ->file_cache_get();
    $filename = $file->name . '.' . implode('.', $file->ext);
    $files[$filename] = $file;
    $this
      ->file_cache_set($files);
    $this->prevent_cache_clear = false;
    return $result;
  }
  public function delete_file($file_id) {

    // Smart caching: update the cache instead of clearing it.
    $this->prevent_cache_clear = true;
    parent::delete_file($file_id);
    $files = $this
      ->file_cache_get();
    $file_info_id = $this
      ->_file_info_filename($file_id);

    // Was this file or its info file in the cache?
    if (isset($files[$file_id]) || isset($files[$file_info_id])) {

      // Yes: delete them from the cache and update the cache entry.
      unset($files[$file_id]);
      unset($files[$file_info_id]);
      $this
        ->file_cache_set($files);
    }
    $this->prevent_cache_clear = false;
  }

  /**
   * {@inheritdoc}
   *
   * This destination updates the cache in a smart way. Normally, the list of
   * (remote) files is created and cached in these steps:
   * - Create list in list_files() and cache it for 1 day.
   * - Clear cache on expiry, save a new backup, remove an existing backup, and
   *   refresh (admin/config/system/backup_migrate/backups?refresh=true), but
   *   not on settings form submit.
   *
   * We do this as follows:
   * - Create list in list_files() and cache it for 32 days.
   * - Clear cache on expiry, change token, change path, and refresh
   *   (admin/config/system/backup_migrate/backups?refresh=true).
   * - Update cache on save a new backup, and remove an existing backup.
   *
   * So, refresh should only be necessary when the Dropbox folder gets updated
   * outside Drupal Backup and Migrate.
   */
  public function file_cache_clear() {

    // Check if we are in a smart caching process and if so, do not clear the
    // cache as we wil be updating it.
    if (!$this->prevent_cache_clear) {
      parent::file_cache_clear();
    }
  }

  /**
   * @return \BackupMigrateDropboxAPI
   */
  private function getDropboxApi() {
    static $dropbox_api = null;
    if ($dropbox_api === null) {
      module_load_include('inc', 'backup_migrate_dropbox', 'backup_migrate_dropbox.dropbox_api');
      $dropbox_api = new BackupMigrateDropboxAPI();
      $dropbox_api
        ->setToken($this
        ->settings('token'));
    }
    return $dropbox_api;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
backup_migrate_destination_dropbox::$cache_expire public property
backup_migrate_destination_dropbox::$cache_files public property
backup_migrate_destination_dropbox::$prevent_cache_clear protected property
backup_migrate_destination_dropbox::$supported_ops property
backup_migrate_destination_dropbox::delete_file public function
backup_migrate_destination_dropbox::edit_form function Get the form for the settings for this filter.
backup_migrate_destination_dropbox::edit_form_submit function Submit the form for the settings for the files destination.
backup_migrate_destination_dropbox::file_cache_clear public function This destination updates the cache in a smart way. Normally, the list of (remote) files is created and cached in these steps:
backup_migrate_destination_dropbox::getDropboxApi private function
backup_migrate_destination_dropbox::get_display_location public function We override to insert /home/Apps/{App name} between host and path.
backup_migrate_destination_dropbox::load_file public function Loads the file with the given destination specific id.
backup_migrate_destination_dropbox::local_path public function Generate a local file path with the correct prefix.
backup_migrate_destination_dropbox::remote_path public function Generate a remote file path for the given path.
backup_migrate_destination_dropbox::save_file public function
backup_migrate_destination_dropbox::_delete_file public function Deletes a file on Dropbox.
backup_migrate_destination_dropbox::_list_files public function Lists all files from the Dropbox destination.
backup_migrate_destination_dropbox::_save_file function Saves a file to the Dropbox destination.