if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
if (!class_exists('UpdraftPlus_PclZip')) require_once(UPDRAFTPLUS_DIR.'/includes/class-zip.php');
* This file contains code that is only needed/loaded when a backup is running
class UpdraftPlus_Backup {
private $zipfiles_added_thisrun = 0;
public $zipfiles_dirbatched;
public $zipfiles_batched;
public $zipfiles_skipped_notaltered;
private $zip_split_every = 419430400; // 400MB
private $zip_last_ratio = 1;
private $zip_basename = '';
private $backup_basename = '';
private $zipfiles_lastwritetime;
// 0 = unknown; false = failed
// Array of entities => times
private $altered_since = -1;
// Time for the current entity
private $makezip_if_altered_since = -1;
private $excluded_extensions = false;
private $use_zip_object = 'UpdraftPlus_ZipArchive';
private $job_file_entities = array();
// Record of zip files created
private $backup_files_array = array();
// Used when deciding to use the 'store' or 'deflate' zip storage method
private $extensions_to_not_compress = array();
// Append to this any skipped tables
// When initialised, a boolean
public $last_storage_instance;
// The absolute upper limit that will be considered for a zip batch (in bytes)
private $zip_batch_ceiling;
private $backup_excluded_patterns = array();
// Bytes of uncompressed data written since last open
private $db_current_raw_bytes = 0;
private $table_prefix_raw;
private $many_rows_warning = false;
private $expected_rows = false;
private $try_split = false;
* @param Array|String $backup_files - files to backup, or (string)'no'
* @param Integer $altered_since - only backup files altered since this time (UNIX epoch time)
public function __construct($backup_files, $altered_since = -1) {
$this->site_name = $this->get_site_name();
// Decide which zip engine to begin with
$this->debug = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
$this->updraft_dir = $updraftplus->backups_dir_location();
$this->updraft_dir_realpath = realpath($this->updraft_dir);
require_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
if ('no' === $backup_files) {
$this->use_zip_object = 'UpdraftPlus_PclZip';
$this->extensions_to_not_compress = array_unique(array_map('strtolower', array_map('trim', explode(',', UPDRAFTPLUS_ZIP_NOCOMPRESS))));
$this->backup_excluded_patterns = array(
// all in one wp migration pattern: WP_PLUGIN_DIR/all-in-one-wp-migration/storage/*/*.wpress, `ai1wm-backups` folder in wp-content is already implicitly handled on the UDP settings with a `*backups` predefined exclusion rule for `others` directory
'directory' => realpath(WP_PLUGIN_DIR).DIRECTORY_SEPARATOR.'all-in-one-wp-migration'.DIRECTORY_SEPARATOR.'storage',
'regex' => '/.+\.wpress$/is',
$this->altered_since = $altered_since;
$resumptions_since_last_successful = $updraftplus->current_resumption - $updraftplus->last_successful_resumption;
// false means 'tried + failed'; whereas 0 means 'not yet tried'
// Disallow binzip on OpenVZ when we're not sure there's plenty of memory
if (0 === $this->binzip && (!defined('UPDRAFTPLUS_PREFERPCLZIP') || !UPDRAFTPLUS_PREFERPCLZIP) && (!defined('UPDRAFTPLUS_NO_BINZIP') || !UPDRAFTPLUS_NO_BINZIP) && ($updraftplus->current_resumption < 9 || $resumptions_since_last_successful < 2)) {
if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$meminfo = @file_get_contents('/proc/meminfo', false, null, 0, 200);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
$memory_mb = $matches[1]/1024;
// If the report is of a large amount, then we're probably getting the total memory on the hypervisor (this has been observed), and don't really know the VPS's memory
$vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." MB";
if ($memory_mb < 1024 || $memory_mb > 8192) {
$vz_log .= " (will not use BinZip)";
$updraftplus->log($vz_log);
if (empty($openvz_lowmem)) {
$updraftplus->log('Checking if we have a zip executable available');
$binzip = $updraftplus->find_working_bin_zip();
if (is_string($binzip)) {
$updraftplus->log("Zip engine: found/will use a binary zip: $binzip");
$this->use_zip_object = 'UpdraftPlus_BinZip';
// In tests, PclZip was found to be 25% slower than ZipArchive
if ('UpdraftPlus_PclZip' != $this->use_zip_object && empty($this->binzip) && ((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
$updraftplus->log("Zip engine: ZipArchive (a.k.a. php-zip) is not available or is disabled (will use PclZip (much slower) if needed)");
$this->use_zip_object = 'UpdraftPlus_PclZip';
$this->zip_batch_ceiling = (defined('UPDRAFTPLUS_ZIP_BATCH_CEILING') && UPDRAFTPLUS_ZIP_BATCH_CEILING > 104857600) ? UPDRAFTPLUS_ZIP_BATCH_CEILING : 200 * 1048576;
add_filter('updraftplus_exclude_file', array($this, 'backup_exclude_file'), 10, 2);
* Get a site name suitable for use in the backup filename
private function get_site_name() {
// Get the blog name and rip out known-problematic characters. Remember that we may need to be able to upload this to any FTP server or cloud storage, where filename support may be unknown
$site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr(get_bloginfo(), 0, 32))));
if (!$site_name || preg_match('#^_+$#', $site_name)) {
$parsed_url = parse_url(home_url(), PHP_URL_HOST);
$parsed_subdir = untrailingslashit(parse_url(home_url(), PHP_URL_PATH));
if ($parsed_subdir && '/' != $parsed_subdir) $parsed_url .= str_replace(array('/', '\\'), '_', $parsed_subdir);
$site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr($parsed_url, 0, 32))));
if (!$site_name || preg_match('#^_+$#', $site_name)) $site_name = 'WordPress_Backup';
// Allow an over-ride. Careful about introducing characters not supported by your filesystem or cloud storage.
return apply_filters('updraftplus_blog_name', $site_name);
* Public, because called from the 'More Files' add-on
* @param String|Array $create_from_dir Directory/ies to create the zip
* @param String $whichone Entity being backed up (e.g. 'plugins', 'uploads')
* @param String $backup_file_basename Name of backup file
* @param Integer $index Index of zip in the sequence
* @param Integer|Boolean $first_linked_index First linked index in the sequence, or false
public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index, $first_linked_index = false) {
// Note: $create_from_dir can be an array or a string
if (function_exists('set_time_limit')) set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
$original_index = $index;
$this->first_linked_index = (false === $first_linked_index) ? 0 : $first_linked_index;
$this->whichone = $whichone;
$this->zip_split_every = max((int) $updraftplus->jobdata_get('split_every'), UPDRAFTPLUS_SPLIT_MIN)*1048576;
if ('others' != $whichone) $updraftplus->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576, 1)." MB)");
if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
$updraftplus->log("Does not exist: $create_from_dir");
if ('mu-plugins' == $whichone) {
if (!function_exists('get_mu_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
$mu_plugins = get_mu_plugins();
if (count($mu_plugins) == 0) {
$updraftplus->log("There appear to be no mu-plugins to backup. Will not raise an error.");
if ($flag_error) $updraftplus->log(sprintf(__("%s - could not back this entity up; the corresponding directory does not exist (%s)", 'updraftplus'), $whichone, $create_from_dir), 'error');
$itext = empty($index) ? '' : $index+1;
$base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
$full_path = $this->updraft_dir.'/'.$base_path;
// This is compatible with filenames which indicate increments, as it is looking only for the current increment
if (file_exists($full_path)) {
// Gather any further files that may also exist
$files_existing = array();
while (file_exists($full_path)) {
$files_existing[] = $base_path;
$time_mod = (int) @filemtime($full_path);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$updraftplus->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod, 1)." s)");
if ($time_mod > 100 && ($time_now - $time_mod) < 30) {
UpdraftPlus_Job_Scheduler::terminate_due_to_activity($base_path, $time_now, $time_mod);
// This is compatible with filenames which indicate increments, as it is looking only for the current increment
$base_path = $backup_file_basename.'-'.$whichone.($index+1).'.zip';
$full_path = $this->updraft_dir.'/'.$base_path;
// Temporary file, to be able to detect actual completion (upon which, it is renamed)
// Jun-13 - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 600);
// Firstly, make sure that the temporary file is not already being written to - which can happen if a resumption takes place whilst an old run is still active
$zip_name = $full_path.'.tmp';
$time_mod = file_exists($zip_name) ? filemtime($zip_name) : 0;
if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
UpdraftPlus_Job_Scheduler::terminate_due_to_activity($zip_name, $time_now, $time_mod);
if (file_exists($zip_name)) {
$updraftplus->log("File exists ($zip_name), but was apparently not modified within the last 30 seconds, so we assume that any previous run has now terminated (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod).")");
// Now, check for other forms of temporary file, which would indicate that some activity is going on (even if it hasn't made it into the main zip file yet)
// Note: this doesn't catch PclZip temporary files
$d = dir($this->updraft_dir);
$match = '_'.$updraftplus->file_nonce."-".$whichone;
while (false !== ($e = $d->read())) {
if ('.' == $e || '..' == $e || !is_file($this->updraft_dir.'/'.$e)) continue;
$ziparchive_match = preg_match("/$match(?:[0-9]*)\.zip\.tmp\.[A-Za-z0-9]+$/i", $e);
$binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
$pclzip_match = preg_match("/^pclzip-[a-z0-9]+.(?:gz|tmp)$/", $e);
if ($time_now-filemtime($this->updraft_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $updraftplus->current_resumption && ($binzip_match || $pclzip_match)))) {
UpdraftPlus_Job_Scheduler::terminate_due_to_activity($this->updraft_dir.'/'.$e, $time_now, filemtime($this->updraft_dir.'/'.$e));
@$d->close();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if (isset($files_existing)) {
// Because of zip-splitting, the mere fact that files exist is not enough to indicate that the entity is finished. For that, we need to also see that no subsequent file has been started.
// Q. What if the previous runner died in between zips, and it is our job to start the next one? A. The next temporary file is created before finishing the former zip, so we are safe (and we are also safe-guarded by the updated value of the index being stored in the database).
$this->log_account_space();
$this->zip_microtime_start = microtime(true);
// The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
$zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
$updraftplus->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
$updraftplus->log(sprintf(__("Could not create %s zip. Consult the log file for more information.", 'updraftplus'), $whichone), 'error');
// The caller is required to update $index from $this->index
$itext = empty($this->index) ? '' : $this->index+1;
$full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
if (file_exists($full_path.'.tmp')) {
if (@filesize($full_path.'.tmp') === 0) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
@unlink($full_path.'.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$checksum_description = '';
$checksums = $updraftplus->which_checksums();
foreach ($checksums as $checksum) {
$cksum = hash_file($checksum, $full_path.'.tmp');
$updraftplus->jobdata_set($checksum.'-'.$whichone.$this->index, $cksum);
if ($checksum_description) $checksum_description .= ', ';
$checksum_description .= "$checksum: $cksum";
@rename($full_path.'.tmp', $full_path);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
$kbsize = filesize($full_path)/1024;
$rate = round($kbsize/$timetaken, 1);
$updraftplus->log("Created $whichone zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) ($checksum_description)");
// We can now remove any left-over temporary files from this job
} elseif ($this->index > $original_index) {
$updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed (2)");
// Added 12-Feb-2014 (to help multiple morefiles)
$updraftplus->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0);
// Remove cache list files as well, if there are any
UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0, true);
// Create the results array to send back (just the new ones, not any prior ones)
$files_existing = array();
$res_index = $original_index;
for ($i = $original_index; $i<= $this->index; $i++) {
$itext = empty($i) ? '' : ($i+1);
$full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
if (file_exists($full_path)) {
$files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
* This method is for calling outside of a cloud_backup() context. It constructs a list of services for which prune operations should be attempted, and then calls prune_retained_backups() if necessary upon them.
public function do_prune_standalone() {
$services = (array) $updraftplus->just_one($updraftplus->jobdata_get('service'));
$prune_services = array();
foreach ($services as $service) {
if ('none' === $service || '' == $service) continue;
$objname = "UpdraftPlus_BackupModule_${service}";
if (!class_exists($objname) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php')) {
include_once(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php');
if (class_exists($objname)) {
$remote_obj = new $objname;
$prune_services[$service]['all'] = array($remote_obj, null);
$updraftplus->log("Could not prune from service $service: remote method not found");
if (!empty($prune_services)) $this->prune_retained_backups($prune_services);
* Dispatch to the relevant function
* @param Array $backup_array List of archives for the backup
public function cloud_backup($backup_array) {
$services = (array) $updraftplus->just_one($updraftplus->jobdata_get('service'));
$remote_storage_instances = $updraftplus->jobdata_get('remote_storage_instances', array());
// We need to make sure that the loop below actually runs
if (empty($services)) $services = array('none');
$storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($services, $remote_storage_instances);
$total_instances_count = 0;
foreach ($storage_objects_and_ids as $service) {
if ($service['object']->supports_feature('multi_options')) $total_instances_count += count($service['instance_settings']);
$updraftplus->jobdata_set('jobstatus', 'clouduploading');
$updraftplus->register_wp_http_option_hooks();
$upload_status = $updraftplus->jobdata_get('uploading_substatus');
if (!is_array($upload_status) || !isset($upload_status['t'])) {
$upload_status = array('i' => 0, 'p' => 0, 't' => max(1, $total_instances_count)*count($backup_array));
$updraftplus->jobdata_set('uploading_substatus', $upload_status);
// If there was no check-in last time, then attempt a different service first - in case a time-out on the attempted service leads to no activity and everything stopping
if (count($services) >1 && $updraftplus->no_checkin_last_time) {
$updraftplus->log('No check-in last time: will try a different remote service first');
array_push($services, array_shift($services));
// Make sure that the 'no worthwhile activity' detector isn't flumoxed by the starting of a new upload at 0%
if ($updraftplus->current_resumption > 9) $updraftplus->jobdata_set('uploaded_lastreset', $updraftplus->current_resumption);
if (1 == ($updraftplus->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
$errors_before_uploads = $updraftplus->error_count();
foreach ($services as $ind => $service) {
$total_instance_ids = ('none' !== $service && '' !== $service && $storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) ? count($storage_objects_and_ids[$service]['instance_settings']) : 1;
// Used for logging by record_upload_chunk()
$this->current_service = $service;
// Used when deciding whether to delete the local file
$this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
$log_extra = $this->last_storage_instance ? ' (last)' : '';
$updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if ('none' == $service || '' == $service) {
$updraftplus->log('No remote despatch: user chose no remote backup service');
// Still want to mark as "uploaded", to signal that nothing more needs doing. (Important on incremental runs with no cloud storage).
foreach ($backup_array as $file) {
if ($updraftplus->is_uploaded($file)) {
$updraftplus->log("Already uploaded: $file");
$updraftplus->uploaded_file($file, true);
$fullpath = $this->updraft_dir.'/'.$file;
if (file_exists($fullpath.'.list.tmp')) {
$updraftplus->log("Deleting zip manifest ({$file}.list.tmp)");
unlink($fullpath.'.list.tmp');
$this->prune_retained_backups(array('none' => array('all' => array(null, null))));
} elseif (!empty($storage_objects_and_ids[$service]['object']) && !$storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) {
$remote_obj = $storage_objects_and_ids[$service]['object'];
$do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, ''));
} elseif (!empty($storage_objects_and_ids[$service]['instance_settings'])) {
foreach ($storage_objects_and_ids[$service]['instance_settings'] as $instance_id => $options) {
if ($instance_id_count > 0) {
$this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
$log_extra = $this->last_storage_instance ? ' (last)' : '';
$updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
// Used for logging by record_upload_chunk()
$this->current_instance = $instance_id;
if (!isset($options['instance_enabled'])) $options['instance_enabled'] = 1;
// if $remote_storage_instances is not empty then we are looping over a list of instances the user wants to backup to so we want to ignore if the instance is enabled or not
if (1 == $options['instance_enabled'] || !empty($remote_storage_instances)) {
$remote_obj = $storage_objects_and_ids[$service]['object'];
$remote_obj->set_options($options, true, $instance_id);
$do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, $instance_id));
$updraftplus->log("This instance id ($instance_id) is set as inactive.");