destinations.dropbox.inc in Backup and Migrate Dropbox 7.3
Same filename and directory in other branches
Functions to handle the dropbox backup destination.
File
destinations.dropbox.incView source
<?php
/**
* @file destinations.dropbox.inc
*
* Functions to handle the dropbox backup destination.
*/
/**
* A destination for sending database backups to a Dropbox account.
*
* @property array $dest_url
* @property array $settings
* @ingroup backup_migrate_destinations
*/
class backup_migrate_destination_dropbox extends backup_migrate_destination_remote {
var $supported_ops = [
'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;
/** @var BackupMigrateDropboxAPI */
private $dropbox_api = NULL;
/**
* Get the form for the settings for this filter.
*
* Settings specific for a Dropbox destination:
*
* The user should authorize this App to access (an app folder on) its account
* file system on Dropbox. We do so using the oauth2 protocol. As our app is
* open source not backed up by a "redirect" server that stores tokens or
* codes on behalf of the users we cannot securely store the app secret.
* Therefore we use oauth extended with PKCE, see
* {@link https://dropbox.tech/developers/pkce--what-and-why-}.
*
* To let the user authorize this app, we provide a button that directs the
* user to an authorization URL on the dropbox.com site. Dropbox asks the user
* if (s)he want to give permission to this App and, if so, displays a (short
* lived) access code that the user should enter in this form.
*
* Upon form submission, the entered access code is used to obtain the first
* (short lived) bearer token and a long lived refresh token. The bearer token
* is used as authorization token for the API calls. When it expires,
* subsequent (also short lived) bearer tokens can be get with the refresh
* token.
*
* Furthermore, the user can specify a sub folder on the App folder to store
* the back-ups from this destination. this can be useful, when there are
* multiple destinations (possibly from different sites) backing up to this
* Dropbox account.
*
* So this app should store:
* 1 The refresh token: in the settings.
* 2 The sub folder to write the back-ups to: as part of the dest_url.
* 3 The actual (still valid) bearer token (expired ones can be disposed): the
* settings property of a destination is less suited to constantly changing
* values, so we store this in a variable of our own.
*
* @return array[]
* Drupal form definition for the destination settings form of this module.
*
* @throws Exception
*/
function edit_form() {
$form = parent::edit_form();
// Upgrade warning.
if ($this
->settings('token') !== NULL) {
$app_console_link = l('your App console on Dropbox', 'https://www.dropbox.com/developers/apps', [
'attributes' => [
'target' => '_blank',
],
]);
$app_name = $this
->settings('app_name') ? $this
->settings('app_name') . ' ' : '';
drupal_set_message(t("It seems you are updating from the previous version. Please do not forget to visit !app_console_link and delete the %app_nameapp you created back then.", [
'!app_console_link' => $app_console_link,
'%app_name' => $app_name,
]), 'warning');
}
$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'] = [
'#type' => 'value',
'#value' => 'https',
];
$form['host'] = [
'#type' => 'value',
'#value' => 'www.dropbox.com',
];
$form['user'] = [
'#type' => 'value',
'#value' => '',
];
$form['old_password'] = [
'#type' => 'value',
'#value' => '',
];
$form['pass'] = [
'#type' => 'value',
'#value' => '',
];
// A code verifier is a disposable secret. So we create a new one every time
// this forms gets rendered.
// - It should stay on the server: we use a for item of #type = 'value'.
// - It is needed on form submission: we need to cache the form: we set a
// '#cache' flag to communicate to our own hook_form_FORM_ID_alter() to
// set form caching to true (as we don't have access here to the
// $form_state). This #cache key does not have any meaning to Drupal
// itself.
$code_verifier = $this
->get_dropbox_api()
->create_code_verifier();
$form['code_verifier'] = [
'#type' => 'value',
'#value' => $code_verifier,
];
$form['#cache'] = TRUE;
$refresh_token_known = $this
->settings('refresh_token') !== NULL;
$authorize_help = t('You need to give this app access to your Dropbox folders. ' . 'The only rights this app will be asking for is to be able to read and write to its own sub folder. ' . 'Note: by default, all apps need basic read access to your account details, so that will be asked for too.');
$authorize_instructions = t('By clicking on the button below, which opens in a new tab, ' . 'you will be directed to the Dropbox site where you can authorize this app to access your Dropbox folders. ' . 'Dropbox will give you an authorization code that you need to copy and paste into the field below the button.');
$authorize_url = $this
->get_dropbox_api()
->get_authorize_url($code_verifier);
$authorize_text = $refresh_token_known ? t('Get a new authorization code on') : t('Get an authorization code on');
$path = drupal_get_path('module', 'backup_migrate_dropbox');
$path_logo = file_create_url($path . '/dropbox_logo.svg');
$path_word_mark = file_create_url($path . '/dropbox_wordmark.svg');
$form['authorize_link'] = [
'#type' => 'markup',
'#markup' => "<p>{$authorize_help}</p><p>{$authorize_instructions}</p>" . "<p><a id='authorize_link' href='{$authorize_url}' target='_blank' class='button'><span>{$authorize_text}</span> " . "<img src='{$path_logo}' alt='Dropbox logo'> <img src='{$path_word_mark}' alt='Dropbox word mark'></a></p>",
'#weight' => 12,
'#attached' => [
'css' => [
[
'data' => '#authorize_link {display: inline-block; height: 32px}
#authorize_link span {position: relative; top:-10px}
#authorize_link img {height: 32px}',
'type' => 'inline',
],
],
],
];
$form['access_code'] = [
'#type' => 'textfield',
'#title' => t('Dropbox Access Code'),
'#required' => !$refresh_token_known,
"#default_value" => '',
'#description' => t('Paste the access code you obtained from Dropbox.'),
'#weight' => 13,
];
if ($refresh_token_known) {
// We have a refresh token, so renewing the authorization is optional.
$form['access_code']['#attributes'] = [
'placeholder' => '****************************************',
];
$authorize_note = t('<strong>Please note that you have already authorized this app</strong> and you do not have to do so again. ' . 'However, in the following cases you should obtain a new access code and <strong>revoke the former authorization in your app console on Dropbox</strong>:');
$authorize_note_li1 = 'If you suspect that the data this app stores has been compromised.';
$authorize_note_li2 = 'If you want to back-up to another Dropbox account.';
$form['access_code']['#description'] .= "<p>{$authorize_note}</p><ul><li>{$authorize_note_li1}</li><li>{$authorize_note_li2}</li></ul>";
}
// Position the settings part on the form.
$form['settings']['#weight'] = 15;
// (Note: path: #weight = 20.)
$form['path']['#field_prefix'] = 'apps/DrupalBackup/';
$form['path']['#description'] = t('Your back-ups will be stored in the folder "apps/DrupalBackup".
If you want to arrange your back-ups in a sub folder, e.g. because you have back-ups from multiple sites or multiple destinations, you can specify so here.
Upon saving, the module will create the path in your Dropbox App folder. You should not use slashes before or after the path.');
$form['path']['#required'] = FALSE;
return $form;
}
/**
* Handles the form submission.
*
* Note that the settings property of this destination object will be
* overwritten, not merged, by the 'settings' value in $form_state['values'].
*
* Therefore we have to explicitly set all values that are not on the form
* submission, i.e. the refresh_token.
*
* @param array $form
* @param array $form_state
*/
function edit_form_submit($form, &$form_state) {
// Save the refresh token again.
$form_state['values']['settings']['refresh_token'] = $this
->settings('refresh_token');
// Sub folder: strip any / and \ at begin and end.
$form_state['values']['path'] = trim($form_state['values']['path'], '/\\');
// Has the access code been filled in and changed?
if (!empty($form_state['values']['access_code'])) {
// Access code has changed: obtain a new refresh token and first bearer
// token, but we need our id (machine name) to be set (to be able to store
// the bearer token).
$this
->from_array($form_state['values']);
$form_state['values']['settings']['refresh_token'] = $this
->get_dropbox_api()
->obtain_refresh_token($form_state['values']['access_code'], $form['code_verifier']['#value']);
// Different code may mean different Dropbox account: clear the file list.
$this
->file_cache_clear();
}
// Now we can save all values.
parent::edit_form_submit($form, $form_state);
// Now, with all values in place, we create the sub folder.
if (!empty($form_state['values']['path'])) {
// Create folder and clear the file list cache if it was changed.
$path = $this->dest_url['path'];
try {
$this
->get_dropbox_api()
->create_folder($path);
// No exception: folder has been changed and created: tell user and
// clear the file list
drupal_set_message(t("The folder '%path' has been created in your Dropbox app folder", [
'%path' => $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 ($path !== $form['path']['#default_value']) {
$this
->file_cache_clear();
}
}
else {
watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
->getMessage(), [], WATCHDOG_ERROR);
drupal_set_message('Backup Migrate Dropbox Error: ' . $e
->getMessage(), 'error');
}
}
}
}
/**
* Lists all files from the Dropbox destination.
*
* @return backup_file[]|null
*/
public function _list_files() {
try {
$path = $this
->remote_path();
$response = $this
->get_dropbox_api()
->list_folder($path);
} catch (RuntimeException $e) {
watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
->getMessage(), [], WATCHDOG_ERROR);
drupal_set_message('Backup Migrate Dropbox Error: ' . $e
->getMessage(), 'error');
return NULL;
}
$files = [];
foreach ($response as $entry) {
if ($entry->{'.tag'} === 'file') {
$info = [
'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([
'filename' => $file_id,
]);
try {
$path = $this
->remote_path($file_id);
$contents = $this
->get_dropbox_api()
->file_download($path);
file_put_contents($file
->filepath(), $contents);
} catch (Exception $e) {
watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
->getMessage(), [], 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
->get_dropbox_api()
->file_upload($file
->filepath(), $path);
} catch (Exception $e) {
watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
->getMessage(), [], 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
->get_dropbox_api()
->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(), [], 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;
}
/**
* {@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 = variable_get('backup_migrate_dropbox_app_folder');
$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 get_dropbox_api() {
if ($this->dropbox_api === null) {
module_load_include('inc', 'backup_migrate_dropbox', 'backup_migrate_dropbox.dropbox_api');
$this->dropbox_api = new BackupMigrateDropboxAPI($this);
}
return $this->dropbox_api;
}
}
Classes
Name![]() |
Description |
---|---|
backup_migrate_destination_dropbox | A destination for sending database backups to a Dropbox account. |