From 4cd4868518fb413b3813fa6fc03f7d6e33878823 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jorge=20Mu=C3=B1oz?= <jorge@weeklys.org>
Date: Mon, 27 Sep 2021 23:48:19 +0000
Subject: [PATCH] Add support for borgbackup incremental backups

---
 .../lib/classes/plugin_backuplist.inc.php     |  103 +-
 .../web/admin/form/server_config.tform.php    |    2 +-
 .../web/admin/lib/lang/ar_server_config.lng   |    2 +
 .../web/admin/lib/lang/bg_server_config.lng   |    2 +
 .../web/admin/lib/lang/br_server_config.lng   |    2 +
 .../web/admin/lib/lang/ca_server_config.lng   |    2 +
 .../web/admin/lib/lang/cz_server_config.lng   |    2 +
 .../web/admin/lib/lang/de_server_config.lng   |    2 +
 .../web/admin/lib/lang/dk_server_config.lng   |    2 +
 .../web/admin/lib/lang/el_server_config.lng   |    2 +
 .../web/admin/lib/lang/en_server_config.lng   |    2 +
 .../web/admin/lib/lang/es_server_config.lng   |    2 +
 .../web/admin/lib/lang/fi_server_config.lng   |    2 +
 .../web/admin/lib/lang/fr_server_config.lng   |    2 +
 .../web/admin/lib/lang/hr_server_config.lng   |    2 +
 .../web/admin/lib/lang/hu_server_config.lng   |    2 +
 .../web/admin/lib/lang/id_server_config.lng   |    2 +
 .../web/admin/lib/lang/it_server_config.lng   |    2 +
 .../web/admin/lib/lang/ja_server_config.lng   |    2 +
 .../web/admin/lib/lang/nl_server_config.lng   |    2 +
 .../web/admin/lib/lang/pl_server_config.lng   |    2 +
 .../web/admin/lib/lang/pt_server_config.lng   |    2 +
 .../web/admin/lib/lang/ro_server_config.lng   |    2 +
 .../web/admin/lib/lang/ru_server_config.lng   |    2 +
 .../web/admin/lib/lang/se_server_config.lng   |    2 +
 .../web/admin/lib/lang/sk_server_config.lng   |    2 +
 .../web/admin/lib/lang/tr_server_config.lng   |    2 +
 interface/web/admin/server_config_edit.php    |    5 +-
 .../templates/server_config_server_edit.htm   |    5 +
 .../web/sites/lib/lang/ar_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/bg_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/br_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/ca_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/cz_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/de_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/dk_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/el_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/en_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/es_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/fi_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/fr_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/hr_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/hu_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/id_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/it_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/ja_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/nl_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/pl_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/pt_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/ro_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/ru_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/se_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/sk_web_backup_list.lng |    1 +
 .../web/sites/lib/lang/tr_web_backup_list.lng |    1 +
 server/lib/classes/backup.inc.php             | 1243 +++++++++++++----
 .../plugins-available/backup_plugin.inc.php   |   18 +-
 56 files changed, 1192 insertions(+), 259 deletions(-)

