From 4cd4868518fb413b3813fa6fc03f7d6e33878823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mu=C3=B1oz?= 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').' MB'; + if($backup_mode == "borg") { + $rec['filesize'] = '*' + . $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 + */ + protected static function getBackupDbExtension($format) + { + $prefix = '.sql'; + switch ($format) { + case 'gzip': + return $prefix . '.gz'; + case 'bzip2': + return $prefix . '.bz2'; + case 'xz': + return $prefix . '.xz'; + case 'zip': + case 'zip_bzip2': + return '.zip'; + case 'rar': + return '.rar'; + } + if (strpos($format, "7z_") === 0) { + return $prefix . '.7z'; + } + return null; + } + + /** + * Returns file extension for specified backup format + * @param string $format backup format + * @return string|null + * @author Ramil Valitov + */ + protected static function getBackupWebExtension($format) + { + switch ($format) { + case 'tar_gzip': + return '.tar.gz'; + case 'tar_bzip2': + return '.tar.bz2'; + case 'tar_xz': + return '.tar.xz'; + case 'zip': + case 'zip_bzip2': + return '.zip'; + case 'rar': + return '.rar'; + } + if (strpos($format, "tar_7z_") === 0) { + return '.tar.7z'; + } + return null; + } + 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 @@

{tmpl_var name='server_name'}

+ +
+ {tmpl_var name='backup_missing_utils_txt'} {tmpl_var name='missing_utils'} +
+
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 + * @author Jorge Muñoz (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 + */ + 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 + * @author Jorge Muñoz */ 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.+)_\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.+)_\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 + * @author Jorge Muñoz */ 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 + */ + 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.+)_\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 + */ + 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.+)_\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 + */ + 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 + */ + 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 + * @author Jorge Muñoz */ 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 + */ + 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 + * @author Jorge Muñoz * @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 + * @author Jorge Muñoz * @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 + */ + 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 + */ + 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