Skip to content
backup.inc.php 106 KiB
Newer Older
        //* Validate compression

        $C = explode(',', $compression);
        if (count($C) > 2) {
            $app->log("Invalid compression option " . $C[2] . " from compression " . $compression . ".", LOGLEVEL_WARN);
            $compression = $C[0] . ',' . $C[1];
            $C = [$C[0], $C[1]];
        }
        if (count($C) > 1 && ! ctype_digit($C[1])) {
            $app->log("Invalid compression option " . $C[1] . " from compression " . $compression . ".", LOGLEVEL_WARN);
            $compression = $C[0];
            $C = [$C[0]];
        }

        switch ($C[0]) {
            case 'none':
            case 'lz4':
                if (count($C) > 1) {
                    $app->log("Invalid compression format " . $compression . '. Defaulting to ' . $C[0] . '.', LOGLEVEL_WARN);
                    $compression = $C[0];
                }
                break;
            case 'zstd':
                //* Check borg version
                list(,$ver) = explode(' ', exec('borg --version'));
                if (version_compare($ver, '1.1.4') < 0) {
                    $app->log("Current borg version " . $ver . " does not support compression format " . $compression . '. Defaulting to zlib.', LOGLEVEL_WARN);
                    $compression = 'zlib';
                } elseif (count($C) > 1 && ($C[1] < 1 || $C[1] > 22)) {
                    $app->log("Invalid compression format " . $compression . '. Defaulting to zstd.', LOGLEVEL_WARN);
                    $compression = 'zstd';
                }
                break;
            case 'zlib':
            case 'lzma':
                if (count($C) > 1 && ($C[1] < 0 || $C[1] > 9)) {
                    $app->log("Invalid compression format " . $compression . '. Defaulting to ' . $C[0] . '.', LOGLEVEL_WARN);
                    $compression = $C[0];
                }
                break;
            default:
                $app->log("Unsupported borg compression format " . $compression . '. Defaulting to zlib.', LOGLEVEL_WARN);
                $compression = 'zlib';
        }

        $options = array(
            /**
             * -C --compression
             */
            '-C ' . $compression,
            /**
             * Excludes directories that contain CACHEDIR.TAG
             */
            '--exclude-caches',
            /**
             * specify the chunker parameters (CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE).
             * @see https://borgbackup.readthedocs.io/en/stable/internals/data-structures.html#chunker-details
             * Default: 19,23,21,4095
             */
            //'--chunker-params 19,23,21,4095',
        );
        $options = implode(" ", $options);
        return $options;
    }

    /**
     * Gets a list of repository archives
     * @param string $backup_mode
     * @param string $repos_path absolute path to repository
     * @param string $password repository password or empty string if none
     * @param string $list_format Supports either 'short' or 'json'
     * @return array
     * @author Jorge Muñoz <elgeorge2k@gmail.com>
     */
    protected static function getReposArchives($backup_mode, $repos_path, $password, $list_format = 'short')
    {
        global $app;
        if ( ! is_dir($repos_path)) {
            $app->log("Unknown path " . var_export($repos_path, TRUE)
                . ' called from ' . (function() {
                    $dbt = debug_backtrace();
                    return $dbt[1]['file'] . ':' . $dbt[1]['line'];
                })(), LOGLEVEL_ERROR);
            return FALSE;
        }
        switch ($backup_mode) {
            case 'borg':

                $command = self::getBorgCommand('borg list', $password);

                if ($list_format == 'json') {
                    $command_opts = '--json';
                } else {
                    $command_opts = '--short';
                }

                $app->system->exec_safe($command . ' ' . $command_opts . ' ?', $repos_path);

                if ($app->system->last_exec_retcode() == 0) {
                    return array_map('trim', $app->system->last_exec_out());
                }
                break;
        }
        return FALSE;
    }

    /**
     * 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 <ramilvalitov@gmail.com>
     * @author Jorge Muñoz <elgeorge2k@gmail.com>
    protected static function clearBackups($server_id, $web_id, $max_backup_copies, $backup_dir, $prefix_list=null)
        $server_config = $app->getconf->get_server_config($server_id, 'server');
        $backup_mode = $server_config['backup_mode'];
        //@todo obtain password from server config
        $password = NULL;

        $db_list = array($app->db);
        if ($app->db->dbHost != $app->dbmaster->dbHost)
            array_push($db_list, $app->dbmaster);

        if ($backup_mode == "userzip" || $backup_mode == "rootgz") {
            $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;
            });

            //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);
            }
        } elseif (self::backupModeIsRepos($backup_mode)) {
            $repos_archives = self::getAllArchives($backup_dir, $backup_mode, $password);
            usort($repos_archives, function ($a, $b)  {
                return ($a['created_at'] > $b['created_at']) ? -1 : 1;
            });
            //Delete old files that are beyond the limit
            for ($n = $max_backup_copies; $n < sizeof($repos_archives); $n++) {
                $archive = $repos_archives[$n];
                $app->log('Backup archive ' . $archive['archive'] . ' 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, $archive['archive']);
                }
                $backup_repos_path = $backup_dir . '/' . $archive['repos'];
                self::deleteArchive($backup_mode, $backup_repos_path, $archive['archive'], $password);
    protected static function getAllArchives($backup_dir, $backup_mode, $password)
    {
        $d = dir($backup_dir);
        $archives = [];
        /**
         * $archives[] = [
         *      'repos'      => string,
         *      'archive'    => string,
         *      'created_at' => int,
         * ];
         */
        while (false !== ($entry = $d->read())) {
            if ('.' === $entry || '..' === $entry) {
                continue;
            }
            switch ($backup_mode) {
                case 'borg':
                    $repos_path = $backup_dir . '/' . $entry;
                    if (is_dir($repos_path) && strncmp('borg_', $entry, 5) === 0) {
                        $archivesJson = json_decode(implode("", self::getReposArchives($backup_mode, $repos_path, $password, 'json')), TRUE);
                        foreach ($archivesJson['archives'] as $archive) {
                            $archives[] = [
                                'repos'      => $entry,
                                'archive'    => $archive['name'],
                                'created_at' => strtotime($archive['time']),
                            ];
                        }
                    }
                    break;
            }
        }
        return $archives;
    }

    protected static function deleteArchive($backup_mode, $backup_repos_path, $backup_archive, $password)
    {
        global $app;
        $app->log("Delete Archive - repos = " . $backup_repos_path . ", archive = " . $backup_archive, LOGLEVEL_DEBUG);
        switch ($backup_mode) {
            case 'borg':
                $app->system->exec_safe('borg delete ?', $backup_repos_path . '::' . $backup_archive);
                return $app->system->last_exec_retcode() == 0;
            default:
                $app->log("Unknown repos type " . $backup_mode, LOGLEVEL_ERROR);
        }
        return FALSE;
    }

     * 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 <ramilvalitov@gmail.com>
     * @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 = ? AND backup_mode != 'borg'";
        $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) {
                $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) {
                        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();
    protected static function getReposFolder($backup_mode, $backup_type, $postfix = '')
    {
        switch ($backup_mode) {
            case 'borg':
                return 'borg_' . $backup_type . $postfix;
        }
        return null;
    }

    /**
     * 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 <ramilvalitov@gmail.com>
     */
    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;
    }

    /**
     * Gets list of directories in directory
     * @param string $directory
     * @param string[]|null $prefix_list filter files that have one of the prefixes. Use null for default filtering.
     * @return string[]
     * @author Ramil Valitov <ramilvalitov@gmail.com>
     */
    protected static function get_dirs($directory, $prefix_list = null, $endings_list = null)
    {
        $default_prefix_list = array(
            'borg',
        );
        if (is_null($prefix_list))
            $prefix_list = $default_prefix_list;

        if (!is_dir($directory)) {
            return array();
        }

        $dir_handle = dir($directory);
        $dirs = array();
        while (false !== ($entry = $dir_handle->read())) {
            $full_dirname = $directory . '/' . $entry;
            if ($entry != '.' && $entry != '..' && is_dir($full_dirname)) {
                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)
                    array_push($dirs, $entry);
            }
        }
        $dir_handle->close();

        return $dirs;
    }
    /**
     * Generates excludes list for compressors
     * @param string[] $backup_excludes
     * @param string $arg
     * @param string $pre
     * @param string $post
     * @return string
     * @author Ramil Valitov <ramilvalitov@gmail.com>
     */
    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}") . " ";
            }
    }

    /**
     * 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 <ramilvalitov@gmail.com>
     */
    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);
                    //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 <ramilvalitov@gmail.com>
     */
    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 <ramilvalitov@gmail.com>
     * @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 <ramilvalitov@gmail.com>
     * @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 <ramilvalitov@gmail.com>
     * @author Jorge Muñoz <elgeorge2k@gmail.com>
     * @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_mode = $server_config['backup_mode'];
        $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_extra_params = (strpos(implode($tmp), '--routines') !== false) ? '--routines' : '';
        $mysqldump_extra_params .= (strpos(implode($tmp), '--single-transaction') !== false) ? ' --single-transaction' : '';
        unset($tmp);

        foreach ($records as $rec) {
            if (self::backupModeIsRepos($backup_mode)) {
                //@todo get $password from server config
                $repos_password = '';
                //@todo get compression from server config
                $compression = 'zlib';
                $db_name = $rec['database_name'];
                $db_repos_folder = self::getBackupReposFolder($backup_mode, 'mysql') . '_' . $db_name;
                $backup_repos_path = $db_backup_dir . '/' . $db_repos_folder;
                $backup_format_db = '';
                if (self::prepareRepos($backup_mode, $backup_repos_path, $repos_password)) {
                    $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_extra_params . " ?";
                    switch ($backup_mode) {
                        case 'borg':
                            $borg_cmd = self::getBorgCommand('borg create', $repos_password);
                            $borg_options = self::getBorgCreateOptions($compression);
                            $command = $dump_command . ' | ' . $borg_cmd . ' ' . $borg_options . ' ? -';
                            /** @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;
                    }
                    if ($exit_code == 0) {
                        $archive_size = self::getReposArchiveSize($backup_mode, $backup_repos_path, $db_backup_archive, $repos_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 (?, ?, ?, ?, ?, ?, ?, ?, ?)";
                            //* password is for `Encrypted` column informative purposes, on download password is obtained from web_domain settings
                            $password = $repos_password ? '*secret*' : '';
                            $app->db->query($sql, $server_id, $domain_id, 'mysql', $backup_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', $backup_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);
                    }
                    $app->log('Failed to initialize repository for database ' . $rec['database_name'] . ', folder ' . $backup_repos_path . ', backup mode ' . $backup_mode . '.', LOGLEVEL_ERROR);
                }
            } else {
                $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_extra_params . " --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);
                }  else {
                    $app->log('Failed to process mysql backup format ' . $backup_format_db . ' for database ' . $rec['database_name'], LOGLEVEL_ERROR);
            //* 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);
        }

        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 <ramilvalitov@gmail.com>
     * @author Jorge Muñoz <elgeorge2k@gmail.com>
     * @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));
                }
            }
        }
        if (self::backupModeIsRepos($backup_mode)) {
            $backup_format_web = '';
            $web_backup_archive = ($backup_job == 'manual' ? 'manual-' : '') . 'web' . $web_id . '_' . date('Y-m-d_H-i');
            $backup_repos_folder = self::getBackupReposFolder($backup_mode, 'web');

            $backup_repos_path = $web_backup_dir . '/' . $backup_repos_folder;
            $full_archive_path = $backup_repos_path . '::' . $web_backup_archive;
            /**
             * @todo the internal borg password can't be the backup instance $password because the repos shares all backups
             * in a period of time. Instead we'll set the backup password on backup file download.
             */
            $repos_password = '';
            //@todo get this from the server config perhaps
            $compression = 'zlib';
            if ( ! self::prepareRepos($backup_mode, $backup_repos_path, $repos_password)) {
                $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' using path ' . $backup_repos_path . ' failed. Unable to prepare repository for ' . $backup_mode, LOGLEVEL_ERROR);
                return FALSE;
            }
            #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 ');
            $success = false;

            $app->log('Performing web files backup of ' . $web_path . ', mode ' . $backup_mode, LOGLEVEL_DEBUG);
            switch ($backup_mode) {
                case 'borg':
                    $command = self::getBorgCommand('borg create', $repos_password);
                    $command_opts = self::getBorgCreateOptions($compression);

                    $app->system->exec_safe(
                        'cd ? && ' . $command . ' ' . $command_opts . ' ' . $excludes . ' ? .',
                        $web_path, $backup_repos_path . '::' . $web_backup_archive
                    );
                    $success = $app->system->last_exec_retcode() == 0;
            }

            if ($success) {
                $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::getReposArchiveSize($backup_mode, $backup_repos_path, $web_backup_archive, $repos_password);
                $password = $repos_password ? '*secret*' : '';
                $sql = "INSERT INTO web_backup (server_id, parent_domain_id, backup_type, backup_mode, backup_format, tstamp, filename, filesize, backup_password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
                $backup_time = time();
                $app->db->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, $backup_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, $backup_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);
                $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' using path ' . $web_path . ' failed.', LOGLEVEL_ERROR);
            $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';