* @see backup::run_backup() to run a single backup * @see backup::run_all_backups() to run all backups * @see backup::restoreBackupDatabase() to restore a database * @see backup::restoreBackupWebFiles() to restore web files */ class backup { /** * Returns file extension for specified backup format * @param string $format backup format * @return string|null * @author Ramil Valitov */ protected static function getBackupDbExtension($format) { $prefix = '.sql'; switch ($format) { case 'gzip': return $prefix . '.gz'; case 'bzip2': return $prefix . '.bz2'; case 'xz': return $prefix . '.xz'; case 'zip': case 'zip_bzip2': return '.zip'; case 'rar': return '.rar'; } if (strpos($format, "7z_") === 0) { return $prefix . '.7z'; } return null; } /** * Returns file extension for specified backup format * @param string $format backup format * @return string|null * @author Ramil Valitov */ protected static function getBackupWebExtension($format) { switch ($format) { case 'tar_gzip': return '.tar.gz'; case 'tar_bzip2': return '.tar.bz2'; case 'tar_xz': return '.tar.xz'; case 'zip': case 'zip_bzip2': return '.zip'; case 'rar': return '.rar'; } if (strpos($format, "tar_7z_") === 0) { return '.tar.7z'; } return null; } /** * Sets file ownership to $web_user for all files and folders except log, ssl and web/stats * @param string $web_document_root * @param string $web_user * @author Ramil Valitov */ protected static function restoreFileOwnership($web_document_root, $web_user, $web_group) { global $app; $blacklist = array('bin', 'dev', 'etc', 'home', 'lib', 'lib32', 'lib64', 'log', 'opt', 'proc', 'net', 'run', 'sbin', 'ssl', 'srv', 'sys', 'usr', 'var'); $find_excludes = '-not -path "." -and -not -path "./web/stats/*"'; foreach ( $blacklist as $dir ) { $find_excludes .= ' -and -not -path "./'.$dir.'" -and -not -path "./'.$dir.'/*"'; } $app->log('Restoring permissions for ' . $web_document_root, LOGLEVEL_DEBUG); $app->system->exec_safe('cd ? && find . '.$find_excludes.' -exec chown ?:? {} \;', $web_document_root, $web_user, $web_group); } /** * Returns default backup format used in previous versions of ISPConfig * @param string $backup_mode can be 'userzip' or 'rootgz' * @param string $backup_type can be 'web' or 'mysql' * @return string * @author Ramil Valitov */ protected static function getDefaultBackupFormat($backup_mode, $backup_type) { //We have a backup from old version of ISPConfig switch ($backup_type) { case 'mysql': return 'gzip'; case 'web': return ($backup_mode == 'userzip') ? 'zip' : 'tar_gzip'; } return ""; } /** * Restores a database backup. * The backup directory must be mounted before calling this method. * @param string $backup_format * @param string $password password for encrypted backup or empty string if archive is not encrypted * @param string $backup_dir * @param string $filename * @param string $backup_mode * @param string $backup_type * @return bool true if succeeded * @see backup_plugin::mount_backup_dir() * @author Ramil Valitov */ public static function restoreBackupDatabase($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type) { global $app; //* Added for compatibility with repository type backups if (self::formatIsRepos($backup_format, 'mysql')) { return self::restoreBackupDatabaseRepos($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type); } //* END //* Load sql dump into db include 'lib/mysql_clientdb.conf'; if (empty($backup_format)) { $backup_format = self::getDefaultBackupFormat($backup_mode, $backup_type); } $extension = self::getBackupDbExtension($backup_format); if (!empty($extension)) { //Replace dots for preg_match search $extension = str_replace('.', '\.', $extension); } $success = false; $full_filename = $backup_dir . '/' . $filename; $app->log('Restoring MySQL backup ' . $full_filename . ', backup format "' . $backup_format . '", backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG); if (file_exists($full_filename) && !empty($extension)) { preg_match('@^(manual-)?db_(?P.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}' . $extension . '$@', $filename, $matches); if (!isset($matches['db']) || empty($matches['db'])) { $app->log('Failed to detect database name during restore of ' . $full_filename, LOGLEVEL_ERROR); return false; } $db_name = $matches['db']; switch ($backup_format) { case "gzip": $command = "gunzip --stdout ? | mysql -h ? -u ? -p? ?"; break; case "zip": case "zip_bzip2": $command = "unzip -qq -p -P " . escapeshellarg($password) . " ? | mysql -h ? -u ? -p? ?"; break; case "bzip2": $command = "bunzip2 -q -c ? | mysql -h ? -u ? -p? ?"; break; case "xz": $command = "unxz -q -q -c ? | mysql -h ? -u ? -p? ?"; break; case "rar": //First, test that the archive is correct and we have a correct password $options = self::getUnrarOptions($password); $app->system->exec_safe("rar t " . $options . " ?", $full_filename); if ($app->system->last_exec_retcode() == 0) { $app->log('Archive test passed for ' . $full_filename, LOGLEVEL_DEBUG); $command = "rar x " . $options. " ? | mysql -h ? -u ? -p? ?"; } break; } if (strpos($backup_format, "7z_") === 0) { $options = self::get7zDecompressOptions($password); //First, test that the archive is correct and we have a correct password $app->system->exec_safe("7z t " . $options . " ?", $full_filename); if ($app->system->last_exec_retcode() == 0) { $app->log('Archive test passed for ' . $full_filename, LOGLEVEL_DEBUG); $command = "7z x " . $options . " -so ? | mysql -h ? -u ? -p? ?"; } else $command = null; } if (!empty($command)) { /** @var string $clientdb_host */ /** @var string $clientdb_user */ /** @var string $clientdb_password */ $app->system->exec_safe($command, $full_filename, $clientdb_host, $clientdb_user, $clientdb_password, $db_name); $retval = $app->system->last_exec_retcode(); if ($retval == 0) { $app->log('Restored MySQL backup ' . $full_filename, LOGLEVEL_DEBUG); $success = true; } else { $app->log('Failed to restore web backup ' . $full_filename . ', exit code ' . $retval, LOGLEVEL_ERROR); } } else { $app->log('Archive test failed for ' . $full_filename, LOGLEVEL_DEBUG); } } else { $app->log('Failed to process MySQL backup ' . $full_filename, LOGLEVEL_ERROR); } unset($clientdb_host); unset($clientdb_user); unset($clientdb_password); return $success; } /** * Restores web files backup. * The backup directory must be mounted before calling this method. * @param string $backup_format * @param string $password password for encrypted backup or empty string if archive is not encrypted * @param string $backup_dir * @param string $filename * @param string $backup_mode * @param string $backup_type * @param string $web_root * @param string $web_user * @param string $web_group * @return bool true if succeed * @see backup_plugin::mount_backup_dir() * @author Ramil Valitov */ public static function restoreBackupWebFiles($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type, $web_root, $web_user, $web_group) { global $app; //* Added for compatibility with repository type backups if (self::formatIsRepos($backup_format, 'web')) { return self::restoreBackupWebFilesRepos($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type, $web_root, $web_user, $web_group); } //* END if (empty($backup_format)) { $backup_format = self::getDefaultBackupFormat($backup_mode, $backup_type); } $full_filename = $backup_dir . '/' . $filename; $result = false; $app->log('Restoring web backup ' . $full_filename . ', backup format "' . $backup_format . '", backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG); if (!empty($backup_format)) { $app->system->web_folder_protection($web_root, false); if ($backup_mode == 'userzip' || $backup_mode == 'rootgz') { $user_mode = $backup_mode == 'userzip'; $filename = $user_mode ? ($web_root . '/backup/' . $filename) : $full_filename; if (file_exists($full_filename) && $web_root != '' && $web_root != '/' && !stristr($full_filename, '..') && !stristr($full_filename, 'etc')) { if ($user_mode) { if (file_exists($filename)) rename($filename, $filename . '.bak'); copy($full_filename, $filename); chgrp($filename, $web_group); } $user_prefix_cmd = $user_mode ? 'sudo -u ' . escapeshellarg($web_user) : ''; $success = false; $retval = 0; switch ($backup_format) { case "tar_gzip": case "tar_bzip2": case "tar_xz": $command = $user_prefix_cmd . ' tar xf ? --directory ?'; $app->system->exec_safe($command, $filename, $web_root); $retval = $app->system->last_exec_retcode(); $success = ($retval == 0 || $retval == 2); break; case "zip": case "zip_bzip2": $command = $user_prefix_cmd . ' unzip -qq -P ' . escapeshellarg($password) . ' -o ? -d ? 2> /dev/null'; $app->system->exec_safe($command, $filename, $web_root); $retval = $app->system->last_exec_retcode(); /* * Exit code 50 can happen when zip fails to overwrite files that do not * belong to selected user, so we can consider this situation as success * with warnings. */ $success = ($retval == 0 || $retval == 50); if ($success) { self::restoreFileOwnership($web_root, $web_user, $web_group); } break; case 'rar': $options = self::getUnRarOptions($password); //First, test that the archive is correct and we have a correct password $command = $user_prefix_cmd . " rar t " . $options . " ? ?"; //Rar requires trailing slash $app->system->exec_safe($command, $filename, $web_root . '/'); $success = ($app->system->last_exec_retcode() == 0); if ($success) { //All good, now we can extract $app->log('Archive test passed for ' . $full_filename, LOGLEVEL_DEBUG); $command = $user_prefix_cmd . " rar x " . $options . " ? ?"; //Rar requires trailing slash $app->system->exec_safe($command, $filename, $web_root . '/'); $retval = $app->system->last_exec_retcode(); //Exit code 9 can happen when we have file permission errors, in this case some //files will be skipped during extraction. $success = ($retval == 0 || $retval == 1 || $retval == 9); } else { $app->log('Archive test failed for ' . $full_filename, LOGLEVEL_DEBUG); } break; } if (strpos($backup_format, "tar_7z_") === 0) { $options = self::get7zDecompressOptions($password); //First, test that the archive is correct and we have a correct password $command = $user_prefix_cmd . " 7z t " . $options . " ?"; $app->system->exec_safe($command, $filename); $success = ($app->system->last_exec_retcode() == 0); if ($success) { //All good, now we can extract $app->log('Archive test passed for ' . $full_filename, LOGLEVEL_DEBUG); $command = $user_prefix_cmd . " 7z x " . $options . " -so ? | tar xf - --directory ?"; $app->system->exec_safe($command, $filename, $web_root); $retval = $app->system->last_exec_retcode(); $success = ($retval == 0 || $retval == 2); } else { $app->log('Archive test failed for ' . $full_filename, LOGLEVEL_DEBUG); } } if ($user_mode) { unlink($filename); if (file_exists($filename . '.bak')) rename($filename . '.bak', $filename); } if ($success) { $app->log('Restored web backup ' . $full_filename, LOGLEVEL_DEBUG); $result = true; } else { $app->log('Failed to restore web backup ' . $full_filename . ', exit code ' . $retval, LOGLEVEL_ERROR); } } } else { $app->log('Failed to restore web backup ' . $full_filename . ', backup mode "' . $backup_mode . '" not recognized.', LOGLEVEL_DEBUG); } $app->system->web_folder_protection($web_root, true); } else { $app->log('Failed to restore web backup ' . $full_filename . ', backup format not recognized.', LOGLEVEL_DEBUG); } return $result; } /** * Returns a compression method, for example returns bzip2 for tar_7z_bzip2 * @param string $format * @return false|string * @author Ramil Valitov */ protected static function getCompressionMethod($format) { $pos = strrpos($format, "_"); return substr($format, $pos + 1); } /** * Returns default options for compressing rar * @param string $backup_tmp temporary directory that rar can use * @param string|null $password backup password if any * @return string options for rar */ protected static function getRarOptions($backup_tmp, $password) { /** * All rar options are listed here: * https://documentation.help/WinRAR/HELPCommands.htm * https://documentation.help/WinRAR/HELPSwitches.htm * Some compression profiles and different versions of rar may use different default values, so it's better * to specify everything explicitly. * The difference between compression methods is not big in terms of file size, but is huge in terms of * CPU and RAM consumption. Therefore it makes sense only to use fastest the compression method. */ $options = array( /** * Start with fastest compression method (least compressive) */ '-m1', /** * Disable solid archiving. * Never use solid archive: it's very slow and requires to read and sort all files first */ '-S-', /** * Ignore default profile and environment variables * https://documentation.help/WinRAR/HELPSwCFGm.htm */ '-CFG-', /** * Disable error messages output * https://documentation.help/WinRAR/HELPSwINUL.htm */ '-inul', /** * Lock archive: this switch prevents any further archive modifications by rar * https://documentation.help/WinRAR/HELPSwK.htm */ '-k', /** * Create archive in RAR 5.0 format * https://documentation.help/WinRAR/HELPSwMA.htm */ '-ma', /** * Set dictionary size to 16Mb. * When archiving, rar needs about 6x memory of specified dictionary size. * https://documentation.help/WinRAR/HELPSwMD.htm */ '-md16m', /** * Use only one CPU thread * https://documentation.help/WinRAR/HELPSwMT.htm */ '-mt1', /** * Use this switch when archiving to save file security information and when extracting to restore it. * It stores file owner, group, file permissions and audit information. * https://documentation.help/WinRAR/HELPSwOW.htm */ '-ow', /** * Overwrite all * https://documentation.help/WinRAR/HELPSwO.htm */ '-o+', /** * Exclude base folder from names. * Required for correct directory structure inside archive * https://documentation.help/WinRAR/HELPSwEP1.htm */ '-ep1', /** * Never add quick open information. * This information is useful only if you want to read the contents of archive (list of files). * Besides it can increase the archive size. As we need the archive only for future complete extraction, * there's no need to use this information at all. * https://documentation.help/WinRAR/HELPSwQO.htm */ '-qo-', /** * Set lowest task priority (1) and 10ms sleep time between read/write operations. * https://documentation.help/WinRAR/HELPSwRI.htm */ '-ri1:10', /** * Temporary folder * https://documentation.help/WinRAR/HELPSwW.htm */ '-w' . escapeshellarg($backup_tmp), /** * Assume Yes on all queries * https://documentation.help/WinRAR/HELPSwY.htm */ '-y', ); $options = implode(" ", $options); if (!empty($password)) { /** * Encrypt both file data and headers * https://documentation.help/WinRAR/HELPSwHP.htm */ $options .= ' -HP' . escapeshellarg($password); } return $options; } /** * Returns default options for decompressing rar * @param string|null $password backup password if any * @return string options for rar */ protected static function getUnRarOptions($password) { /** * All rar options are listed here: * https://documentation.help/WinRAR/HELPCommands.htm * https://documentation.help/WinRAR/HELPSwitches.htm * Some compression profiles and different versions of rar may use different default values, so it's better * to specify everything explicitly. * The difference between compression methods is not big in terms of file size, but is huge in terms of * CPU and RAM consumption. Therefore it makes sense only to use fastest the compression method. */ $options = array( /** * Ignore default profile and environment variables * https://documentation.help/WinRAR/HELPSwCFGm.htm */ '-CFG-', /** * Disable error messages output * https://documentation.help/WinRAR/HELPSwINUL.htm */ '-inul', /** * Use only one CPU thread * https://documentation.help/WinRAR/HELPSwMT.htm */ '-mt1', /** * Use this switch when archiving to save file security information and when extracting to restore it. * It stores file owner, group, file permissions and audit information. * https://documentation.help/WinRAR/HELPSwOW.htm */ '-ow', /** * Overwrite all * https://documentation.help/WinRAR/HELPSwO.htm */ '-o+', /** * Set lowest task priority (1) and 10ms sleep time between read/write operations. * https://documentation.help/WinRAR/HELPSwRI.htm */ '-ri1:10', /** * Assume Yes on all queries * https://documentation.help/WinRAR/HELPSwY.htm */ '-y', ); $options = implode(" ", $options); if (!empty($password)) { $options .= ' -P' . escapeshellarg($password); } return $options; } /** * Returns compression options for 7z * @param string $format compression format used in 7z * @param string $password password if any * @return string */ protected static function get7zCompressOptions($format, $password) { $method = self::getCompressionMethod($format); /** * List of 7z options is here: * https://linux.die.net/man/1/7z * https://sevenzip.osdn.jp/chm/cmdline/syntax.htm * https://sevenzip.osdn.jp/chm/cmdline/switches/ */ $options = array( /** * Use 7z format (container) */ '-t7z', /** * Compression method (LZMA, LZMA2, etc.) * https://sevenzip.osdn.jp/chm/cmdline/switches/method.htm */ '-m0=' . $method, /** * Fastest compression method */ '-mx=1', /** * Disable solid mode */ '-ms=off', /** * Disable multithread mode, use less CPU */ '-mmt=off', /** * Disable multithread mode for filters, use less CPU */ '-mmtf=off', /** * Disable progress indicator */ '-bd', /** * Assume yes on all queries * https://sevenzip.osdn.jp/chm/cmdline/switches/yes.htm */ '-y', ); $options = implode(" ", $options); switch (strtoupper($method)) { case 'LZMA': case 'LZMA2': /** * Dictionary size is 5Mb. * 7z can use 12 times more RAM */ $options .= ' -md=5m'; break; case 'PPMD': /** * Dictionary size is 64Mb. * It's the maximum RAM that 7z is allowed to use. */ $options .= ' -mmem=64m'; break; } if (!empty($password)) { $options .= ' -mhe=on -p' . escapeshellarg($password); } return $options; } /** * Returns decompression options for 7z * @param string $password password if any * @return string */ protected static function get7zDecompressOptions($password) { /** * List of 7z options is here: * https://linux.die.net/man/1/7z * https://sevenzip.osdn.jp/chm/cmdline/syntax.htm * https://sevenzip.osdn.jp/chm/cmdline/switches/ */ $options = array( /** * Disable multithread mode, use less CPU */ '-mmt=off', /** * Disable progress indicator */ '-bd', /** * Assume yes on all queries * https://sevenzip.osdn.jp/chm/cmdline/switches/yes.htm */ '-y', ); $options = implode(" ", $options); if (!empty($password)) { $options .= ' -p' . escapeshellarg($password); } return $options; } /** * Clears expired backups. * The backup directory must be mounted before calling this method. * @param integer $server_id * @param integer $web_id id of the website * @param integer $max_backup_copies number of backup copies to keep, all files beyond the limit will be erased * @param string $backup_dir directory to scan * @return bool * @see backup_plugin::backups_garbage_collection() call this method first * @see backup_plugin::mount_backup_dir() * @author Ramil Valitov */ protected static function clearBackups($server_id, $web_id, $max_backup_copies, $backup_dir, $prefix_list=null) { global $app; $files = self::get_files($backup_dir, $prefix_list); usort($files, function ($a, $b) use ($backup_dir) { $time_a = filemtime($backup_dir . '/' . $a); $time_b = filemtime($backup_dir . '/' . $b); return ($time_a > $time_b) ? -1 : 1; }); $db_list = array($app->db); if ($app->db->dbHost != $app->dbmaster->dbHost) array_push($db_list, $app->dbmaster); //Delete old files that are beyond the limit for ($n = $max_backup_copies; $n < sizeof($files); $n++) { $filename = $files[$n]; $full_filename = $backup_dir . '/' . $filename; $app->log('Backup file ' . $full_filename . ' is beyond the limit of ' . $max_backup_copies . " copies and will be deleted from disk and database", LOGLEVEL_DEBUG); $sql = "DELETE FROM web_backup WHERE server_id = ? AND parent_domain_id = ? AND filename = ?"; foreach ($db_list as $db) { $db->query($sql, $server_id, $web_id, $filename); } @unlink($full_filename); } return true; } /** * Garbage collection: deletes records from database about files that do not exist and deletes untracked files and cleans up backup download directories. * The backup directory must be mounted before calling this method. * @param int $server_id * @param string|null $backup_type if defined then process only backups of this type * @param string|null $domain_id if defined then process only backups that belong to this domain * @author Ramil Valitov * @see backup_plugin::mount_backup_dir() */ protected static function backups_garbage_collection($server_id, $backup_type = null, $domain_id = null) { global $app; //First check that all records in database have related files and delete records without files on disk $args_sql = array(); $args_sql_domains = array(); $args_sql_domains_with_backups = array(); $server_config = $app->getconf->get_server_config($server_id, 'server'); $backup_dir = trim($server_config['backup_dir']); $sql = "SELECT * FROM web_backup WHERE server_id = ?"; $sql_domains = "SELECT domain_id,document_root,system_user,system_group,backup_interval FROM web_domain WHERE server_id = ? AND (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias')"; $sql_domains_with_backups = "SELECT domain_id,document_root,system_user,system_group,backup_interval FROM web_domain WHERE domain_id in (SELECT parent_domain_id FROM web_backup WHERE server_id = ?" . ((!empty($backup_type)) ? " AND backup_type = ?" : "") . ") AND (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias')"; array_push($args_sql, $server_id); array_push($args_sql_domains, $server_id); array_push($args_sql_domains_with_backups, $server_id); if (!empty($backup_type)) { $sql .= " AND backup_type = ?"; array_push($args_sql, $backup_type); array_push($args_sql_domains_with_backups, $backup_type); } if (!empty($domain_id)) { $sql .= " AND parent_domain_id = ?"; $sql_domains .= " AND domain_id = ?"; $sql_domains_with_backups .= " AND domain_id = ?"; array_push($args_sql, $domain_id); array_push($args_sql_domains, $domain_id); array_push($args_sql_domains_with_backups, $domain_id); } $db_list = array($app->db); if ($app->db->dbHost != $app->dbmaster->dbHost) array_push($db_list, $app->dbmaster); // Cleanup web_backup entries for non-existent backup files foreach ($db_list as $db) { $backups = $app->db->queryAllRecords($sql, true, $args_sql); foreach ($backups as $backup) { /** Addition to support repository formats */ if (self::formatIsRepos($backup['backup_format_web'], 'web')) { //@todo im too lazy continue; } /** END */ $backup_file = $backup_dir . '/web' . $backup['parent_domain_id'] . '/' . $backup['filename']; if (!is_file($backup_file)) { $app->log('Backup file ' . $backup_file . ' does not exist on disk, deleting this entry from database', LOGLEVEL_DEBUG); $sql = "DELETE FROM web_backup WHERE backup_id = ?"; $db->query($sql, $backup['backup_id']); } } } // Cleanup backup files with missing web_backup entries (runs on all servers) $domains = $app->dbmaster->queryAllRecords($sql_domains_with_backups, true, $args_sql_domains_with_backups); foreach ($domains as $rec) { $domain_id = $rec['domain_id']; $domain_backup_dir = $backup_dir . '/web' . $domain_id; $files = self::get_files($domain_backup_dir); if (!empty($files)) { // leave out server_id here, in case backup storage is shared between servers $sql = "SELECT backup_id, filename FROM web_backup WHERE parent_domain_id = ?"; $untracked_backup_files = array(); foreach ($db_list as $db) { $backups = $db->queryAllRecords($sql, $domain_id); foreach ($backups as $backup) { /** Addition to support repository formats */ if (self::formatIsRepos($backup['backup_format_web'], 'web')) { //@todo im too lazy continue; } /** END */ if (!in_array($backup['filename'],$files)) { $untracked_backup_files[] = $backup['filename']; } } } array_unique( $untracked_backup_files ); foreach ($untracked_backup_files as $f) { $backup_file = $backup_dir . '/web' . $domain_id . '/' . $f; $app->log('Backup file ' . $backup_file . ' is not contained in database, deleting this file from disk', LOGLEVEL_DEBUG); @unlink($backup_file); } } } // This cleanup only runs on web servers $domains = $app->db->queryAllRecords($sql_domains, true, $args_sql_domains); foreach ($domains as $rec) { $domain_id = $rec['domain_id']; $domain_backup_dir = $backup_dir . '/web' . $domain_id; // Remove backupdir symlink and create as directory instead if (is_link($backup_download_dir) || !is_dir($backup_download_dir)) { $web_path = $rec['document_root']; $app->system->web_folder_protection($web_path, false); $backup_download_dir = $web_path . '/backup'; if (is_link($backup_download_dir)) { unlink($backup_download_dir); } if (!is_dir($backup_download_dir)) { mkdir($backup_download_dir); chown($backup_download_dir, $rec['system_user']); chgrp($backup_download_dir, $rec['system_group']); } $app->system->web_folder_protection($web_path, true); } // delete old files from backup download dir (/var/www/example.com/backup) if (is_dir($backup_download_dir)) { $dir_handle = dir($backup_download_dir); $now = time(); while (false !== ($entry = $dir_handle->read())) { $full_filename = $backup_download_dir . '/' . $entry; if ($entry != '.' && $entry != '..' && is_file($full_filename) && ! is_link($full_filename)) { // delete files older than 3 days if ($now - filemtime($full_filename) >= 60 * 60 * 24 * 3) { $app->log('Backup file ' . $full_filename . ' is too old, deleting this file from disk', LOGLEVEL_DEBUG); @unlink($full_filename); } } } $dir_handle->close(); } } } /** * Gets list of files in directory * @param string $directory * @param string[]|null $prefix_list filter files that have one of the prefixes. Use null for default filtering. * @param string[]|null $endings_list filter files that have one of the endings. Use null for default filtering. * @return string[] * @author Ramil Valitov */ protected static function get_files($directory, $prefix_list = null, $endings_list = null) { $default_prefix_list = array( 'web', 'manual-web', 'db_', 'manual-db_', ); $default_endings_list = array( '.gz', '.7z', '.rar', '.zip', '.xz', '.bz2', ); if (is_null($prefix_list)) $prefix_list = $default_prefix_list; if (is_null($endings_list)) $endings_list = $default_endings_list; if (!is_dir($directory)) { return array(); } $dir_handle = dir($directory); $files = array(); while (false !== ($entry = $dir_handle->read())) { $full_filename = $directory . '/' . $entry; if ($entry != '.' && $entry != '..' && is_file($full_filename)) { if (!empty($prefix_list)) { $add = false; foreach ($prefix_list as $prefix) { if (substr($entry, 0, strlen($prefix)) == $prefix) { $add = true; break; } } } else $add = true; if ($add && !empty($endings_list)) { $add = false; foreach ($endings_list as $ending) { if (substr($entry, -strlen($ending)) == $ending) { $add = true; break; } } } if ($add) array_push($files, $entry); } } $dir_handle->close(); return $files; } /** * Generates excludes list for compressors * @param string[] $backup_excludes * @param string $arg * @param string $pre * @param string $post * @return string * @author Ramil Valitov */ protected static function generateExcludeList($backup_excludes, $arg, $pre='', $post='') { $excludes = ""; foreach ($backup_excludes as $ex) { # pass through escapeshellarg if not already done if ( preg_match( "/^'.+'$/", $ex ) ) { $excludes .= "${arg}${pre}${ex}${post} "; } else { $excludes .= "${arg}" . escapeshellarg("${pre}${ex}${post}") . " "; } } return trim( $excludes ); } /** * Runs a web compression routine * @param string $format * @param string[] $backup_excludes * @param string $backup_mode * @param string $web_path * @param string $web_backup_dir * @param string $web_backup_file * @param string $web_user * @param string $web_group * @param string $http_server_user * @param string $backup_tmp * @param string|null $password * @return bool true if success * @author Ramil Valitov */ protected static function runWebCompression($format, $backup_excludes, $backup_mode, $web_path, $web_backup_dir, $web_backup_file, $web_user, $web_group, $http_server_user, $backup_tmp, $password) { global $app; $find_user_files = 'cd ? && sudo -u ? find . -group ? -or -user ? -print 2> /dev/null'; $excludes = self::generateExcludeList($backup_excludes, '--exclude='); $tar_dir = 'tar pcf - ' . $excludes . ' --directory ? .'; $tar_input = 'tar pcf --null -T -'; $app->log('Performing web files backup of ' . $web_path . ' in format ' . $format . ', mode ' . $backup_mode, LOGLEVEL_DEBUG); switch ($format) { case 'tar_gzip': if ($app->system->is_installed('pigz')) { //use pigz if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | ' . $tar_input . ' | pigz > ?', $web_path, $web_user, $web_group, $http_server_user, $web_path, $web_backup_dir . '/' . $web_backup_file); } else { //Standard casual behaviour of ISPConfig $app->system->exec_safe($tar_dir . ' | pigz > ?', $web_path, $web_backup_dir . '/' . $web_backup_file); } $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; } else { //use gzip if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | tar pczf ? --null -T -', $web_path, $web_user, $web_group, $http_server_user, $web_backup_dir . '/' . $web_backup_file); } else { //Standard casual behaviour of ISPConfig $app->system->exec_safe('tar pczf ? ' . $excludes . ' --directory ? .', $web_backup_dir . '/' . $web_backup_file, $web_path); } $exit_code = $app->system->last_exec_retcode(); // tar can return 1 and still create valid backups return ($exit_code == 0 || $exit_code == 1); } case 'zip': case 'zip_bzip2': $zip_options = ($format === 'zip_bzip2') ? ' -Z bzip2 ' : ''; if (!empty($password)) { $zip_options .= ' --password ' . escapeshellarg($password); } $excludes = self::generateExcludeList($backup_excludes, '-x '); $excludes .= " " . self::generateExcludeList($backup_excludes, '-x ', '', '/*'); if ($backup_mode == 'user_zip') { //Standard casual behaviour of ISPConfig $app->system->exec_safe($find_user_files . ' | zip ' . $zip_options . ' -b ? --symlinks ? -@ ' . $excludes, $web_path, $web_user, $web_group, $http_server_user, $backup_tmp, $web_backup_dir . '/' . $web_backup_file); } else { //Use cd to have a correct directory structure inside the archive, zip current directory "." to include hidden (dot) files $app->system->exec_safe('cd ? && zip ' . $zip_options . ' -b ? --symlinks -r ? . ' . $excludes, $web_path, $backup_tmp, $web_backup_dir . '/' . $web_backup_file); } $exit_code = $app->system->last_exec_retcode(); // zip can return 12(due to harmless warnings) and still create valid backups return ($exit_code == 0 || $exit_code == 12); case 'tar_bzip2': if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | tar pcjf ? --null -T -', $web_path, $web_user, $web_group, $http_server_user, $web_backup_dir . '/' . $web_backup_file); } else { $app->system->exec_safe('tar pcjf ? ' . $excludes . ' --directory ? .', $web_backup_dir . '/' . $web_backup_file, $web_path); } $exit_code = $app->system->last_exec_retcode(); // tar can return 1 and still create valid backups return ($exit_code == 0 || $exit_code == 1); case 'tar_xz': if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | tar pcJf ? --null -T -', $web_path, $web_user, $web_group, $http_server_user, $web_backup_dir . '/' . $web_backup_file); } else { $app->system->exec_safe('tar pcJf ? ' . $excludes . ' --directory ? .', $web_backup_dir . '/' . $web_backup_file, $web_path); } $exit_code = $app->system->last_exec_retcode(); // tar can return 1 and still create valid backups return ($exit_code == 0 || $exit_code == 1); case 'rar': $options = self::getRarOptions($backup_tmp,$password); if ($backup_mode != 'user_zip') { //Recurse subfolders, otherwise we will pass a list of files to compress $options .= ' -r'; } $excludes = self::generateExcludeList($backup_excludes, '-x'); $zip_command = 'rar a ' . $options . ' '.$excludes.' ?'; if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | ' . $zip_command . ' ? @', $web_path, $web_user, $web_group, $http_server_user, $web_path, $web_backup_dir . '/' . $web_backup_file); } else { $app->system->exec_safe('cd ? && ' . $zip_command . ' .', $web_path, $web_backup_dir . '/' . $web_backup_file); } $exit_code = $app->system->last_exec_retcode(); return ($exit_code == 0 || $exit_code == 1); } if (strpos($format, "tar_7z_") === 0) { $options = self::get7zCompressOptions($format, $password); $zip_command = '7z a ' . $options . ' -si ?'; if ($backup_mode == 'user_zip') { $app->system->exec_safe($find_user_files . ' | ' . $tar_input . ' | '. $zip_command, $web_path, $web_user, $web_group, $http_server_user, $web_path, $web_backup_dir . '/' . $web_backup_file); } else { $app->system->exec_safe($tar_dir . ' | ' . $zip_command, $web_path, $web_backup_dir . '/' . $web_backup_file); } $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; } return false; } /** * Runs a database compression routine * @param string $format * @param string $db_backup_dir * @param string $db_backup_file * @param string $compressed_backup_file * @param string $backup_tmp * @param string|null $password * @return bool true if success * @author Ramil Valitov */ protected static function runDatabaseCompression($format, $db_backup_dir, $db_backup_file, $compressed_backup_file, $backup_tmp, $password) { global $app; $app->log('Performing database backup to file ' . $compressed_backup_file . ' in format ' . $format, LOGLEVEL_DEBUG); switch ($format) { case 'gzip': if ($app->system->is_installed('pigz')) { //use pigz $zip_cmd = 'pigz'; } else { //use gzip $zip_cmd = 'gzip'; } $app->system->exec_safe($zip_cmd . " -c ? > ?", $db_backup_dir . '/' . $db_backup_file, $db_backup_dir . '/' . $compressed_backup_file); $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; case 'zip': case 'zip_bzip2': $zip_options = ($format === 'zip_bzip2') ? ' -Z bzip2 ' : ''; if (!empty($password)) { $zip_options .= ' --password ' . escapeshellarg($password); } $app->system->exec_safe('zip ' . $zip_options . ' -j -b ? ? ?', $backup_tmp, $db_backup_dir . '/' . $compressed_backup_file, $db_backup_dir . '/' . $db_backup_file); $exit_code = $app->system->last_exec_retcode(); // zip can return 12(due to harmless warnings) and still create valid backups return ($exit_code == 0 || $exit_code == 12); case 'bzip2': $app->system->exec_safe("bzip2 -q -c ? > ?", $db_backup_dir . '/' . $db_backup_file, $db_backup_dir . '/' . $compressed_backup_file); $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; case 'xz': $app->system->exec_safe("xz -q -q -c ? > ?", $db_backup_dir . '/' . $db_backup_file, $db_backup_dir . '/' . $compressed_backup_file); $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; case 'rar': $options = self::getRarOptions($backup_tmp, $password); $zip_command = 'rar a ' . $options . ' ? ?'; $app->system->exec_safe($zip_command, $db_backup_dir . '/' . $compressed_backup_file, $db_backup_dir . '/' . $db_backup_file); $exit_code = $app->system->last_exec_retcode(); return ($exit_code == 0 || $exit_code == 1); } if (strpos($format, "7z_") === 0) { $options = self::get7zCompressOptions($format, $password); $zip_command = '7z a ' . $options . ' ? ?'; $app->system->exec_safe($zip_command, $db_backup_dir . '/' . $compressed_backup_file, $db_backup_dir . '/' . $db_backup_file); $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; } return false; } /** * Mounts the backup directory if required * @param int $server_id * @return bool true if success * @author Ramil Valitov * @see backup_plugin::unmount_backup_dir() */ public static function mount_backup_dir($server_id) { global $app; $server_config = $app->getconf->get_server_config($server_id, 'server'); if ($server_config['backup_dir_is_mount'] == 'y') return $app->system->mount_backup_dir($server_config['backup_dir']); return true; } /** * Unmounts the backup directory if required * @param int $server_id * @return bool true if success * @author Ramil Valitov * @see backup_plugin::mount_backup_dir() */ public static function unmount_backup_dir($server_id) { global $app; $server_config = $app->getconf->get_server_config($server_id, 'server'); if ($server_config['backup_dir_is_mount'] == 'y') return $app->system->umount_backup_dir($server_config['backup_dir']); return true; } /** * Makes backup of database. * The backup directory must be mounted before calling this method. * This method is for private use only, don't call this method unless you know what you're doing. * @param array $web_domain * @param string $backup_job type of backup job: manual or auto * @return bool true if success * @author Ramil Valitov * @see backup_plugin::run_backup() recommeneded to use if you need to make backups */ protected static function make_database_backup($web_domain, $backup_job) { global $app; $server_id = intval($web_domain['server_id']); $domain_id = intval($web_domain['domain_id']); $server_config = $app->getconf->get_server_config($server_id, 'server'); $backup_dir = trim($server_config['backup_dir']); $backup_tmp = trim($server_config['backup_tmp']); $db_backup_dir = $backup_dir . '/web' . $domain_id; $success = false; if (empty($backup_job)) $backup_job = "auto"; $records = $app->dbmaster->queryAllRecords("SELECT * FROM web_database WHERE server_id = ? AND parent_domain_id = ?", $server_id, $domain_id); if (empty($records)){ $app->log('Skipping database backup for domain ' . $web_domain['domain_id'] . ', because no related databases found.', LOGLEVEL_DEBUG); return true; } self::prepare_backup_dir($server_id, $web_domain); include '/usr/local/ispconfig/server/lib/mysql_clientdb.conf'; //* Check mysqldump capabilities exec('mysqldump --help', $tmp); $mysqldump_routines = (strpos(implode($tmp), '--routines') !== false) ? '--routines' : ''; unset($tmp); foreach ($records as $rec) { $password = ($web_domain['backup_encrypt'] == 'y') ? trim($web_domain['backup_password']) : ''; $backup_format_db = $web_domain['backup_format_db']; if (empty($backup_format_db)) { $backup_format_db = 'gzip'; } $backup_extension_db = self::getBackupDbExtension($backup_format_db); if (!empty($backup_extension_db)) { //* Do the mysql database backup with mysqldump $db_name = $rec['database_name']; $db_file_prefix = 'db_' . $db_name . '_' . date('Y-m-d_H-i'); $db_backup_file = $db_file_prefix . '.sql'; $db_compressed_file = ($backup_job == 'manual' ? 'manual-' : '') . $db_file_prefix . $backup_extension_db; $command = "mysqldump -h ? -u ? -p? -c --add-drop-table --create-options --quick --max_allowed_packet=512M " . $mysqldump_routines . " --result-file=? ?"; /** @var string $clientdb_host */ /** @var string $clientdb_user */ /** @var string $clientdb_password */ $app->system->exec_safe($command, $clientdb_host, $clientdb_user, $clientdb_password, $db_backup_dir . '/' . $db_backup_file, $db_name); $exit_code = $app->system->last_exec_retcode(); //* Compress the backup if ($exit_code == 0) { $exit_code = self::runDatabaseCompression($backup_format_db, $db_backup_dir, $db_backup_file, $db_compressed_file, $backup_tmp, $password) ? 0 : 1; if ($exit_code !== 0) $app->log('Failed to make backup of database ' . $rec['database_name'], LOGLEVEL_ERROR); } else { $app->log('Failed to make backup of database ' . $rec['database_name'] . ', because mysqldump failed', LOGLEVEL_ERROR); } if ($exit_code == 0) { if (is_file($db_backup_dir . '/' . $db_compressed_file)) { chmod($db_backup_dir . '/' . $db_compressed_file, 0750); chown($db_backup_dir . '/' . $db_compressed_file, fileowner($db_backup_dir)); chgrp($db_backup_dir . '/' . $db_compressed_file, filegroup($db_backup_dir)); //* Insert web backup record in database $file_size = filesize($db_backup_dir . '/' . $db_compressed_file); $sql = "INSERT INTO web_backup (server_id, parent_domain_id, backup_type, backup_mode, backup_format, tstamp, filename, filesize, backup_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; //Making compatible with previous versions of ISPConfig: $sql_mode = ($backup_format_db == 'gzip') ? 'sqlgz' : ('sql' . $backup_format_db); $app->db->query($sql, $server_id, $domain_id, 'mysql', $sql_mode, $backup_format_db, time(), $db_compressed_file, $file_size, $password); if ($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $domain_id, 'mysql', $sql_mode, $backup_format_db, time(), $db_compressed_file, $file_size, $password); $success = true; } } else { if (is_file($db_backup_dir . '/' . $db_compressed_file)) unlink($db_backup_dir . '/' . $db_compressed_file); } //* Remove the uncompressed file if (is_file($db_backup_dir . '/' . $db_backup_file)) unlink($db_backup_dir . '/' . $db_backup_file); //* Remove old backups self::backups_garbage_collection($server_id, 'mysql', $domain_id); $prefix_list = array( "db_${db_name}_", "manual-db_${db_name}_", ); self::clearBackups($server_id, $domain_id, intval($rec['backup_copies']), $db_backup_dir, $prefix_list); } else { $app->log('Failed to process mysql backup format ' . $backup_format_db . ' for database ' . $rec['database_name'], LOGLEVEL_ERROR); } } unset($clientdb_host); unset($clientdb_user); unset($clientdb_password); return $success; } /** * Makes backup of web files. * The backup directory must be mounted before calling this method. * This method is for private use only, don't call this method unless you know what you're doing * @param array $web_domain info about domain to backup, SQL record of table 'web_domain' * @param string $backup_job type of backup job: manual or auto * @return bool true if success * @author Ramil Valitov * @see backup_plugin::mount_backup_dir() * @see backup_plugin::run_backup() recommeneded to use if you need to make backups */ protected static function make_web_backup($web_domain, $backup_job) { global $app; $server_id = intval($web_domain['server_id']); $domain_id = intval($web_domain['domain_id']); $server_config = $app->getconf->get_server_config($server_id, 'server'); $global_config = $app->getconf->get_global_config('sites'); $backup_dir = trim($server_config['backup_dir']); $backup_mode = $server_config['backup_mode']; $backup_tmp = trim($server_config['backup_tmp']); if (empty($backup_mode)) $backup_mode = 'userzip'; $web_config = $app->getconf->get_server_config($server_id, 'web'); $http_server_user = $web_config['user']; if (empty($backup_dir)) { $app->log('Failed to make backup of web files for domain id ' . $domain_id . ' on server id ' . $server_id . ', because backup directory is not defined', LOGLEVEL_ERROR); return false; } if (empty($backup_job)) $backup_job = "auto"; $backup_format_web = $web_domain['backup_format_web']; //Check if we're working with data saved in old version of ISPConfig if (empty($backup_format_web)) { $backup_format_web = 'default'; } if ($backup_format_web == 'default') { $backup_format_web = self::getDefaultBackupFormat($backup_mode, 'web'); } $password = ($web_domain['backup_encrypt'] == 'y') ? trim($web_domain['backup_password']) : ''; $backup_extension_web = self::getBackupWebExtension($backup_format_web); if (empty($backup_extension_web)) { $app->log('Failed to make backup of web files, because of unknown backup format ' . $backup_format_web . ' for website ' . $web_domain['domain'], LOGLEVEL_ERROR); return false; } $web_path = $web_domain['document_root']; $web_user = $web_domain['system_user']; $web_group = $web_domain['system_group']; $web_id = $web_domain['domain_id']; self::prepare_backup_dir($server_id, $web_domain); $web_backup_dir = $backup_dir . '/web' . $web_id; # default exclusions $backup_excludes = array( './backup*', './bin', './dev', './etc', './lib', './lib32', './lib64', './opt', './sys', './usr', './var', './proc', './run', './tmp', ); $b_excludes = explode(',', trim($web_domain['backup_excludes'])); if (is_array($b_excludes) && !empty($b_excludes)) { foreach ($b_excludes as $b_exclude) { $b_exclude = trim($b_exclude); if ($b_exclude != '') { array_push($backup_excludes, escapeshellarg($b_exclude)); } } } $web_backup_file = ($backup_job == 'manual' ? 'manual-' : '') . 'web' . $web_id . '_' . date('Y-m-d_H-i') . $backup_extension_web; $full_filename = $web_backup_dir . '/' . $web_backup_file; if (self::runWebCompression($backup_format_web, $backup_excludes, $backup_mode, $web_path, $web_backup_dir, $web_backup_file, $web_user, $web_group, $http_server_user, $backup_tmp, $password)) { if (is_file($full_filename)) { $backup_username = ($global_config['backups_include_into_web_quota'] == 'y') ? $web_user : 'root'; $backup_group = ($global_config['backups_include_into_web_quota'] == 'y') ? $web_group : 'root'; chown($full_filename, $backup_username); chgrp($full_filename, $backup_group); chmod($full_filename, 0750); //Insert web backup record in database $file_size = filesize($full_filename); $sql = "INSERT INTO web_backup (server_id, parent_domain_id, backup_type, backup_mode, backup_format, tstamp, filename, filesize, backup_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; $app->db->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, time(), $web_backup_file, $file_size, $password); if ($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, time(), $web_backup_file, $file_size, $password); unset($file_size); $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' completed successfully to file ' . $full_filename, LOGLEVEL_DEBUG); } else { $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' reported success, but the resulting file ' . $full_filename . ' not found.', LOGLEVEL_ERROR); } } else { if (is_file($full_filename)) unlink($full_filename); $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' failed using path ' . $web_path . ' failed.', LOGLEVEL_ERROR); } $prefix_list = array( 'web', 'manual-web', ); self::clearBackups($server_id, $web_id, intval($web_domain['backup_copies']), $web_backup_dir, $prefix_list); return true; } /** * Creates and prepares a backup dir * @param int $server_id * @param array $domain_data * @author Ramil Valitov */ protected static function prepare_backup_dir($server_id, $domain_data) { global $app; $server_config = $app->getconf->get_server_config($server_id, 'server'); $global_config = $app->getconf->get_global_config('sites'); if (isset($server_config['backup_dir_ftpread']) && $server_config['backup_dir_ftpread'] == 'y') { $backup_dir_permissions = 0755; } else { $backup_dir_permissions = 0750; } $backup_dir = $server_config['backup_dir']; if (!is_dir($backup_dir)) { mkdir($backup_dir, $backup_dir_permissions, true); } else { chmod($backup_dir, $backup_dir_permissions); } $web_backup_dir = $backup_dir . '/web' . $domain_data['domain_id']; if (!is_dir($web_backup_dir)) mkdir($web_backup_dir, 0750); chmod($web_backup_dir, 0750); $backup_username = 'root'; $backup_group = 'root'; if ($global_config['backups_include_into_web_quota'] == 'y') { $backup_username = $domain_data['system_user']; $backup_group = $domain_data['system_group']; } chown($web_backup_dir, $backup_username); chgrp($web_backup_dir, $backup_group); } /** * Makes a backup of website files or database. * @param string|int $domain_id * @param string $type backup type: web or mysql * @param string $backup_job how the backup is initiated: manual or auto * @param bool $mount if true, then the backup dir will be mounted and unmounted automatically * @return bool returns true if success * @author Ramil Valitov */ public static function run_backup($domain_id, $type, $backup_job, $mount = true) { global $app, $conf; $domain_id = intval($domain_id); $sql = "SELECT * FROM web_domain WHERE (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias') AND domain_id = ?"; $rec = $app->dbmaster->queryOneRecord($sql, $domain_id); if (empty($rec)) { $app->log('Failed to make backup of type ' . $type . ', because no information present about requested domain id ' . $domain_id, LOGLEVEL_ERROR); return false; } $server_id = intval($conf['server_id']); if ($mount && !self::mount_backup_dir($server_id)) { $app->log('Failed to make backup of type ' . $type . ' for domain id ' . $domain_id . ', because failed to mount backup directory', LOGLEVEL_ERROR); return false; } $ok = false; switch ($type) { case 'web': //$ok = self::make_web_backup($rec, $backup_job); /** The next changes are for repository specific backup method selection */ if (self::formatIsRepos($rec['backup_format_web'], 'web')) { $ok = self::make_web_backup_repos($rec, $backup_job); } else { $ok = self::make_web_backup($rec, $backup_job); } /** END */ break; case 'mysql': $rec['server_id'] = $server_id; //$ok = self::make_database_backup($rec, $backup_job); /** The next changes are for repository specific backup method selection */ if (self::formatIsRepos($rec['backup_format_db'], 'mysql')) { $ok = self::make_database_backup_repos($rec, $backup_job); } else { $ok = self::make_database_backup($rec, $backup_job); } /** END */ break; default: $app->log('Failed to make backup, because backup type is unknown: ' . $type, LOGLEVEL_ERROR); break; } if ($mount) self::unmount_backup_dir($server_id); return $ok; } /** * Runs backups of all websites that have backups enabled with respect to their backup interval settings * @param int $server_id * @param string $backup_job backup tupe: auto or manual * @author Ramil Valitov */ public static function run_all_backups($server_id, $backup_job = "auto") { global $app; $server_id = intval($server_id); $sql = "SELECT * FROM web_domain WHERE server_id = ? AND (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias') AND active = 'y' AND backup_interval != 'none' AND backup_interval != ''"; $domains = $app->dbmaster->queryAllRecords($sql, $server_id); if (!self::mount_backup_dir($server_id)) { $app->log('Failed to run regular backups routine because failed to mount backup directory', LOGLEVEL_ERROR); return; } self::backups_garbage_collection($server_id); $date_of_week = date('w'); $date_of_month = date('d'); foreach ($domains as $domain) { if (($domain['backup_interval'] == 'daily' or ($domain['backup_interval'] == 'weekly' && $date_of_week == 0) or ($domain['backup_interval'] == 'monthly' && $date_of_month == '01'))) { self::run_backup($domain['domain_id'], 'web', $backup_job, false); } } $sql = "SELECT DISTINCT d.*, db.server_id as `server_id` FROM web_database as db INNER JOIN web_domain as d ON (d.domain_id = db.parent_domain_id) WHERE db.server_id = ? AND db.active = 'y' AND d.backup_interval != 'none' AND d.backup_interval != ''"; $databases = $app->dbmaster->queryAllRecords($sql, $server_id); foreach ($databases as $database) { if (($database['backup_interval'] == 'daily' or ($database['backup_interval'] == 'weekly' && $date_of_week == 0) or ($database['backup_interval'] == 'monthly' && $date_of_month == '01'))) { self::run_backup($database['domain_id'], 'mysql', $backup_job, false); } } self::unmount_backup_dir($server_id); } /**** The following lines are additions for special borg format repos ******/ /** * Makes a web backup for a repository type backup **/ protected static function make_web_backup_repos($web_domain, $backup_job) { global $app; $server_id = intval($web_domain['server_id']); $domain_id = intval($web_domain['domain_id']); $server_config = $app->getconf->get_server_config($server_id, 'server'); $global_config = $app->getconf->get_global_config('sites'); $backup_dir = trim($server_config['backup_dir']); $backup_mode = $server_config['backup_mode']; $backup_tmp = trim($server_config['backup_tmp']); $web_config = $app->getconf->get_server_config($server_id, 'web'); $http_server_user = $web_config['user']; if (empty($backup_dir)) { $app->log('Failed to make backup of web files for domain id ' . $domain_id . ' on server id ' . $server_id . ', because backup directory is not defined', LOGLEVEL_ERROR); return false; } if (empty($backup_job)) $backup_job = "auto"; $backup_format_web = $web_domain['backup_format_web']; //Check if we're working with data saved in old version of ISPConfig if (empty($backup_format_web)) { $backup_format_web = 'default'; } if ($backup_format_web == 'default') { $backup_format_web = 'borg'; } $password = ($web_domain['backup_encrypt'] == 'y') ? trim($web_domain['backup_password']) : ''; $backup_repos_folder = self::getBackupReposFolder($backup_format_web, 'web'); if (empty($backup_repos_folder)) { $app->log('Failed to make backup of web files, because of unknown backup format ' . $backup_format_web . ' for website ' . $web_domain['domain'], LOGLEVEL_ERROR); return false; } $web_path = $web_domain['document_root']; $web_user = $web_domain['system_user']; $web_group = $web_domain['system_group']; $web_id = $web_domain['domain_id']; backup::prepare_backup_dir($server_id, $web_domain); $web_backup_dir = $backup_dir . '/web' . $web_id; # default exclusions $backup_excludes = array( './backup*', './bin', './dev', './etc', './lib', './lib32', './lib64', './opt', './sys', './usr', './var', './proc', './run', './tmp', ); $b_excludes = explode(',', trim($web_domain['backup_excludes'])); if (is_array($b_excludes) && !empty($b_excludes)) { foreach ($b_excludes as $b_exclude) { $b_exclude = trim($b_exclude); if ($b_exclude != '') { array_push($backup_excludes, escapeshellarg($b_exclude)); } } } $web_backup_archive = ($backup_job == 'manual' ? 'manual-' : '') . 'web' . $web_id . '_' . date('Y-m-d_H-i'); $backup_repos_path = $web_backup_dir . '/' . $backup_repos_folder; $full_archive_path = $backup_repos_path . '::' . $web_backup_archive; $archives = self::prepareRepos($backup_format_web, $backup_repos_path, $password); if ($archives === FALSE) { $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' using path ' . $web_path . ' failed.', LOGLEVEL_ERROR); return FALSE; } if (self::runBackupRepos($backup_format_web, $backup_excludes, $backup_mode, $web_path, $backup_repos_path, $web_backup_archive, $web_user, $web_group, $http_server_user, $backup_tmp, $password)) { $backup_username = ($global_config['backups_include_into_web_quota'] == 'y') ? $web_user : 'root'; $backup_group = ($global_config['backups_include_into_web_quota'] == 'y') ? $web_group : 'root'; //Insert web backup record in database $archive_size = self::getArchiveSize($backup_format_web, $backup_repos_path, $web_backup_archive, $password); $sql = "INSERT INTO web_backup (server_id, parent_domain_id, backup_type, backup_mode, backup_format, tstamp, filename, filesize, backup_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; $app->db->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, time(), $web_backup_archive, $archive_size, $password); if ($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, time(), $web_backup_archive, $archive_size, $password); unset($archive_size); $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' completed successfully to archive ' . $full_archive_path, LOGLEVEL_DEBUG); } else { if ($archives && in_array($web_backup_archive, $archives)) { self::deleteArchive($backup_format_web, $backup_repos_path, $web_backup_archive, $password); } $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' using path ' . $web_path . ' failed.', LOGLEVEL_ERROR); } $prefix_list = array( 'web', 'manual-web', ); self::cleanup_repository($backup_format_web, $server_id, $web_id, intval($web_domain['backup_copies']), $backup_repos_path, $prefix_list, 'web', $password); return true; } protected static function cleanup_repository($format, $server_id, $web_id, $backup_copies, $backup_repos_path, $prefix_list, $backup_type, $password) { global $app; self::pruneRepos($format, $backup_repos_path, $prefix_list, $backup_copies, $password); $archives = self::getReposArchives($format, $backup_repos_path, $password); if ($archives === false) { $app->log('Error trying to access list of repos ' . $backup_repos_path . ' to prune', LOGLEVEL_ERROR); return false; } $sql = "DELETE FROM web_backup WHERE server_id = ? AND parent_domain_id = ? AND backup_type = ? AND backup_format = ? "; if ($archives) { $sql .= "AND filename NOT IN ('" . implode("','", $archives) . "')"; } #var_export(['sql' => $sql, 'binds' => [$server_id, $web_id, $backup_type, $format]]); $app->db->query($sql, $server_id, $web_id, $backup_type, $format); if ($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $web_id, $backup_type, $format); return true; } protected static function getBackupReposFolder($backup_format, $backup_type) { switch ($backup_format) { case 'borg': return 'borg_' . $backup_type; } return null; } protected static function runBackupRepos($format, $backup_excludes, $backup_mode, $web_path, $backup_repos_path, $web_backup_archive, $web_user, $web_group, $http_server_user, $backup_tmp, $password) { global $app; #we wont use tar to be able to speed up things and extract specific files easily #$find_user_files = 'cd ? && find . -group ? -or -user ? -print 2> /dev/null'; $excludes = backup::generateExcludeList($backup_excludes, '--exclude '); $app->log('Performing web files backup of ' . $web_path . ' in format ' . $format . ', mode ' . $backup_mode, LOGLEVEL_DEBUG); switch ($format) { case 'borg': $password_cmd = self::getPasswordCommand($format, $password); $app->system->exec_safe( 'cd ? && ' . $password_cmd . 'borg create -C zlib --exclude-caches ' . $excludes . ' ? .', $web_path, $backup_repos_path . '::' . $web_backup_archive ); $exit_code = $app->system->last_exec_retcode(); return $exit_code == 0; } return FALSE; } protected static function getPasswordCommand($format, $password, $is_new = false) { switch ($format) { case 'borg': if ($password) { $password = str_replace("'", "'\"'\"'", $password); if ($is_new) { return "BORG_NEW_PASSPHRASE='{$password}' "; } return "BORG_PASSPHRASE='{$password}' "; } return ''; } return NULL; } protected static function prepareRepos($backup_format, $repos_path, $password) { global $app; if (is_dir($repos_path)) { $archives = self::getReposArchives($backup_format, $repos_path, $password); if ($app->system->last_exec_retcode() == 0) { return $archives; } if ($app->system->last_exec_retcode() == 2 && preg_match('/passphrase supplied in.*is incorrect/', $app->system->last_exec_out()[0])) { //Password was updated, so we rename folder and alert the event. $repos_stat = stat($repos_path); $mtime = $repos_stat['mtime']; $new_repo_path = $repos_path . '_' . date('Y-m-d_H-i', $mtime); rename($repos_path, $new_repo_name); $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' are encrypted under a different password. Original repos was moved to ' . $new_repo_name, LOGLEVEL_WARN); } else { return false; } } switch ($backup_format) { case 'borg': if ($password) { $password_cmd = self::getPasswordCommand($backup_format, $password, TRUE); $app->system->exec_safe($password_cmd . 'borg init --make-parent-dirs -e authenticated ?', $repos_path); } else { $app->system->exec_safe('borg init --make-parent-dirs -e none ?', $repos_path); } return $app->system->last_exec_retcode() == 0; } return false; } protected static function getReposArchives($backup_format, $repos_path, $password) { global $app; if ( ! is_dir($repos_path)) { return FALSE; } switch ($backup_format) { case 'borg': $password_cmd = self::getPasswordCommand($backup_format, $password); $app->system->exec_safe($password_cmd . "borg list --short ?", $repos_path); if ($app->system->last_exec_retcode() == 0) { return $app->system->last_exec_out(); } break; } return FALSE; } protected static function getArchiveSize($backup_format, $backup_repos_path, $backup_archive, $password) { global $app; switch ($backup_format) { case 'borg': $password_cmd = self::getPasswordCommand($backup_format, $password); $ret = $app->system->exec_safe( $password_cmd . 'borg info --json ? | awk -F \'[ ,]*\' \'/compressed_size/{print $3}\'', $backup_repos_path . '::' . $backup_archive ); if ($app->system->last_exec_retcode() == 0) { return (int)$ret; } } return NULL; } protected static function deleteArchive($format, $backup_repos_path, $backup_archive, $password) { global $app; switch ($format) { case 'borg': $password_cmd = self::getPasswordCommand($format, $password); $app->system->exec_safe($password_cmd . 'borg delete ?', $backup_repos_path . '::' . $backup_archive); return $app->system->last_exec_retcode() == 0; default: $app->log("Unknown repos format " . $format, LOGLEVEL_ERROR); } return FALSE; } protected static function pruneRepos($format, $backup_repos_path, $prefix_list, $backup_copies, $password) { global $app; switch ($format) { case 'borg': $password_cmd = self::getPasswordCommand($format, $password); foreach ($prefix_list as $prefix) { $app->system->exec_safe( $password_cmd . 'borg prune --keep-last ? -P ? ?', $backup_copies, $prefix, $backup_repos_path ); $ret_val = $app->system->last_exec_retcode(); if ($ret_val != 0) { break; } } return $ret_val == 0; } return FALSE; } /** * Makes backup of database into a repository type format. * The backup directory must be mounted before calling this method. * This method is for private use only, don't call this method unless you know what you're doing. * @param array $web_domain * @param string $backup_job type of backup job: manual or auto * @return bool true if success * @author Ramil Valitov * @see backup_plugin::run_backup() recommeneded to use if you need to make backups */ protected static function make_database_backup_repos($web_domain, $backup_job) { global $app; $server_id = intval($web_domain['server_id']); $domain_id = intval($web_domain['domain_id']); $server_config = $app->getconf->get_server_config($server_id, 'server'); $backup_dir = trim($server_config['backup_dir']); $backup_tmp = trim($server_config['backup_tmp']); $db_backup_dir = $backup_dir . '/web' . $domain_id; $success = false; $web_user = $web_domain['system_user']; $web_group = $web_domain['system_group']; if (empty($backup_job)) $backup_job = "auto"; $records = $app->dbmaster->queryAllRecords("SELECT * FROM web_database WHERE server_id = ? AND parent_domain_id = ?", $server_id, $domain_id); if (empty($records)){ $app->log('Skipping database backup for domain ' . $web_domain['domain_id'] . ', because no related databases found.', LOGLEVEL_DEBUG); return true; } self::prepare_backup_dir($server_id, $web_domain); include '/usr/local/ispconfig/server/lib/mysql_clientdb.conf'; //* Check mysqldump capabilities exec('mysqldump --help', $tmp); $mysqldump_routines = (strpos(implode($tmp), '--routines') !== false) ? '--routines' : ''; unset($tmp); $backup_dir .= DIRECTORY_SEPARATOR . 'web' . $domain_id; foreach ($records as $rec) { $password = ($web_domain['backup_encrypt'] == 'y') ? trim($web_domain['backup_password']) : ''; $backup_format_db = $web_domain['backup_format_db']; if (empty($backup_format_db)) { $backup_format_db = 'borg'; } if (!empty($backup_format_db)) { //* Do the mysql database backup with mysqldump $db_name = $rec['database_name']; $db_repos_folder = self::getBackupReposFolder($backup_format_db, 'mysql') . '_' . $db_name; $backup_repos_path = $backup_dir . DIRECTORY_SEPARATOR . $db_repos_folder; $archives = self::prepareRepos($backup_format_db, $backup_repos_path, $password); if ($archives === false) { $app->log('Backup of database ' . $rec['database_name'] . ' failed.', LOGLEVEL_ERROR); return false; } $db_backup_archive = ($backup_job == 'manual' ? 'manual-' : '') . 'db_' . $db_name . '_' . date('Y-m-d_H-i'); $full_archive_path = $backup_repos_path . '::' . $db_backup_archive; $dump_command = "mysqldump -h ? -u ? -p? -c --add-drop-table --create-options --quick --max_allowed_packet=512M " . $mysqldump_routines . " ?"; switch ($backup_format_db) { case 'borg': $password_cmd = self::getPasswordCommand($backup_format_db, $password); $command = $dump_command . ' | ' . $password_cmd . 'borg create -C zlib ? -'; /** @var string $clientdb_host */ /** @var string $clientdb_user */ /** @var string $clientdb_password */ $app->system->exec_safe($command, $clientdb_host, $clientdb_user, $clientdb_password, $db_name, #mysqldump command part $full_archive_path #borg command part ); $exit_code = $app->system->last_exec_retcode(); break; default: $app->log('Failed to make backup of database ' . $rec['database_name'] . ', because format ' . $backup_format_db . ' is unknown', LOGLEVEL_ERROR); return false; } if ($exit_code == 0) { $archive_size = self::getArchiveSize($backup_format_db, $backup_repos_path, $db_backup_archive, $password); if ($archive_size !== false) { //* Insert web backup record in database $sql = "INSERT INTO web_backup (server_id, parent_domain_id, backup_type, backup_mode, backup_format, tstamp, filename, filesize, backup_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; //Making compatible with previous versions of ISPConfig: $sql_mode = 'sql' . $backup_format_db; $app->db->query($sql, $server_id, $domain_id, 'mysql', $sql_mode, $backup_format_db, time(), $db_backup_archive, $archive_size, $password); if ($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $domain_id, 'mysql', $sql_mode, $backup_format_db, time(), $db_backup_archive, $archive_size, $password); $success = true; } else { $app->log('Failed to obtain backup size of ' . $full_archive_path . ' for database ' . $rec['database_name'], LOGLEVEL_ERROR); return false; } } else { rename($backup_repos_path, $new_path = $backup_repos_path . '_failed_' . uniqid()); $app->log('Failed to process mysql backup format ' . $backup_format_db . ' for database ' . $rec['database_name'] . ' repos renamed to ' . $new_path, LOGLEVEL_ERROR); } //* Remove old backups $prefix_list = array( "db_${db_name}_", "manual-db_${db_name}_", ); self::cleanup_repository($backup_format_db, $server_id, $domain_id, intval($rec['backup_copies']), $backup_repos_path, $prefix_list, 'mysql', $password); } else { $app->log('Failed to process mysql backup format ' . $backup_format_db . ' for database ' . $rec['database_name'], LOGLEVEL_ERROR); } } unset($clientdb_host); unset($clientdb_user); unset($clientdb_password); return $success; } public static function restoreBackupDatabaseRepos($backup_format, $password, $backup_dir, $backup_archive, $backup_mode, $backup_type) { global $app; //* Load sql dump into db include 'lib/mysql_clientdb.conf'; if (empty($backup_format)) { $backup_format = 'borg'; } $success = false; preg_match('@^(manual-)?db_(?P.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$@', $backup_archive, $matches); if (!isset($matches['db']) || empty($matches['db'])) { $app->log('Failed to detect database name during restore of ' . $backup_archive, LOGLEVEL_ERROR); return false; } $db_name = $matches['db']; $backup_repos_folder = self::getBackupReposFolder($backup_format, 'mysql') . '_' . $db_name; $backup_repos_path = $backup_dir . DIRECTORY_SEPARATOR . $backup_repos_folder; $full_archive_path = $backup_repos_path . '::' . $backup_archive; $app->log('Restoring MySQL backup ' . $full_archive_path . ', backup format "' . $backup_format . '", backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG); $archives = self::getReposArchives($backup_format, $backup_repos_path, $password); if (is_array($archives) && in_array($backup_archive, $archives)) { switch ($backup_format) { case "borg": $password_cmd = self::getPasswordCommand($backup_format, $password); $command = $password_cmd . "borg extract --stdout ? stdin | mysql -h ? -u ? -p? ?"; break; } if (!empty($command)) { /** @var string $clientdb_host */ /** @var string $clientdb_user */ /** @var string $clientdb_password */ $app->system->exec_safe($command, $full_archive_path, $clientdb_host, $clientdb_user, $clientdb_password, $db_name); $retval = $app->system->last_exec_retcode(); if ($retval == 0) { $app->log('Restored MySQL backup ' . $full_archive_path, LOGLEVEL_DEBUG); $success = true; } else { $app->log('Failed to restore web backup ' . $full_archive_path . ', exit code ' . $retval, LOGLEVEL_ERROR); } } else { $app->log('Archive test failed for ' . $full_archive_path, LOGLEVEL_DEBUG); } } else { $app->log('Failed to process MySQL backup ' . $full_archive_path . ' because it does not exist', LOGLEVEL_ERROR); } unset($clientdb_host); unset($clientdb_user); unset($clientdb_password); return $success; } public static function restoreBackupWebFilesRepos($backup_format, $password, $backup_dir, $backup_archive, $backup_mode, $backup_type, $web_root, $web_user, $web_group) { global $app; if (empty($backup_format) || $backup_format == 'default') { $backup_format = 'borg'; } $backup_repos_folder = self::getBackupReposFolder($backup_format, 'web'); $backup_repos_path = $backup_dir . DIRECTORY_SEPARATOR . $backup_repos_folder; $full_archive_path = $backup_repos_path . '::' . $backup_archive; $result = false; $app->log('Restoring web backup ' . $full_archive_path . ', backup format "' . $backup_format . '", backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG); if (!empty($backup_format)) { $app->system->web_folder_protection($web_root, false); $user_mode = $backup_mode == 'userzip'; $archives = self::getReposArchives($backup_format, $backup_repos_path, $password); if (in_array($backup_archive, $archives) && $web_root != '' && $web_root != '/' && !stristr($full_filename, '..') && !stristr($full_filename, 'etc')) { $success = false; $retval = 0; switch ($backup_format) { case "borg": $password_cmd = self::getPasswordCommand($backup_format, $password); $command = 'cd ? && ' . $password_cmd . 'borg extract ?'; $app->system->exec_safe($command, $web_root, $full_archive_path); $retval = $app->system->last_exec_retcode(); $success = ($retval == 0 || $retval == 1); break; } if ($success) { $app->log('Restored web backup ' . $full_archive_path, LOGLEVEL_DEBUG); $result = true; } else { $app->log('Failed to restore web backup ' . $full_archive_path . ', exit code ' . $retval, LOGLEVEL_ERROR); } } $app->system->web_folder_protection($web_root, true); } else { $app->log('Failed to restore web backup ' . $full_archive_path . ', backup format not recognized.', LOGLEVEL_DEBUG); } return $result; } public static function deleteBackup($backup_format, $backup_password, $backup_dir, $filename, $backup_mode, $backup_type, $domain_id) { global $app, $conf; $server_id = $conf['server_id']; $success = false; if (empty($backup_format) || $backup_format == 'default') { $backup_format = 'borg'; } if(self::formatIsRepos($backup_format, $backup_type)) { $backup_archive = $filename; $backup_repos_folder = self::getBackupReposFolder($backup_format, $backup_type); if ($backup_type != 'web') { preg_match('@^(manual-)?db_(?P.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$@', $backup_archive, $matches); if (!isset($matches['db']) || empty($matches['db'])) { $app->log('Failed to detect database name during restore of ' . $backup_archive, LOGLEVEL_ERROR); return false; } $db_name = $matches['db']; $backup_repos_folder .= '_' . $db_name; } $backup_repos_path = $backup_dir . DIRECTORY_SEPARATOR . $backup_repos_folder; $success = self::deleteArchive($backup_format, $backup_repos_path, $backup_archive, $password); } else { if(file_exists($backup_dir.'/'.$filename) && !stristr($backup_dir.'/'.$filename, '..') && !stristr($backup_dir.'/'.$filename, 'etc')) { $success = unlink($backup_dir.'/'.$filename); } else { $success = true; } } if ($success) { $sql = "DELETE FROM web_backup WHERE server_id = ? AND parent_domain_id = ? AND filename = ?"; $app->db->query($sql, $server_id, $domain_id, $filename); if($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $server_id, $domain_id, $filename); $app->log($sql . ' - ' . json_encode([$server_id, $domain_id, $filename]), LOGLEVEL_DEBUG); } return $success; } protected static function formatIsRepos($backup_format, $backup_type) { switch ($backup_type) { case 'web': case 'mysql': case 'mongodb': return in_array($backup_format, ['borg']); break; } return false; } public static function downloadBackup($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type, $domain) { global $app; if (self::formatIsRepos($backup_format, $backup_type)) { $backup_archive = $filename; if ($backup_type == 'web') { $backup_repos_folder = self::getBackupReposFolder($backup_format, 'web'); $filename .= '.tar.gz'; } else { preg_match('@^(manual-)?db_(?P.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$@', $backup_archive, $matches); if (!isset($matches['db']) || empty($matches['db'])) { $app->log('Failed to detect database name during restore of ' . $backup_archive, LOGLEVEL_ERROR); return false; } $db_name = $matches['db']; $backup_repos_folder = self::getBackupReposFolder($backup_format, $backup_type) . '_' . $db_name; if ($backup_type == 'mysql') { $filename .= '.sql.gz'; } else { $filename .= '.tar.gz'; } } $backup_repos_path = $backup_dir . DIRECTORY_SEPARATOR . $backup_repos_folder; $full_archive_path = $backup_repos_path . '::' . $backup_archive; $archives = self::getReposArchives($backup_format, $backup_repos_path, $password); if ( $archives === FALSE || ! in_array($backup_archive, $archives)) { $app->log('Failed to find archive ' . $full_archive_path . ' for download', LOGLEVEL_ERROR); return false; } $app->log('Extracting ' . $backup_type . ' backup from repository archive '.$full_archive_path. ' to ' . $domain['document_root'].'/backup/' . $filename, LOGLEVEL_DEBUG); switch ($backup_format) { case 'borg': $password_cmd = self::getPasswordCommand($backup_format, $password); if ($backup_type != 'mysql') { $command = $password_cmd . 'borg export-tar ? ?'; } else { $command = $password_cmd . 'borg extract --stdout ? stdin | gzip -c > ?'; } $app->system->exec_safe( $command, $full_archive_path, $domain['document_root'].'/backup/' . $filename ); if ($app->system->last_exec_retcode() != 0) { $app->log('Copy of archive ' . $full_archive_path . ' failed', LOGLEVEL_ERROR); return false; } break; default: $app->log('Download of archive ' . $full_archive_path . ' failed because format ' . $backup_format . ' is unknown', LOGLEVEL_ERROR); return false; } } //* Copy the backup file to the backup folder of the website elseif(file_exists($backup_dir.'/'.$filename) && file_exists($domain['document_root'].'/backup/') && !stristr($backup_dir.'/'.$filename, '..') && !stristr($backup_dir.'/'.$filename, 'etc')) { copy($backup_dir.'/'.$filename, $domain['document_root'].'/backup/'.$filename); } chgrp($domain['document_root'].'/backup/'.$filename, $domain['system_group']); chown($domain['document_root'].'/backup/'.$filename, $domain['system_user']); chmod($domain['document_root'].'/backup/'.$filename,0600); $app->log('Ready '.$domain['document_root'].'/backup/'.$filename, LOGLEVEL_DEBUG); return true; } /** END **/ }