diff --git a/interface/lib/classes/plugin_backuplist.inc.php b/interface/lib/classes/plugin_backuplist.inc.php
index 0b98dc9ec0..6fff24f611 100644
--- a/interface/lib/classes/plugin_backuplist.inc.php
+++ b/interface/lib/classes/plugin_backuplist.inc.php
@@ -160,7 +160,7 @@ class plugin_backuplist extends plugin_base {
 
 		//* Get the data
 		$server_ids = array();
-		$web = $app->db->queryOneRecord("SELECT server_id FROM web_domain WHERE domain_id = ?", $this->form->id);
+		$web = $app->db->queryOneRecord("SELECT server_id, backup_format_web, backup_format_db, backup_password, backup_encrypt FROM web_domain WHERE domain_id = ?", $this->form->id);
 		$databases = $app->db->queryAllRecords("SELECT server_id FROM web_database WHERE parent_domain_id = ?", $this->form->id);
 		if($app->functions->intval($web['server_id']) > 0) $server_ids[] = $app->functions->intval($web['server_id']);
 		if(is_array($databases) && !empty($databases)){
@@ -181,8 +181,33 @@ class plugin_backuplist extends plugin_base {
 				$rec["bgcolor"] = $bgcolor;
 
 				$rec['date'] = date($app->lng('conf_format_datetime'), $rec['tstamp']);
+
 				$backup_format = $rec['backup_format'];
-				if (empty($backup_format)) {
+				$backup_mode = $rec['backup_mode'];
+				if ($backup_mode == 'borg') {
+					// Get backup format from domain config
+					switch ($rec['backup_type']) {
+						case 'mysql':
+							$backup_format = $web['backup_format_db'];
+							if (empty($backup_format) || $backup_format == 'default') {
+								$backup_format = self::getDefaultBackupFormat('rootgz', 'mysql');
+							}
+							$rec['filename'] .= self::getBackupDbExtension($backup_format);
+							break;
+						case 'web':
+							$backup_format = $web['backup_format_web'];
+							if (empty($backup_format) || $backup_format == 'default') {
+								$backup_format = self::getDefaultBackupFormat($backup_mode, 'web');
+							}
+							$rec['filename'] .= self::getBackupWebExtension($backup_format);
+							break;
+						default:
+							$app->log('Unsupported backup type "' . $rec['backup_type'] . '" for backup id ' . $rec['backup_id'], LOGLEVEL_ERROR);
+							break;
+					}
+					$rec['backup_password'] = $web['backup_encrypt'] == 'y' ? trim($web['backup_password']) : '';
+
+				} elseif (empty($backup_format)) {
 					//We have a backup from old version of ISPConfig
 					switch ($rec['backup_type']) {
 						case 'mysql':
@@ -210,7 +235,13 @@ class plugin_backuplist extends plugin_base {
 				if($rec['server_id'] != $web['server_id']) $rec['download_available'] = false;
 
 				if($rec['filesize'] > 0){
-					$rec['filesize'] = $app->functions->currency_format($rec['filesize']/(1024*1024), 'client').' MB';
+					$rec['filesize'] = $app->functions->currency_format($rec['filesize']/(1024*1024), 'client').'&nbsp;MB';
+					if($backup_mode == "borg") {
+						$rec['filesize'] = '<a href="javascript:void(0)" data-toggle="tooltip" title="'
+											. $wb['final_size_txt']
+											. '"><strong>*</strong></a>'
+											. $rec['filesize'];
+					}
 				}
 
 				$records_new[] = $rec;
@@ -235,6 +266,72 @@ class plugin_backuplist extends plugin_base {
 		return $listTpl->grab();
 	}
 
+	/**
+	 * Returns file extension for specified backup format
+	 * @param string $format backup format
+	 * @return string|null
+	 * @author Ramil Valitov <ramilvalitov@gmail.com>
+	 */
+	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 <ramilvalitov@gmail.com>
+	 */
+	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;
+	}
+	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 "";
+	}
+
 }
 
 ?>
diff --git a/interface/web/admin/form/server_config.tform.php b/interface/web/admin/form/server_config.tform.php
index d5133f2a01..9de923ac8c 100644
--- a/interface/web/admin/form/server_config.tform.php
+++ b/interface/web/admin/form/server_config.tform.php
@@ -207,7 +207,7 @@ $form["tabs"]['server'] = array(
 			'datatype' => 'VARCHAR',
 			'formtype' => 'SELECT',
 			'default' => 'userzip',
-			'value' => array('userzip' => 'backup_mode_userzip', 'rootgz' => 'backup_mode_rootgz'),
+			'value' => array('userzip' => 'backup_mode_userzip', 'rootgz' => 'backup_mode_rootgz', 'borg' => 'backup_mode_borg_txt'),
 			'width' => '40',
 			'maxlength' => '255'
 		),
diff --git a/interface/web/admin/lib/lang/ar_server_config.lng b/interface/web/admin/lib/lang/ar_server_config.lng
index ac03605279..b5d43392c8 100644
--- a/interface/web/admin/lib/lang/ar_server_config.lng
+++ b/interface/web/admin/lib/lang/ar_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/bg_server_config.lng b/interface/web/admin/lib/lang/bg_server_config.lng
index b9d6e648ce..398e15db8f 100644
--- a/interface/web/admin/lib/lang/bg_server_config.lng
+++ b/interface/web/admin/lib/lang/bg_server_config.lng
@@ -174,6 +174,8 @@ $wb['connect_userid_to_webid_start_txt'] = 'Start ID for userid/webid connect';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/br_server_config.lng b/interface/web/admin/lib/lang/br_server_config.lng
index ac551c588c..51828f2e15 100644
--- a/interface/web/admin/lib/lang/br_server_config.lng
+++ b/interface/web/admin/lib/lang/br_server_config.lng
@@ -114,6 +114,8 @@ $wb['fastcgi_config_syntax_txt'] = 'Sintaxe das configurações do FastCGI';
 $wb['backup_mode_txt'] = 'Modo do backup';
 $wb['backup_mode_userzip'] = 'Arquivos de backup com propriedade do usuário web e compactados como zip';
 $wb['backup_mode_rootgz'] = 'Todos os arquivos no diretório web com proprietário root';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Faça backup de todos os arquivos no diretório vhost e bancos de dados em repositórios incrementais';
+$wb['backup_missing_utils_txt'] = 'O seguinte modo de backup não pode ser usado porque as ferramentas necessárias não estão instaladas:';
 $wb['tmpdir_path_error_empty'] = 'Caminho do diretório temporário está vazio.';
 $wb['tmpdir_path_error_regex'] = 'Caminho do diretório temporário é inválido.';
 $wb['backup_time_txt'] = 'Hora do backup';
diff --git a/interface/web/admin/lib/lang/ca_server_config.lng b/interface/web/admin/lib/lang/ca_server_config.lng
index 25ed761836..1e048dad50 100644
--- a/interface/web/admin/lib/lang/ca_server_config.lng
+++ b/interface/web/admin/lib/lang/ca_server_config.lng
@@ -113,6 +113,8 @@ $wb['fastcgi_config_syntax_txt'] = 'FastCGI config syntax';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['server_type_txt'] = 'Server Type';
 $wb['nginx_vhost_conf_dir_txt'] = 'Nginx Vhost config dir';
 $wb['nginx_vhost_conf_enabled_dir_txt'] = 'Nginx Vhost config enabled dir';
diff --git a/interface/web/admin/lib/lang/cz_server_config.lng b/interface/web/admin/lib/lang/cz_server_config.lng
index 6da8dfc0bb..9fa2a8e1d0 100644
--- a/interface/web/admin/lib/lang/cz_server_config.lng
+++ b/interface/web/admin/lib/lang/cz_server_config.lng
@@ -162,6 +162,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Režim zálohování';
 $wb['backup_mode_userzip'] = 'Zálohování všech souborů v adresáři web jako uživatel vlastnící web adresář do souboru zip';
 $wb['backup_mode_rootgz'] = 'Zálohování všech souborů v adresáři web jako uživatel root';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Zálohujte všechny soubory v adresáři vhost a databázích do přírůstkových úložišť';
+$wb['backup_missing_utils_txt'] = 'Následující režim zálohování nelze použít, protože nejsou nainstalovány požadované nástroje:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(jednotlivé RBL databáze oddělujte čárkou)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/de_server_config.lng b/interface/web/admin/lib/lang/de_server_config.lng
index d0b43059c4..4877141a50 100644
--- a/interface/web/admin/lib/lang/de_server_config.lng
+++ b/interface/web/admin/lib/lang/de_server_config.lng
@@ -190,6 +190,8 @@ $wb['awstats_settings_txt'] = 'AWStats Einstellungen';
 $wb['backup_mode_txt'] = 'Backupmodus';
 $wb['backup_mode_userzip'] = 'Backup Dateien gehören dem Web Benutzer (.zip Datei)';
 $wb['backup_mode_rootgz'] = 'Backup aller Dateien des Webverzeichnisses als Root Benutzer';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Maak een back-up van alle bestanden in de vhost-directory en databases in incrementele repositories';
+$wb['backup_missing_utils_txt'] = 'De volgende back-upmodus kan niet worden gebruikt omdat de vereiste tools niet zijn geïnstalleerd:';
 $wb['backup_time_txt'] = 'Backupzeit';
 $wb['firewall_txt'] = 'Firewall';
 $wb['mailbox_quota_stats_txt'] = 'E-Mailkonto Beschränkung Statistiken';
diff --git a/interface/web/admin/lib/lang/dk_server_config.lng b/interface/web/admin/lib/lang/dk_server_config.lng
index b1ebcec391..cc923d1959 100644
--- a/interface/web/admin/lib/lang/dk_server_config.lng
+++ b/interface/web/admin/lib/lang/dk_server_config.lng
@@ -100,6 +100,8 @@ $wb['fastcgi_config_syntax_txt'] = 'FastCGI config syntax';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup webfiler ejet af web-bruger som zip';
 $wb['backup_mode_rootgz'] = 'Backup alle filer i web mappe som root-bruger';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Sikkerhedskopier alle filer i vhost -bibliotek og databaser til inkrementelle lagre';
+$wb['backup_missing_utils_txt'] = 'Følgende backup -tilstand kan ikke bruges, fordi de nødvendige værktøjer ikke er installeret:';
 $wb['server_type_txt'] = 'Server Type';
 $wb['nginx_vhost_conf_dir_txt'] = 'Nginx Vhost config dir';
 $wb['nginx_vhost_conf_enabled_dir_txt'] = 'Nginx Vhost config enabled dir';
diff --git a/interface/web/admin/lib/lang/el_server_config.lng b/interface/web/admin/lib/lang/el_server_config.lng
index b147f15e5c..2bcda04116 100644
--- a/interface/web/admin/lib/lang/el_server_config.lng
+++ b/interface/web/admin/lib/lang/el_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/en_server_config.lng b/interface/web/admin/lib/lang/en_server_config.lng
index 4125b2648e..a908cffb80 100644
--- a/interface/web/admin/lib/lang/en_server_config.lng
+++ b/interface/web/admin/lib/lang/en_server_config.lng
@@ -120,6 +120,8 @@ $wb['fastcgi_config_syntax_txt'] = 'FastCGI config syntax';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['tmpdir_path_error_empty'] = 'tmp-dir Path is empty.';
 $wb['tmpdir_path_error_regex'] = 'Invalid tmp-dir path.';
 $wb['backup_time_txt'] = 'Backup time';
diff --git a/interface/web/admin/lib/lang/es_server_config.lng b/interface/web/admin/lib/lang/es_server_config.lng
index 67e77efac8..b8f59c9ab3 100644
--- a/interface/web/admin/lib/lang/es_server_config.lng
+++ b/interface/web/admin/lib/lang/es_server_config.lng
@@ -36,6 +36,8 @@ $wb['backup_dir_is_mount_txt'] = 'El directorio de copias de seguridad está mon
 $wb['backup_dir_mount_cmd_txt'] = 'Comando de montaje, si el directorio de copias de seguridad no está montado';
 $wb['backup_dir_txt'] = 'Directorio para respaldos';
 $wb['backup_mode_rootgz'] = 'Respaldar todos los archivos en el directorio web siendo root el propietario';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Respaldar todos los archivos en el directorio vhost y las bases de datos en repositorios incrementales';
+$wb['backup_missing_utils_txt'] = 'El siguiente modo de respaldo no se puede utilizar porque las herramientas necesarias no están instaladas:';
 $wb['backup_mode_txt'] = 'Modo de respaldo';
 $wb['backup_mode_userzip'] = 'Respaldar archivos web siendo el usuario el propietario en formato zip';
 $wb['bind_group_error_empty'] = 'El grupo para BIND está vacío.';
diff --git a/interface/web/admin/lib/lang/fi_server_config.lng b/interface/web/admin/lib/lang/fi_server_config.lng
index dac02a14b7..591a6df75f 100644
--- a/interface/web/admin/lib/lang/fi_server_config.lng
+++ b/interface/web/admin/lib/lang/fi_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/fr_server_config.lng b/interface/web/admin/lib/lang/fr_server_config.lng
index 0599b8bbed..692f9ab5f5 100644
--- a/interface/web/admin/lib/lang/fr_server_config.lng
+++ b/interface/web/admin/lib/lang/fr_server_config.lng
@@ -164,6 +164,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL’s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/hr_server_config.lng b/interface/web/admin/lib/lang/hr_server_config.lng
index e0894ceb82..8baa83beea 100644
--- a/interface/web/admin/lib/lang/hr_server_config.lng
+++ b/interface/web/admin/lib/lang/hr_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/hu_server_config.lng b/interface/web/admin/lib/lang/hu_server_config.lng
index 97774f9ecd..dde66a317c 100644
--- a/interface/web/admin/lib/lang/hu_server_config.lng
+++ b/interface/web/admin/lib/lang/hu_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/id_server_config.lng b/interface/web/admin/lib/lang/id_server_config.lng
index 814e963f64..0349aad4ca 100644
--- a/interface/web/admin/lib/lang/id_server_config.lng
+++ b/interface/web/admin/lib/lang/id_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/it_server_config.lng b/interface/web/admin/lib/lang/it_server_config.lng
index f9e30f3937..09529e278f 100644
--- a/interface/web/admin/lib/lang/it_server_config.lng
+++ b/interface/web/admin/lib/lang/it_server_config.lng
@@ -164,6 +164,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Modalità di Backup';
 $wb['backup_mode_userzip'] = 'Backup files siti web come utente web in formato zip';
 $wb['backup_mode_rootgz'] = 'Backup di tutti i files nella cartella sito come utente root';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Lista Real-Time Blackhole';
 $wb['realtime_blackhole_list_note_txt'] = '(Separare RBL con le virgole)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/ja_server_config.lng b/interface/web/admin/lib/lang/ja_server_config.lng
index 52ac44a351..ca56575953 100644
--- a/interface/web/admin/lib/lang/ja_server_config.lng
+++ b/interface/web/admin/lib/lang/ja_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/nl_server_config.lng b/interface/web/admin/lib/lang/nl_server_config.lng
index e9e412b609..dba658411c 100644
--- a/interface/web/admin/lib/lang/nl_server_config.lng
+++ b/interface/web/admin/lib/lang/nl_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/pl_server_config.lng b/interface/web/admin/lib/lang/pl_server_config.lng
index f9d43d3bfd..01fbf234c6 100644
--- a/interface/web/admin/lib/lang/pl_server_config.lng
+++ b/interface/web/admin/lib/lang/pl_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Tryb tworzenia kopii';
 $wb['backup_mode_userzip'] = 'Pliki kopii z prawami użytkownika jako zip';
 $wb['backup_mode_rootgz'] = 'Twórz kopie wszystkich plików w katalogu web jako root';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Twórz kopię wszystkich plików z katalogu vhost i baz danych w repozytoriach przyrostowych';
+$wb['backup_missing_utils_txt'] = 'Nie można użyć następującego trybu kopii zapasowej, ponieważ wymagane narzędzia nie są zainstalowane:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(oddziel RBL-e przecinkami)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/pt_server_config.lng b/interface/web/admin/lib/lang/pt_server_config.lng
index 468413a1de..0f52c114dd 100644
--- a/interface/web/admin/lib/lang/pt_server_config.lng
+++ b/interface/web/admin/lib/lang/pt_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/ro_server_config.lng b/interface/web/admin/lib/lang/ro_server_config.lng
index e20fb9ee9f..f81d0cbb56 100644
--- a/interface/web/admin/lib/lang/ro_server_config.lng
+++ b/interface/web/admin/lib/lang/ro_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/ru_server_config.lng b/interface/web/admin/lib/lang/ru_server_config.lng
index 1799b075f4..c134698d80 100644
--- a/interface/web/admin/lib/lang/ru_server_config.lng
+++ b/interface/web/admin/lib/lang/ru_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Заменители:';
 $wb['backup_mode_txt'] = 'Режим резервного копирования';
 $wb['backup_mode_userzip'] = 'Делать резервные копии web файлов принадлежащих ползователю web как архив ZIP';
 $wb['backup_mode_rootgz'] = 'Делать резервные копии всех файлов в веб-каталог как корневой пользователь';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Делать резервные копии всех файлов в каталоге vhost и базах данных в инкрементные репозитории';
+$wb['backup_missing_utils_txt'] = 'Следующий режим резервного копирования нельзя использовать, поскольку не установлены необходимые инструменты:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Разделяйте RBL запятыми)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/se_server_config.lng b/interface/web/admin/lib/lang/se_server_config.lng
index fe3c2e9234..ada1562fee 100644
--- a/interface/web/admin/lib/lang/se_server_config.lng
+++ b/interface/web/admin/lib/lang/se_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/sk_server_config.lng b/interface/web/admin/lib/lang/sk_server_config.lng
index bc7f9f514b..39948a1c12 100644
--- a/interface/web/admin/lib/lang/sk_server_config.lng
+++ b/interface/web/admin/lib/lang/sk_server_config.lng
@@ -176,6 +176,8 @@ $wb['website_autoalias_note_txt'] = 'Placeholders:';
 $wb['backup_mode_txt'] = 'Backup mode';
 $wb['backup_mode_userzip'] = 'Backup web files owned by web user as zip';
 $wb['backup_mode_rootgz'] = 'Backup all files in web directory as root user';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Backup all files in vhost directory and databases into incremental repositories';
+$wb['backup_missing_utils_txt'] = 'The following backup mode can not be used because the required tools are not installed:';
 $wb['realtime_blackhole_list_txt'] = 'Real-time Blackhole List';
 $wb['realtime_blackhole_list_note_txt'] = '(Separate RBL\'s with commas)';
 $wb['stress_adaptive_txt'] = 'Adapt to temporary load spikes';
diff --git a/interface/web/admin/lib/lang/tr_server_config.lng b/interface/web/admin/lib/lang/tr_server_config.lng
index 0d0c84f2c7..be980a321b 100644
--- a/interface/web/admin/lib/lang/tr_server_config.lng
+++ b/interface/web/admin/lib/lang/tr_server_config.lng
@@ -114,6 +114,8 @@ $wb['fastcgi_config_syntax_txt'] = 'FastCGI Ayar Yazımı';
 $wb['backup_mode_txt'] = 'Yedekleme Kipi';
 $wb['backup_mode_userzip'] = 'Web kullanıcısına ait web dosyaları ZIP biçiminde yedeklensin';
 $wb['backup_mode_rootgz'] = 'Web klasöründeki tüm dosyalar root kullanıcısı olarak yedeklensin';
+$wb['backup_mode_borg_txt'] = 'BorgBackup: Vhost dizinindeki ve veritabanlarındaki tüm dosyaları artımlı depolara yedekleyin';
+$wb['backup_missing_utils_txt'] = 'Gerekli araçlar kurulu olmadığı için aşağıdaki yedekleme modu kullanılamaz:';
 $wb['tmpdir_path_error_empty'] = 'tmp klasörü yolu boş olamaz.';
 $wb['tmpdir_path_error_regex'] = 'tmp klasörü yolu geçersiz.';
 $wb['backup_time_txt'] = 'Yedekleme Zamanı';
diff --git a/interface/web/admin/server_config_edit.php b/interface/web/admin/server_config_edit.php
index 1fd1921b84..29384ea941 100644
--- a/interface/web/admin/server_config_edit.php
+++ b/interface/web/admin/server_config_edit.php
@@ -63,7 +63,10 @@ class page_action extends tform_actions {
 			unset($app->tform->formDef["tabs"]["fastcgi"]);
 			unset($app->tform->formDef["tabs"]["vlogger"]);
 		}
-		
+		//Check if borg is installed
+		if (!$app->system->is_installed('borg')) {
+			$app->tpl->setVar('missing_utils', 'BorgBackup');
+		}
 		parent::onShow();
 	}
 
diff --git a/interface/web/admin/templates/server_config_server_edit.htm b/interface/web/admin/templates/server_config_server_edit.htm
index 394bf55278..1898f2bc55 100644
--- a/interface/web/admin/templates/server_config_server_edit.htm
+++ b/interface/web/admin/templates/server_config_server_edit.htm
@@ -1,4 +1,9 @@
 <div class="alert alert-info"><h4 style="text-align:center"><tmpl_var name="config_for_txt"> {tmpl_var name='server_name'}</h4></div>
+            <tmpl_if name="missing_utils">
+                <div class="tab-content alert alert-warning">
+                    {tmpl_var name='backup_missing_utils_txt'} {tmpl_var name='missing_utils'}
+                </div>
+            </tmpl_if>
 
 
 
diff --git a/interface/web/sites/lib/lang/ar_web_backup_list.lng b/interface/web/sites/lib/lang/ar_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/ar_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/ar_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/bg_web_backup_list.lng b/interface/web/sites/lib/lang/bg_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/bg_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/bg_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/br_web_backup_list.lng b/interface/web/sites/lib/lang/br_web_backup_list.lng
index 77580e3cc6..f718769c44 100644
--- a/interface/web/sites/lib/lang/br_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/br_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Backup manual';
 $wb['make_backup_web_txt'] = 'Criar backup dos arquivos Web';
 $wb['make_backup_database_txt'] = 'Criar backup dos bancos de dados';
 $wb['make_backup_confirm_txt'] = 'Você está prestes a iniciar um processo de backup manual. Os backups manuais contam para o número total de cópias de backup permitidas: portanto, se o limite for excedido, os backups mais antigos podem ser excluídos automaticamente. Continuar?';
+$wb['final_size_txt'] = 'O tamanho final do download pode variar dependendo do formato de compressão selecionado.';
 $wb['yes_txt'] = 'Sim';
 $wb['no_txt'] = 'Não';
 $wb['backup_is_encrypted_txt'] = 'Criptografado';
diff --git a/interface/web/sites/lib/lang/ca_web_backup_list.lng b/interface/web/sites/lib/lang/ca_web_backup_list.lng
index af33c31142..fbb0495a98 100644
--- a/interface/web/sites/lib/lang/ca_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/ca_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/cz_web_backup_list.lng b/interface/web/sites/lib/lang/cz_web_backup_list.lng
index e6f80dac09..d06d69fa2d 100644
--- a/interface/web/sites/lib/lang/cz_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/cz_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Ruční zálohování';
 $wb['make_backup_web_txt'] = 'Vytvořit zálohu webových souborů';
 $wb['make_backup_database_txt'] = 'Vytvořit zálohu databází';
 $wb['make_backup_confirm_txt'] = 'Chystáte se zahájit proces ručního zálohování. Ruční zálohy se započítávají do celkového počtu povolených záložních kopií: proto pokud bude limit překročen, mohou být nejstarší zálohy automaticky odstraněny. Pokračovat ?';
+$wb['final_size_txt'] = 'Konečná velikost stahování se může lišit v závislosti na zvoleném kompresním formátu.';
 $wb['yes_txt'] = 'Ano';
 $wb['no_txt'] = 'Ne';
 $wb['backup_is_encrypted_txt'] = 'Šifrované';
diff --git a/interface/web/sites/lib/lang/de_web_backup_list.lng b/interface/web/sites/lib/lang/de_web_backup_list.lng
index 790156206a..11ebcd0645 100644
--- a/interface/web/sites/lib/lang/de_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/de_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/dk_web_backup_list.lng b/interface/web/sites/lib/lang/dk_web_backup_list.lng
index ba5b7234f8..1529fa1a96 100644
--- a/interface/web/sites/lib/lang/dk_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/dk_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/el_web_backup_list.lng b/interface/web/sites/lib/lang/el_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/el_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/el_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/en_web_backup_list.lng b/interface/web/sites/lib/lang/en_web_backup_list.lng
index f2cde7f216..571f3959b8 100644
--- a/interface/web/sites/lib/lang/en_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/en_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/es_web_backup_list.lng b/interface/web/sites/lib/lang/es_web_backup_list.lng
index 61a7e83cb1..fb8a547202 100644
--- a/interface/web/sites/lib/lang/es_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/es_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/fi_web_backup_list.lng b/interface/web/sites/lib/lang/fi_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/fi_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/fi_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/fr_web_backup_list.lng b/interface/web/sites/lib/lang/fr_web_backup_list.lng
index 1c3bd84662..49da9188a1 100644
--- a/interface/web/sites/lib/lang/fr_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/fr_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/hr_web_backup_list.lng b/interface/web/sites/lib/lang/hr_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/hr_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/hr_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/hu_web_backup_list.lng b/interface/web/sites/lib/lang/hu_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/hu_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/hu_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/id_web_backup_list.lng b/interface/web/sites/lib/lang/id_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/id_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/id_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/it_web_backup_list.lng b/interface/web/sites/lib/lang/it_web_backup_list.lng
index 67cacff864..08f3b48e84 100644
--- a/interface/web/sites/lib/lang/it_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/it_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/ja_web_backup_list.lng b/interface/web/sites/lib/lang/ja_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/ja_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/ja_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/nl_web_backup_list.lng b/interface/web/sites/lib/lang/nl_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/nl_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/nl_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/pl_web_backup_list.lng b/interface/web/sites/lib/lang/pl_web_backup_list.lng
index dfdd53c25f..6185db50a3 100644
--- a/interface/web/sites/lib/lang/pl_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/pl_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/pt_web_backup_list.lng b/interface/web/sites/lib/lang/pt_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/pt_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/pt_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/ro_web_backup_list.lng b/interface/web/sites/lib/lang/ro_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/ro_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/ro_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/ru_web_backup_list.lng b/interface/web/sites/lib/lang/ru_web_backup_list.lng
index 2a92f2761b..3569ae7c09 100644
--- a/interface/web/sites/lib/lang/ru_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/ru_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/se_web_backup_list.lng b/interface/web/sites/lib/lang/se_web_backup_list.lng
index 8e0167aa13..3f210ecc42 100644
--- a/interface/web/sites/lib/lang/se_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/se_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/sk_web_backup_list.lng b/interface/web/sites/lib/lang/sk_web_backup_list.lng
index 8f0d3a7469..0f19d9339e 100644
--- a/interface/web/sites/lib/lang/sk_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/sk_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/interface/web/sites/lib/lang/tr_web_backup_list.lng b/interface/web/sites/lib/lang/tr_web_backup_list.lng
index 51bca34f2e..c61a5ab018 100644
--- a/interface/web/sites/lib/lang/tr_web_backup_list.lng
+++ b/interface/web/sites/lib/lang/tr_web_backup_list.lng
@@ -30,6 +30,7 @@ $wb['manual_backup_title_txt'] = 'Manual backup';
 $wb['make_backup_web_txt'] = 'Make backup of web files';
 $wb['make_backup_database_txt'] = 'Make backup of databases';
 $wb['make_backup_confirm_txt'] = 'You are about to start a manual backup process. Manual backups count towards the total number of allowed backup copies: therefore if the limit will be exceeded, then oldest backups may be deleted automatically. Proceed?';
+$wb['final_size_txt'] = 'Final download size may vary depending on selected compression format.';
 $wb['yes_txt'] = 'Yes';
 $wb['no_txt'] = 'No';
 $wb['backup_is_encrypted_txt'] = 'Encrypted';
diff --git a/server/lib/classes/backup.inc.php b/server/lib/classes/backup.inc.php
index 3cdf17d1fc..2b6d21e57e 100644
--- a/server/lib/classes/backup.inc.php
+++ b/server/lib/classes/backup.inc.php
@@ -32,6 +32,7 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  * Class backup
  * All code that makes actual backup and restore of web files and database is here.
  * @author Ramil Valitov <ramilvalitov@gmail.com>
+ * @author Jorge Muñoz <elgeorge2k@gmail.com> (Repository addition)
  * @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
@@ -93,6 +94,16 @@ class backup
         }
         return null;
     }
+    /**
+     * Checks whatever a backup mode is for a repository
+     * @param $mode Backup mode
+     * @return bool
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    protected static function backupModeIsRepos($mode)
+    {
+        return 'borg' === $mode;
+    }
 
     /**
      * Sets file ownership to $web_user for all files and folders except log, ssl and web/stats
@@ -148,6 +159,7 @@ class backup
      * @return bool true if succeeded
      * @see backup_plugin::mount_backup_dir()
      * @author Ramil Valitov <ramilvalitov@gmail.com>
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
      */
     public static function restoreBackupDatabase($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type)
     {
@@ -155,60 +167,116 @@ class backup
 
         //* Load sql dump into db
         include 'lib/mysql_clientdb.conf';
+        if (self::backupModeIsRepos($backup_mode)) {
 
-        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;
+            $backup_archive = $filename;
+
+            preg_match('@^(manual-)?db_(?P<db>.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$@', $backup_archive, $matches);
+            if (isset($matches['db']) && ! empty($matches['db'])) {
+                $db_name = $matches['db'];
+                $backup_repos_folder = self::getReposFolder($backup_mode, 'mysql', '_' . $db_name);
+                $backup_repos_path = $backup_dir . '/' . $backup_repos_folder;
+                $full_archive_path = $backup_repos_path . '::' . $backup_archive;
+
+                $app->log('Restoring MySQL backup from archive ' . $backup_archive . ', backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG);
+
+                $archives = self::getReposArchives($backup_mode, $backup_repos_path, $password);
+            } else {
+                $app->log('Failed to detect database name during restore of ' . $backup_archive, LOGLEVEL_ERROR);
+                $db_name = null;
+                $archives = null;
+            }
+            if (is_array($archives)) {
+                if (in_array($backup_archive, $archives)) {
+                    switch ($backup_mode) {
+                        case "borg":
+                            $command = self::getBorgCommand('borg extract --nobsdflags', $password);
+                            $command .= " --stdout ? stdin | mysql -h ? -u ? -p? ?";
+                            break;
+                    }
+                } else {
+                    $app->log('Failed to process MySQL backup ' . $full_archive_path . ' because it does not exist', LOGLEVEL_ERROR);
+                    $command = null;
+                }
+            }
+            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 database backup ' . $full_archive_path, LOGLEVEL_DEBUG);
+                    $success = true;
+                } else {
+                    $app->log('Failed to restore database backup ' . $full_archive_path . ', exit code ' . $retval, LOGLEVEL_ERROR);
+                }
+            }
+        } else {
+            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);
+            $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<db>.+)_\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 = null;
+            } else {
+                $db_name = $matches['db'];
             }
