if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.');
if (!class_exists('UpdraftPlus_BackupModule')) require_once(UPDRAFTPLUS_DIR.'/methods/backup-module.php');
class UpdraftPlus_BackupModule_openstack_base extends UpdraftPlus_BackupModule {
public function __construct($method, $desc, $long_desc = null, $img_url = '') {
$this->long_desc = (is_string($long_desc)) ? $long_desc : $desc;
$this->img_url = $img_url;
public function backup($backup_array) {
$default_chunk_size = (defined('UPDRAFTPLUS_UPLOAD_CHUNKSIZE') && UPDRAFTPLUS_UPLOAD_CHUNKSIZE > 0) ? max(UPDRAFTPLUS_UPLOAD_CHUNKSIZE, 1048576) : 5242880;
$this->chunk_size = $updraftplus->jobdata_get('openstack_chunk_size', $default_chunk_size);
$opts = $this->get_options();
$this->container = $opts['path'];
$storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
} catch (AuthenticationError $e) {
$updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
$updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
$updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
$updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
$this->container_object = $storage->getContainer($this->container);
$updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
$updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
foreach ($backup_array as $file) {
$file_key = 'status_'.md5($file);
$file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key);
if (is_array($file_status) && !empty($file_status['chunks']) && !empty($file_status['chunks'][1]['size'])) $this->chunk_size = $file_status['chunks'][1]['size'];
// First, see the object's existing size (if any)
$uploaded_size = $this->get_remote_size($file);
if (1 === $updraftplus->chunked_upload($this, $file, $this->method."://".$this->container."/$file", $this->desc, $this->chunk_size, $uploaded_size)) {
if (false !== ($data = fopen($updraftplus->backups_dir_location().'/'.$file, 'r+'))) {
$this->container_object->uploadObject($file, $data);
$updraftplus->log($this->desc." regular upload: success");
$updraftplus->uploaded_file($file);
throw new Exception('uploadObject failed: fopen failed');
$this->log("regular upload: failed ($file) (".$e->getMessage().")");
$this->log("$file: ".sprintf(__('Error: Failed to upload', 'updraftplus')), 'error');
$updraftplus->log($this->desc.' error - failed to upload file'.' ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
$updraftplus->log(sprintf(__('%s error - failed to upload file', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
return array('object' => $this->container_object, 'orig_path' => $opts['path'], 'container' => $this->container);
private function get_remote_size($file) {
$response = $this->container_object->getClient()->head($this->container_object->getUrl($file))->send();
$response_object = $this->container_object->dataObject()->populateFromResponse($response)->setName($file);
return $response_object->getContentLength();
// Allow caller to distinguish between zero-sized and not-found
* This function lists the files found in the configured storage location
* @param String $match a substring to require (tested via strpos() !== false)
* @return Array - each file is represented by an array with entries 'name' and (optional) 'size'
public function listfiles($match = 'backup_') {
$opts = $this->get_options();
$container = $opts['path'];
if (empty($opts['user']) || (empty($opts['apikey']) && empty($opts['password']))) return new WP_Error('no_settings', __('No settings were found', 'updraftplus'));
$storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')');
$this->container_object = $storage->getContainer($container);
return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')');
// http://php-opencloud.readthedocs.io/en/latest/services/object-store/objects.html#list-objects-in-a-container
while (null !== $marker) {
$objects = $this->container_object->objectList($params);
$total = $objects->count();
while (false !== ($file = $objects->offsetGet($index)) && !empty($file)) {
if ((is_object($file) && !empty($file->name))) {
$result = array('name' => $file->name);
// Rackspace returns the size of a manifested file properly; other OpenStack implementations may not
if (!empty($file->bytes)) {
$result['size'] = $file->bytes;
$size = $this->get_remote_size($file->name);
if (false !== $size && $size > 0) $result['size'] = $size;
$marker = (!empty($file->name) && $total >= $page_size) ? $file->name : null;
* Called when all chunks have been uploaded, to allow any required finishing actions to be carried out
* @param String $file - the basename of the file being uploaded
* @return Boolean - success or failure state of any finishing actions
public function chunked_upload_finish($file) {
$chunk_path = 'chunk-do-not-delete-'.$file;
'X-Object-Manifest' => sprintf('%s/%s', $this->container, $chunk_path.'_')
$url = $this->container_object->getUrl($file);
$this->container_object->getClient()->put($url, $headers)->send();
$updraftplus->log("Error when sending manifest (".get_class($e)."): ".$e->getMessage());
* N.B. Since we use varying-size chunks, we must be careful as to what we do with $chunk_index
* @param String $file Full path for the file being uploaded
* @param Resource $fp File handle to read upload data from
* @param Integer $chunk_index Index of chunked upload
* @param Integer $upload_size Size of the upload, in bytes
* @param Integer $upload_start How many bytes into the file the upload process has got
* @param Integer $upload_end How many bytes into the file we will be after this chunk is uploaded
* @param Integer $total_file_size Total file size
public function chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $total_file_size) {
$file_key = 'status_'.md5($file);
$file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key);
$next_chunk_size = $upload_size;
$bytes_already_uploaded = 0;
$last_uploaded_chunk_index = 0;
// Once a chunk is uploaded, its status is set, allowing the sequence to be reconstructed
if (is_array($file_status) && isset($file_status['chunks']) && !empty($file_status['chunks'])) {
foreach ($file_status['chunks'] as $c_id => $c_status) {
if ($c_id > $last_uploaded_chunk_index) $last_uploaded_chunk_index = $c_id;
if ($chunk_index + 1 == $c_id) {
$next_chunk_size = $c_status['size'];
$bytes_already_uploaded += $c_status['size'];
$file_status = array('chunks' => array());
$this->jobdata_set($file_key, $file_status);
if ($upload_start < $bytes_already_uploaded) {
if ($next_chunk_size != $upload_size) {
$response = new stdClass;
$response->new_chunk_size = $upload_size;
// Shouldn't be able to happen
if ($chunk_index <= $last_uploaded_chunk_index) {
$updraftplus->log($this->desc.": Chunk sequence error; chunk_index=$chunk_index, last_uploaded_chunk_index=$last_uploaded_chunk_index, upload_start=$upload_start, upload_end=$upload_end, file_status=".json_encode($file_status));
// Used to use $chunk_index here, before switching to variable chunk sizes
$upload_remotepath = 'chunk-do-not-delete-'.$file.'_'.sprintf("%016d", $chunk_index);
$remote_size = $this->get_remote_size($upload_remotepath);
// Without this, some versions of Curl add Expect: 100-continue, which results in Curl then giving this back: curl error: 55) select/poll returned error
// Didn't make the difference - instead we just check below for actual success even when Curl reports an error
// $chunk_object->headers = array('Expect' => '');
if ($remote_size >= $upload_size) {
$updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): already uploaded");
$updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): begin upload");
$data = fread($fp, $upload_size);
$time_start = microtime(true);
$this->container_object->uploadObject($upload_remotepath, $data);
$time_now = microtime(true);
$time_taken = $time_now - $time_start;
if ($next_chunk_size < 52428800 && $total_file_size > 0 && $upload_end + 1 < $total_file_size) {
$job_run_time = $time_now - $updraftplus->job_time_ms;
$upload_rate = $upload_size / max($time_taken, 0.0001);
$upload_secs = min(floor($job_run_time), 10);
if ($job_run_time < 15) $upload_secs = max(6, $job_run_time*0.6);
$memory_limit_mb = $updraftplus->memory_check_current();
$bytes_used = memory_get_usage();
$bytes_free = $memory_limit_mb * 1048576 - $bytes_used;
$new_chunk = max(min($upload_secs * $upload_rate * 0.9, 52428800, $bytes_free), 5242880);
$new_chunk = $new_chunk - ($new_chunk % 5242880);
$next_chunk_size = (int) $new_chunk;
$updraftplus->jobdata_set('openstack_chunk_size', $next_chunk_size);
$updraftplus->log($this->desc." chunk upload: error: ($file / $chunk_index) (".$e->getMessage().") (line: ".$e->getLine().', file: '.$e->getFile().')');
// Experience shows that Curl sometimes returns a select/poll error (curl error 55) even when everything succeeded. Google seems to indicate that this is a known bug.
$remote_size = $this->get_remote_size($upload_remotepath);
if ($remote_size >= $upload_size) {
$updraftplus->log("$file: Chunk now exists; ignoring error (presuming it was an apparently known curl bug)");
$updraftplus->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $this->desc), 'error');
$file_status['chunks'][$chunk_index]['size'] = $upload_size;
$this->jobdata_set($file_key, $file_status);
if ($next_chunk_size != $upload_size) {
$response = new stdClass;
$response->new_chunk_size = $next_chunk_size;
* Delete a single file from the service using OpenStack API
* @param Array|String $files - array of file names to delete
* @param Array $data - service object and container details
* @param Array $sizeinfo - unused here
* @return Boolean|String - either a boolean true or an error code string
public function delete($files, $data = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $sizeinfo unused
if (is_string($files)) $files = array($files);
$container_object = $data['object'];
$container = $data['container'];
$opts = $this->get_options();
$container = $opts['path'];
$storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
} catch (AuthenticationError $e) {
$updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
$updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
return 'authentication_fail';
$updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')');
$updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
return 'service_unavailable';
$container_object = $storage->getContainer($container);
$updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')');
$updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
return 'container_access_error';
foreach ($files as $file) {
$updraftplus->log($this->desc.": Delete remote: container=$container, path=$file");
// We need to search for chunks
$chunk_path = "chunk-do-not-delete-".$file;
$objects = $container_object->objectList(array('prefix' => $chunk_path));
while (false !== ($chunk = $objects->offsetGet($index)) && !empty($chunk)) {
$container_object->dataObject()->setName($name)->delete();
$updraftplus->log($this->desc.': Chunk deleted: '.$name);
$updraftplus->log($this->desc." chunk delete failed: $name: ".$e->getMessage());
$updraftplus->log($this->desc.' chunk delete failed: '.$e->getMessage());
// Finally, delete the object itself
$container_object->dataObject()->setName($file)->delete();
$updraftplus->log($this->desc.': Deleted: '.$file);
$updraftplus->log($this->desc.' delete failed: '.$e->getMessage());
$ret = 'file_delete_error';
public function download($file) {
$opts = $this->get_options();
$storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
} catch (AuthenticationError $e) {
$updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
$updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
$updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')');
$updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
$container = untrailingslashit($opts['path']);
$updraftplus->log($this->desc." download: ".$this->method."://$container/$file");
$this->container_object = $storage->getContainer($container);
$updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')');
$updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
// Get information about the object within the container
$remote_size = $this->get_remote_size($file);
if (false === $remote_size) {
$updraftplus->log('Could not access '.$this->desc.' object');
$updraftplus->log(sprintf(__('The %s object was not found', 'updraftplus'), $this->desc), 'error');
return (!is_bool($remote_size)) ? $updraftplus->chunked_download($file, $this, $remote_size, true, $this->container_object) : false;
public function chunked_download($file, $headers, $container_object) {
$dl = $container_object->getObject($file, $headers);
$updraftplus->log("$file: Failed to download (".$e->getMessage().")");
$updraftplus->log("$file: ".sprintf(__("%s Error", 'updraftplus'), $this->desc).": ".__('Error downloading remote file: Failed to download', 'updraftplus').' ('.$e->getMessage().")", 'error');
return $dl->getContent();
public function credentials_test_go($opts, $path, $useservercerts, $disableverify) {
if (preg_match("#^([^/]+)/(.*)$#", $path, $bmatches)) {
$container = $bmatches[1];
_e('Failure: No container details were given.', 'updraftplus');
$storage = $this->get_openstack_service($opts, $useservercerts, $disableverify);
// @codingStandardsIgnoreLine
} catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
$response = $e->getResponse();
$code = $response->getStatusCode();
$reason = $response->getReasonPhrase();
if (401 == $code && 'Unauthorized' == $reason) {
echo __('Authorisation failed (check your credentials)', 'updraftplus');
echo __('Authorisation failed (check your credentials)', 'updraftplus')." ($code:$reason)";
} catch (AuthenticationError $e) {
echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')';
echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')';