-            $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":
+
+            if ( ! empty($db_name)) {
+                $file_check = file_exists($full_filename) && !empty($extension);
+                if ( ! $file_check) {
+                    $app->log('Archive test failed for ' . $full_filename, LOGLEVEL_WARN);
+                }
+            } else {
+                $file_check = false;
+            }
+            if ($file_check) {
+                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
-                    $options = self::getUnrarOptions($password);
-                    $app->system->exec_safe("rar t " . $options . " ?", $full_filename);
+                    $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 = "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;
+                        $command = "7z x " . $options . " -so ? | mysql -h ? -u ? -p? ?";
+                    } else
+                        $command = null;
+                }
             }
             if (!empty($command)) {
                 /** @var string $clientdb_host */
@@ -220,13 +288,9 @@ class backup
                     $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);
+                    $app->log('Failed to restore MySQL 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);
@@ -250,118 +314,337 @@ class backup
      * @return bool true if succeed
      * @see backup_plugin::mount_backup_dir()
      * @author Ramil Valitov <ramilvalitov@gmail.com>
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
      */
     public static function restoreBackupWebFiles($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type, $web_root, $web_user, $web_group)
     {
         global $app;
 
-        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);
+        $app->system->web_folder_protection($web_root, false);
+        if (self::backupModeIsRepos($backup_mode)) {
+            $backup_archive = $filename;
+            $backup_repos_folder = self::getReposFolder($backup_mode, 'web');
+            $backup_repos_path = $backup_dir . '/' . $backup_repos_folder;
+            $full_archive_path = $backup_repos_path . '::' . $backup_archive;
+
+            $app->log('Restoring web backup archive ' . $full_archive_path . ', backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG);
+
+            $archives = self::getReposArchives($backup_mode, $backup_repos_path, $password);
+            if (is_array($archives) && in_array($backup_archive, $archives)) {
+                $retval = 0;
+                switch ($backup_mode) {
+                    case "borg":
+                        $command = 'cd ? && borg extract --nobsdflags ?';
+                        $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);
+                }
+            } else {
+                $app->log('Web backup archive does not exist ' . $full_archive_path, LOGLEVEL_ERROR);
+            }
 
-        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;
+        } elseif ($backup_mode == 'userzip' || $backup_mode == 'rootgz') {
 
-                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);
+            if (empty($backup_format) || $backup_format == 'default') {
+                $backup_format = self::getDefaultBackupFormat($backup_mode, $backup_type);
+            }
+            $full_filename = $backup_dir . '/' . $filename;
+
+            $app->log('Restoring web backup ' . $full_filename . ', backup format "' . $backup_format . '", backup mode "' . $backup_mode . '"', LOGLEVEL_DEBUG);
+
+            $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 . " 7z t " . $options . " ?";
-                        $app->system->exec_safe($command, $filename);
+                        $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 . " 7z x " . $options . " -so ? | tar xf - --directory ?";
-                            $app->system->exec_safe($command, $filename, $web_root);
+                            $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();
-                            $success = ($retval == 0 || $retval == 2);
+                            //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);
                         }
-                    }
-                    if ($user_mode) {
-                        unlink($filename);
-                        if (file_exists($filename . '.bak')) rename($filename . '.bak', $filename);
-                    }
+                        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) {
-                        $app->log('Restored web backup ' . $full_filename, LOGLEVEL_DEBUG);
-                        $result = true;
+                        //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('Failed to restore web backup ' . $full_filename . ', exit code ' . $retval, LOGLEVEL_ERROR);
+                        $app->log('Archive test failed for ' . $full_filename, LOGLEVEL_DEBUG);
                     }
                 }
-            } else {
-                $app->log('Failed to restore web backup ' . $full_filename . ', backup mode "' . $backup_mode . '" not recognized.', 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);
+                }
             }
-            $app->system->web_folder_protection($web_root, true);
         } else {
-            $app->log('Failed to restore web backup ' . $full_filename . ', backup format not recognized.', LOGLEVEL_DEBUG);
+            $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);
         return $result;
     }
 
+    /**
+     * Deletes backup copy
+     * @param string $backup_format
+     * @param string $backup_password
+     * @param string $backup_dir
+     * @param string $filename
+     * @param string $backup_mode
+     * @param string $backup_type
+     * @param int $domain_id
+     * @param bool true on success
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    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 = self::getDefaultBackupFormat($backup_mode, $backup_type);
+        }
+        if(self::backupModeIsRepos($backup_mode)) {
+            $repos_password = '';
+            $backup_archive = $filename;
+            $backup_repos_folder = self::getBackupReposFolder($backup_mode, $backup_type);
+            if ($backup_type != 'web') {
+                preg_match('@^(manual-)?db_(?P<db>.+)_\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 delete of ' . $backup_archive, LOGLEVEL_ERROR);
+                    return false;
+                }
+                $db_name = $matches['db'];
+                $backup_repos_folder .= '_' . $db_name;
+            }
+            $backup_repos_path = $backup_dir . '/' . $backup_repos_folder;
+            $archives = self::getReposArchives($backup_mode, $backup_repos_path, $repos_password);
+            if (is_array($archives) && in_array($backup_archive, $archives)) {
+                $success = self::deleteArchive($backup_mode, $backup_repos_path, $backup_archive, $repos_password);
+            } else {
+                $success = true;
+            }
+        } 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;
+    }
+    /**
+     * Downloads the backup copy
+     * @param string $backup_format
+     * @param string $password
+     * @param string $backup_dir
+     * @param string $filename
+     * @param string $backup_mode
+     * @param string $backup_type
+     * @param array $domain web_domain record
+     * @param bool true on success
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    public static function downloadBackup($backup_format, $password, $backup_dir, $filename, $backup_mode, $backup_type, $domain)
+    {
+        global $app;
+
+        $success = false;
+
+        if (self::backupModeIsRepos($backup_mode)) {
+            $backup_archive = $filename;
+            //When stored in repos, we first get target backup format to generate final download file
+            $repos_password = '';
+            $server_id = $domain['server_id'];
+            $password = $domain['backup_encrypt'] == 'y' ? trim($domain['backup_password']) : '';
+            $server_config = $app->getconf->get_server_config($server_id, 'server');
+            $backup_tmp = trim($server_config['backup_tmp']);
+
+            if ($backup_type == 'web') {
+                $backup_format = $domain['backup_format_web'];
+                if (empty($backup_format) || $backup_format == 'default') {
+                    $backup_format = self::getDefaultBackupFormat($server_backup_mode, 'web');
+                }
+                $backup_repos_folder = self::getBackupReposFolder($backup_mode, 'web');
+                $extension = self::getBackupWebExtension($backup_format);
+            } else {
+                if (preg_match('@^(manual-)?db_(?P<db>.+)_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$@', $backup_archive, $matches)) {
+                    $db_name = $matches['db'];
+                    $backup_format = $domain['backup_format_db'];
+                    if (empty($backup_format)) {
+                        $backup_format = self::getDefaultBackupFormat($server_backup_mode, $backup_type);
+                    }
+                    $backup_repos_folder = self::getBackupReposFolder($backup_mode, $backup_type) . '_' . $db_name;
+                    $extension = self::getBackupDbExtension($backup_format);
+                } else {
+                    $app->log('Failed to detect database name during download of ' . $backup_archive, LOGLEVEL_ERROR);
+                    $db_name = null;
+                }
+            }
+            if ( ! empty($extension)) {
+                $filename .= $extension;
+                $backup_repos_path = $backup_dir . '/' . $backup_repos_folder;
+                $full_archive_path = $backup_repos_path . '::' . $backup_archive;
+                $archives = self::getReposArchives($backup_mode, $backup_repos_path, $repos_password);
+            } else {
+                $archives = null;
+            }
+            if (is_array($archives)) {
+                if (in_array($backup_archive, $archives)) {
+                    $app->log('Extracting ' . $backup_type . ' backup from repository archive '.$full_archive_path. ' to ' . $domain['document_root'].'/backup/' . $filename, LOGLEVEL_DEBUG);
+                    switch ($backup_mode) {
+                        case 'borg':
+                            if ($backup_type == 'mysql') {
+                                if (strpos($extension, '.sql.gz') === 0 || strpos($extension, '.sql.bz2') === 0) {
+                                    //* .sql.gz and .sql.bz2 don't need a source file, so we can just pipe through the compression command
+                                    $ccmd = strpos($extension, '.sql.gz') === 0 ? 'gzip' : 'bzip2';
+                                    $command = self::getBorgCommand('borg extract', $repos_password) . ' --stdout ? stdin | ' . $ccmd . ' -c > ?';
+                                    $success = $app->system->exec_safe($command, $full_archive_path, $domain['document_root'].'/backup/'.$filename) == 0;
+                                } else {
+                                    $tmp_extract = $backup_tmp . '/' . $backup_archive . '.sql';
+                                    if (file_exists($tmp_extract)) {
+                                        unlink($tmp_extract);
+                                    }
+                                    $command = self::getBorgCommand('borg extract', $repos_password) . ' --stdout ? stdin > ?';
+                                    $app->system->exec_safe($command, $full_archive_path, $tmp_extract);
+                                }
+                            } else {
+                                if (strpos($extension, '.tar') === 0 && ($password == '' || strpos($extension, '.tar.7z') !== 0)) {
+                                    //* .tar.gz, .tar.bz2, etc are supported via borg export-tar, if they don't need encryption
+                                    $command = self::getBorgCommand('borg export-tar', $repos_password) . ' ? ?';
+                                    $app->system->exec_safe($command, $full_archive_path, $domain['document_root'].'/backup/'.$filename);
+                                    $success = $app->system->last_exec_retcode() == 0;
+                                } else {
+                                    $tmp_extract = tempnam($backup_tmp, $backup_archive);
+                                    unlink($tmp_extract);
+                                    mkdir($tmp_extract);
+                                    $command = 'cd ' . $tmp_extract . ' && ' . self::getBorgCommand('borg extract --nobsdflags', $repos_password) . ' ?';
+                                    $app->system->exec_safe($command, $full_archive_path);
+                                    if ($app->system->last_exec_retcode() != 0) {
+                                        $app->log('Extraction of ' . $full_archive_path . ' into ' . $tmp_extract . ' failed.', LOGLEVEL_ERROR);
+                                        $tmp_extract = null;
+                                    }
+                                }
+                            }
+                            break;
+                    }
+                    if ( ! empty($tmp_extract)) {
+                        if (is_dir($tmp_extract)) {
+                            $web_config = $app->getconf->get_server_config($server_id, 'web');
+                            $http_server_user = $web_config['user'];
+                            $success = self::runWebCompression($backup_format, [], 'rootgz', $tmp_extract, $domain['document_root'].'/backup/', $filename, $domain['system_user'], $domain['system_group'], $http_server_user, $backup_tmp, $password);
+                        } else {
+                            self::runDatabaseCompression($backup_format, dirname($tmp_extract), basename($tmp_extract), $filename, $backup_tmp, $password)
+                                AND $success = rename(dirname($tmp_extract) . '/' . $filename, $domain['document_root'].'/backup/'. $filename);
+                        }
+                        if ($success) {
+                            $app->system->exec_safe('rm -Rf ?', $tmp_extract);
+                        } else {
+                             $app->log('Failed to run compression of ' . $tmp_extract . ' into ' . $domain['document_root'].'/backup/' . $filename . ' failed.', LOGLEVEL_ERROR);
+                        }
+                    }
+                } else {
+                    $app->log('Failed to find archive ' . $full_archive_path . ' for download', LOGLEVEL_ERROR);
+                }
+            }
+            if ($success) {
+                $app->log('Download of archive ' . $full_archive_path . ' into ' . $domain['document_root'].'/backup/'.$filename . ' succeeded.', LOGLEVEL_DEBUG);
+            }
+        }
+        //* 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')) {
+            $success = copy($backup_dir.'/'.$filename, $domain['document_root'].'/backup/'.$filename);
+        }
+        if (file_exists($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;
+        } else {
+            $app->log('Failed download of '.$domain['document_root'].'/backup/'.$filename , LOGLEVEL_ERROR);
+            return false;
+        }
+    }
+
+
     /**
      * Returns a compression method, for example returns bzip2 for tar_7z_bzip2
      * @param string $format
@@ -688,6 +971,139 @@ class backup
         return $options;
     }
 
+    /**
+     * Get borg command with password appended to the base command
+     * @param $command Base command to add password to
+     * @param $password Password to add
+     * @param $is_new Specify if command is for a new borg repository initialization
+     */
+    protected static function getBorgCommand($command, $password, $is_new = false)
+    {
+        if ($password) {
+            if ($is_new) {
+                return "BORG_NEW_PASSPHRASE='" . escapeshellarg($password) . "' " . $command;
+            }
+            return "BORG_PASSPHRASE='" . escapeshellarg($password) . "' " . $command;
+        }
+        return $command;
+    }
+    /**
+     * Obtains command line options for "borg create" command.
+     * @param string $compression Compression options are validated and fixed if necessary.
+     *      See: https://borgbackup.readthedocs.io/en/stable/internals/data-structures.html#compression
+     * @return string
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    protected static function getBorgCreateOptions($compression)
+    {
+        global $app;
+
+        //* 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.
@@ -699,36 +1115,108 @@ class backup
      * @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)
     {
         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;
-        });
+        $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);
 
-        //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);
+        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);
             }
-            @unlink($full_filename);
         }
         return true;
     }
 
+    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.
@@ -748,7 +1236,7 @@ class backup
         $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 = "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);
@@ -772,7 +1260,7 @@ class backup
         if ($app->db->dbHost != $app->dbmaster->dbHost)
             array_push($db_list, $app->dbmaster);
 
-	// Cleanup web_backup entries for non-existent backup files
+        // 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) {
@@ -785,7 +1273,7 @@ class backup
             }
         }
 
-	// Cleanup backup files with missing web_backup entries (runs on all servers)
+        // 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'];
@@ -795,7 +1283,7 @@ class backup
             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();
+                $untracked_backup_files = array();
                 foreach ($db_list as $db) {
                     $backups = $db->queryAllRecords($sql, $domain_id);
                     foreach ($backups as $backup) {
@@ -804,8 +1292,8 @@ class backup
                         }
                     }
                 }
-		array_unique( $untracked_backup_files );
-		foreach ($untracked_backup_files as $f) {
+                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);
@@ -813,7 +1301,7 @@ class backup
             }
         }
 
-	// This cleanup only runs on web servers
+        // 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'];
@@ -856,6 +1344,15 @@ class backup
         }
     }
 
+    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
@@ -922,6 +1419,48 @@ class backup
         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
@@ -935,7 +1474,7 @@ class backup
     {
         $excludes = "";
         foreach ($backup_excludes as $ex) {
-	    # pass through escapeshellarg if not already done
+            # pass through escapeshellarg if not already done
             if ( preg_match( "/^'.+'$/", $ex ) ) {
                 $excludes .= "${arg}${pre}${ex}${post} ";
             } else {
@@ -1167,6 +1706,7 @@ class backup
      * @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)
@@ -1176,6 +1716,7 @@ class backup
         $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;
@@ -1200,67 +1741,118 @@ class backup
         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
+            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_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);
+                $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_routines . " ?";
+                    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);
+                    }
                 } else {
-                    $app->log('Failed to make backup of database ' . $rec['database_name'] . ', because mysqldump failed', 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_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();
 
-                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;
+                    //* 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);
                     }
-                } else {
-                    if (is_file($db_backup_dir . '/' . $db_compressed_file)) unlink($db_backup_dir . '/' . $db_compressed_file);
+
+                    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 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);
             }
+            //* 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);
@@ -1278,6 +1870,7 @@ class backup
      * @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
      */
@@ -1328,11 +1921,11 @@ class backup
         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',
-		);
+        # 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)) {
@@ -1343,43 +1936,217 @@ class backup
                 }
             }
         }
+        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';
 
-        $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)) {
+            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';
-                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);
+                $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 (?, ?, ?, ?, ?, ?, ?, ?, ?)";
-                $app->db->query($sql, $server_id, $web_id, 'web', $backup_mode, $backup_format_web, time(), $web_backup_file, $file_size, $password);
+                $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, 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);
+                    $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);
             } else {
-                $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' reported success, but the resulting file ' . $full_filename . ' not found.', LOGLEVEL_ERROR);
+                $app->log('Backup of web files for domain ' . $web_domain['domain'] . ' using path ' . $web_path . ' failed.', 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);
+            $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',
-		);
+        $prefix_list = array(
+            'web',
+            'manual-web',
+        );
         self::clearBackups($server_id, $web_id, intval($web_domain['backup_copies']), $web_backup_dir, $prefix_list);
         return true;
     }
 
+    protected static function getBackupReposFolder($backup_mode, $backup_type)
+    {
+        switch ($backup_mode) {
+            case 'borg': return 'borg_' . $backup_type;
+        }
+        return null;
+    }
+
+    /**
+     * Prepares repository for backup. Initialization, etc.
+     */
+    protected static function prepareRepos($backup_mode, $repos_path, $password)
+    {
+        global $app;
+        if (is_dir($repos_path)) {
+            self::getReposArchives($backup_mode, $repos_path, $password);
+            if ($app->system->last_exec_retcode() == 0) {
+                return true;
+            }
+            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_mode) {
+            case 'borg':
+                if ($password) {
+                    $command = self::getBorgCommand('borg init', $password, true);
+                    $app->system->exec_safe($command . ' --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;
+    }
+
+    /**
+     * Obtains archive compressed size from specific repository.
+     * @param string $backup_mode Server backup mode.
+     * @param string $backup_repos_path Absolute path to repository.
+     * @param string $backup_archive Name of the archive to obtain size from.
+     * @param string $password Provide repository password or empty string if there is none.
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    protected static function getReposArchiveSize($backup_mode, $backup_repos_path, $backup_archive, $password)
+    {
+        $info = self::getReposArchiveInfo($backup_mode, $backup_repos_path, $backup_archive, $password);
+        if ($info) {
+            return $info['compressed_size'];
+        }
+        return false;
+    }
+    /**
+     * Obtains archive information for specific repository archive.
+     * @param string $backup_mode Server backup mode.
+     * @param string $backup_repos_path Absolute path to repository.
+     * @param string $backup_archive Name of the archive to obtain size from.
+     * @param string $password Provide repository password or empty string if there is none.
+     * @return array Can contain one or more of the following keys:
+     *      'created_at': int unixtime
+     *      'created_by': string
+     *      'comment': string
+     *      'original_size': int
+     *      'compressed_size': int
+     *      'deduplicated_size': int
+     *      'num_files': int
+     *      'compression': string
+     * @author Jorge Muñoz <elgeorge2k@gmail.com>
+     */
+    protected static function getReposArchiveInfo($backup_mode, $backup_repos_path, $backup_archive, $password)
+    {
+        global $app;
+        $info = [];
+        switch ($backup_mode) {
+            case 'borg':
+                $command = self::getBorgCommand('borg info', $password);
+                $full_archive_path = $backup_repos_path . '::' . $backup_archive;
+                $app->system->exec_safe($command . ' --json ?', $full_archive_path);
+                if ($app->system->last_exec_retcode() != 0) {
+                    $app->log('Command `borg info` failed for ' . $full_archive_path . '.', LOGLEVEL_ERROR);
+                }
+                $out = implode("", $app->system->last_exec_out());
+                if ($out) {
+                    $out = json_decode($out, true);
+                }
+                if (empty($out)) {
+                    $app->log('No json result could be parsed from `borg info --json` command for repository ' . $full_archive_path . '.', LOGLEVEL_ERROR);
+                    return false;
+                }
+                if (empty($out['archives'])) {
+                    $app->log('No archive ' . $backup_archive . ' found for repository ' . $backup_repos_path . '.', LOGLEVEL_WARN);
+                    return false;
+                }
+                $info['created_at'] = strtotime($out['archives'][0]['start']);
+                $info['created_by'] = $out['archives'][0]['username'];
+                $info['comment'] = $out['archives'][0]['comment'];
+                $info['original_size'] = (int)$out['archives'][0]['stats']['original_size'];
+                $info['compressed_size'] = (int)$out['archives'][0]['stats']['compressed_size'];
+                $info['deduplicated_size'] = (int)$out['archives'][0]['stats']['deduplicated_size'];
+                $info['num_files'] = (int)$out['archives'][0]['stats']['nfiles'];
+                $prev_arg = null;
+                foreach ($out['archives'][0]['command_line'] as $arg) {
+                    if ($prev_arg == '-C' || $prev_arg == '--compression') {
+                        $info['compression'] = $arg;
+                        break;
+                    }
+                    $prev = $arg;
+                }
+        }
+        return $info;
+    }
+
     /**
      * Creates and prepares a backup dir
      * @param int $server_id
@@ -1498,7 +2265,7 @@ class backup
             }
         }
 
-	$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 != ''";
+        $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) {
diff --git a/server/plugins-available/backup_plugin.inc.php b/server/plugins-available/backup_plugin.inc.php
index a92165ba6d..8ba345b4c4 100644
--- a/server/plugins-available/backup_plugin.inc.php
+++ b/server/plugins-available/backup_plugin.inc.php
@@ -87,14 +87,7 @@ class backup_plugin {
 			if($backup_dir_is_ready){
 				//* Make backup available for download
 				if($action_name == 'backup_download') {
-					//* Copy the backup file to the backup folder of the website
-					if(file_exists($backup_dir.'/'.$backup['filename']) && file_exists($web['document_root'].'/backup/') && !stristr($backup_dir.'/'.$backup['filename'], '..') && !stristr($backup_dir.'/'.$backup['filename'], 'etc')) {
-						copy($backup_dir.'/'.$backup['filename'], $web['document_root'].'/backup/'.$backup['filename']);
-						chgrp($web['document_root'].'/backup/'.$backup['filename'], $web['system_group']);
-						chown($web['document_root'].'/backup/'.$backup['filename'], $web['system_user']);
-						chmod($web['document_root'].'/backup/'.$backup['filename'],0600);
-						$app->log('cp '.$backup_dir.'/'.$backup['filename'].' '.$web['document_root'].'/backup/'.$backup['filename'], LOGLEVEL_DEBUG);
-					}
+					backup::downloadBackup($backup['backup_format'], trim($backup['backup_password']), $backup_dir, $backup['filename'], $backup['backup_mode'], $backup['backup_type'], $web);
 				}
 
 				//* Restore a MongoDB backup
@@ -134,14 +127,7 @@ class backup_plugin {
 				}
 				
 				if($action_name == 'backup_delete') {
-					if(file_exists($backup_dir.'/'.$backup['filename']) && !stristr($backup_dir.'/'.$backup['filename'], '..') && !stristr($backup_dir.'/'.$backup['filename'], 'etc')) {
-						unlink($backup_dir.'/'.$backup['filename']);
-						
-						$sql = "DELETE FROM web_backup WHERE server_id = ? AND parent_domain_id = ? AND filename = ?";
-						$app->db->query($sql, $conf['server_id'], $backup['parent_domain_id'], $backup['filename']);
-						if($app->db->dbHost != $app->dbmaster->dbHost) $app->dbmaster->query($sql, $conf['server_id'], $backup['parent_domain_id'], $backup['filename']);
-						$app->log('unlink '.$backup_dir.'/'.$backup['filename'], LOGLEVEL_DEBUG);
-					}
+					backup::deleteBackup($backup['backup_format'], trim($backup['backup_password']), $backup_dir, $backup['filename'], $backup['backup_mode'], $backup['backup_type'], $backup['parent_domain_id']);
 				}
 
 				backup::unmount_backup_dir($conf['server_id']);
-- 
GitLab