From c52d9bbd0307b673c12f6a91a7da3553083a190a Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Thu, 27 Jul 2023 11:08:27 +0200 Subject: [PATCH] work on backup storages Signed-off-by: Michael Kaufmann --- actions/admin/settings/230.backup.php | 90 +++------- admin_customers.php | 4 +- composer.json | 1 + composer.lock | 131 +++++++-------- install/froxlor.sql.php | 6 +- install/updates/froxlor/update_2.1.inc.php | 6 +- lib/Froxlor/Api/Commands/BackupStorages.php | 4 +- lib/Froxlor/Api/Commands/Backups.php | 3 +- lib/Froxlor/Backup/Backup.php | 53 ++++++ lib/Froxlor/Backup/Storages/Ftp.php | 98 +++++++++++ lib/Froxlor/Backup/Storages/Local.php | 60 +++++++ lib/Froxlor/Backup/Storages/Rsync.php | 42 +++++ lib/Froxlor/Backup/Storages/S3.php | 42 +++++ lib/Froxlor/Backup/Storages/Sftp.php | 42 +++++ lib/Froxlor/Backup/Storages/Storage.php | 155 ++++++++++++++++++ .../Backup/Storages/StorageFactory.php | 12 ++ lib/Froxlor/Cron/Backup/BackupCron.php | 17 +- 17 files changed, 623 insertions(+), 143 deletions(-) create mode 100644 lib/Froxlor/Backup/Backup.php create mode 100644 lib/Froxlor/Backup/Storages/Ftp.php create mode 100644 lib/Froxlor/Backup/Storages/Local.php create mode 100644 lib/Froxlor/Backup/Storages/Rsync.php create mode 100644 lib/Froxlor/Backup/Storages/S3.php create mode 100644 lib/Froxlor/Backup/Storages/Sftp.php create mode 100644 lib/Froxlor/Backup/Storages/Storage.php create mode 100644 lib/Froxlor/Backup/Storages/StorageFactory.php diff --git a/actions/admin/settings/230.backup.php b/actions/admin/settings/230.backup.php index 01ba96ed10..63ebdbc06d 100644 --- a/actions/admin/settings/230.backup.php +++ b/actions/admin/settings/230.backup.php @@ -30,7 +30,7 @@ 'icon' => 'fa-solid fa-sliders', 'advanced_mode' => true, 'fields' => [ - 'system_backup_enabled' => [ + 'backup_enabled' => [ 'label' => lng('serversettings.backup_enabled'), 'settinggroup' => 'backup', 'varname' => 'enabled', @@ -40,74 +40,39 @@ 'overview_option' => true, 'cronmodule' => 'froxlor/backup' ], - 'system_backup_type' => [ - 'label' => lng('serversettings.backup_type'), + 'backup_default_storage' => [ + 'label' => lng('serversettings.backup_default_storage'), 'settinggroup' => 'backup', - 'varname' => 'type', + 'varname' => 'default_storage', 'type' => 'select', - 'default' => 'Local', - 'select_var' => [ - 'Local' => lng('serversettings.local'), - 'SFTP' => lng('serversettings.sftp'), - 'FTPS' => lng('serversettings.ftps'), - 'S3' => lng('serversettings.s3'), + 'default' => '1', + 'option_options_method' => [ + '\\Froxlor\\Backup\\Backup', + 'getBackupStorages' ], - 'save_method' => 'storeSettingField', - 'overview_option' => true, - ], - 'system_backup_region' => [ - 'label' => lng('serversettings.backup_region'), - 'settinggroup' => 'backup', - 'varname' => 'region', - 'type' => 'text', - 'default' => 'eu-central-1', - 'save_method' => 'storeSettingField', - ], - 'system_backup_bucket' => [ - 'label' => lng('serversettings.backup_bucket'), - 'settinggroup' => 'backup', - 'varname' => 'bucket', - 'type' => 'text', - 'default' => '', - 'save_method' => 'storeSettingField', - ], - 'system_backup_destination_path' => [ - 'label' => lng('serversettings.backup_destination_path'), - 'settinggroup' => 'backup', - 'varname' => 'destination_path', - 'type' => 'text', - 'string_type' => 'confdir', - 'default' => '/srv/backups/', - 'save_method' => 'storeSettingField', - ], - 'system_backup_hostname' => [ - 'label' => lng('serversettings.backup_hostname'), - 'settinggroup' => 'backup', - 'varname' => 'hostname', - 'type' => 'text', - 'default' => '', - 'save_method' => 'storeSettingField', + 'save_method' => 'storeSettingField' ], - 'system_backup_username' => [ - 'label' => lng('serversettings.backup_username'), + 'backup_default_retention' => [ + 'label' => lng('serversettings.backup_default_retention'), 'settinggroup' => 'backup', - 'varname' => 'username', - 'type' => 'text', - 'default' => '', + 'varname' => 'default_retention', + 'type' => 'number', + 'default' => 3, + 'min' => 0, 'save_method' => 'storeSettingField', ], - 'system_backup_password' => [ - 'label' => lng('serversettings.backup_password'), + 'backup_default_customer_access' => [ + 'label' => lng('serversettings.backup_default_customer_access'), 'settinggroup' => 'backup', - 'varname' => 'password', - 'type' => 'password', - 'default' => '', + 'varname' => 'default_customer_access', + 'type' => 'checkbox', + 'default' => true, 'save_method' => 'storeSettingField', ], - 'system_backup_pgp_public_key' => [ - 'label' => lng('serversettings.backup_pgp_public_key'), + 'backup_default_pgp_public_key' => [ + 'label' => lng('serversettings.backup_default_pgp_public_key'), 'settinggroup' => 'backup', - 'varname' => 'pgp_public_key', + 'varname' => 'default_pgp_public_key', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', @@ -116,15 +81,6 @@ 'checkPgpPublicKeySetting' ], ], - 'system_backup_retention' => [ - 'label' => lng('serversettings.backup_retention'), - 'settinggroup' => 'backup', - 'varname' => 'retention', - 'type' => 'number', - 'default' => 3, - 'min' => 0, - 'save_method' => 'storeSettingField', - ], ] ] ] diff --git a/admin_customers.php b/admin_customers.php index 6fe2cd188f..c7813e3b34 100644 --- a/admin_customers.php +++ b/admin_customers.php @@ -236,7 +236,7 @@ $result_json = BackupStorages::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $storagedata) { - $backup_storages[$storagedata['id']] = $storagedata['description'] . ' (' . $storagedata['type'] . ')'; + $backup_storages[$storagedata['id']] = "[" . $storagedata['type'] . "] " . html_entity_decode($storagedata['description']); } } catch (Exception $e) { /* just none */ @@ -335,7 +335,7 @@ $result_json = BackupStorages::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $storagedata) { - $backup_storages[$storagedata['id']] = $storagedata['description'] . ' (' . $storagedata['type'] . ')'; + $backup_storages[$storagedata['id']] = "[" . $storagedata['type'] . "] " . html_entity_decode($storagedata['description']); } } catch (Exception $e) { /* just none */ diff --git a/composer.json b/composer.json index cbc90494b8..7da177d4a9 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "ext-fileinfo": "*", "ext-gmp": "*", "ext-gd": "*", + "ext-ftp": "*", "phpmailer/phpmailer": "~6.0", "monolog/monolog": "^1.24", "robthree/twofactorauth": "^1.6", diff --git a/composer.lock b/composer.lock index d34f97e943..14e4df9b5b 100644 --- a/composer.lock +++ b/composer.lock @@ -561,16 +561,16 @@ }, { "name": "symfony/console", - "version": "v5.4.22", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8" + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3cd51fd2e6c461ca678f84d419461281bd87a0a8", - "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8", + "url": "https://api.github.com/repos/symfony/console/zipball/560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", "shasum": "" }, "require": { @@ -640,7 +640,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.22" + "source": "https://github.com/symfony/console/tree/v5.4.24" }, "funding": [ { @@ -656,7 +656,7 @@ "type": "tidelift" } ], - "time": "2023-03-25T09:27:28+00:00" + "time": "2023-05-26T05:13:16+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1547,16 +1547,16 @@ }, { "name": "twig/twig", - "version": "v3.5.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" + "reference": "5cf942bbab3df42afa918caeba947f1b690af64b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/5cf942bbab3df42afa918caeba947f1b690af64b", + "reference": "5cf942bbab3df42afa918caeba947f1b690af64b", "shasum": "" }, "require": { @@ -1565,15 +1565,10 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", + "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { "psr-4": { "Twig\\": "src/" @@ -1607,7 +1602,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.1" + "source": "https://github.com/twigphp/Twig/tree/v3.7.0" }, "funding": [ { @@ -1619,20 +1614,20 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:49:20+00:00" + "time": "2023-07-26T07:16:09+00:00" }, { "name": "voku/anti-xss", - "version": "4.1.41", + "version": "4.1.42", "source": { "type": "git", "url": "https://github.com/voku/anti-xss.git", - "reference": "55a403436494e44a2547a8d42de68e6cad4bca1d" + "reference": "bca1f8607e55a3c5077483615cd93bd8f11bd675" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/anti-xss/zipball/55a403436494e44a2547a8d42de68e6cad4bca1d", - "reference": "55a403436494e44a2547a8d42de68e6cad4bca1d", + "url": "https://api.github.com/repos/voku/anti-xss/zipball/bca1f8607e55a3c5077483615cd93bd8f11bd675", + "reference": "bca1f8607e55a3c5077483615cd93bd8f11bd675", "shasum": "" }, "require": { @@ -1678,7 +1673,7 @@ ], "support": { "issues": "https://github.com/voku/anti-xss/issues", - "source": "https://github.com/voku/anti-xss/tree/4.1.41" + "source": "https://github.com/voku/anti-xss/tree/4.1.42" }, "funding": [ { @@ -1702,7 +1697,7 @@ "type": "tidelift" } ], - "time": "2023-02-12T15:56:55+00:00" + "time": "2023-07-03T14:40:46+00:00" }, { "name": "voku/portable-ascii", @@ -2151,16 +2146,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -2201,22 +2196,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "pdepend/pdepend", - "version": "2.13.0", + "version": "2.14.0", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad" + "reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", - "reference": "31be7cd4f305f3f7b52af99c1cb13fc938d1cfad", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1121d4b04af06e33e9659bac3a6741b91cab1de1", + "reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1", "shasum": "" }, "require": { @@ -2250,9 +2245,15 @@ "BSD-3-Clause" ], "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], "support": { "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.13.0" + "source": "https://github.com/pdepend/pdepend/tree/2.14.0" }, "funding": [ { @@ -2260,7 +2261,7 @@ "type": "tidelift" } ], - "time": "2023-02-28T20:56:15+00:00" + "time": "2023-05-26T13:15:18+00:00" }, { "name": "phar-io/manifest", @@ -2582,16 +2583,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.14", + "version": "1.10.26", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c" + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d232901b09e67538e5c86a724be841bea5768a7c", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f", + "reference": "5d660cbb7e1b89253a47147ae44044f49832351f", "shasum": "" }, "require": { @@ -2640,7 +2641,7 @@ "type": "tidelift" } ], - "time": "2023-04-19T13:47:27+00:00" + "time": "2023-07-19T12:44:37+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2962,16 +2963,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.7", + "version": "9.6.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", "shasum": "" }, "require": { @@ -3045,7 +3046,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" }, "funding": [ { @@ -3061,7 +3062,7 @@ "type": "tidelift" } ], - "time": "2023-04-14T08:58:40+00:00" + "time": "2023-07-10T04:04:23+00:00" }, { "name": "sebastian/cli-parser", @@ -3363,16 +3364,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -3417,7 +3418,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -3425,7 +3426,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", @@ -4227,16 +4228,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v5.4.22", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "e1b7c1432efb4ad1dd89d62906187271e2601ed9" + "reference": "f0410c30a6c86bbce6c719c2b5cfc343362b982e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e1b7c1432efb4ad1dd89d62906187271e2601ed9", - "reference": "e1b7c1432efb4ad1dd89d62906187271e2601ed9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f0410c30a6c86bbce6c719c2b5cfc343362b982e", + "reference": "f0410c30a6c86bbce6c719c2b5cfc343362b982e", "shasum": "" }, "require": { @@ -4296,7 +4297,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.22" + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.25" }, "funding": [ { @@ -4312,20 +4313,20 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:02:45+00:00" + "time": "2023-06-24T09:45:28+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.21", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e75960b1bbfd2b8c9e483e0d74811d555ca3de9f" + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e75960b1bbfd2b8c9e483e0d74811d555ca3de9f", - "reference": "e75960b1bbfd2b8c9e483e0d74811d555ca3de9f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", "shasum": "" }, "require": { @@ -4360,7 +4361,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.21" + "source": "https://github.com/symfony/filesystem/tree/v5.4.25" }, "funding": [ { @@ -4376,7 +4377,7 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2023-05-31T13:04:02+00:00" }, { "name": "symfony/polyfill-php81", diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php index 4453aea7f9..39621cbb74 100644 --- a/install/froxlor.sql.php +++ b/install/froxlor.sql.php @@ -705,6 +705,8 @@ ('backup', 'enabled', 0), ('backup', 'default_storage', '1'), ('backup', 'default_customer_access', '1'), + ('backup', 'default_pgp_public_key', ''), + ('backup', 'default_retention', '3'), ('api', 'enabled', '0'), ('api', 'customer_default', '1'), ('2fa', 'enabled', '1'), @@ -1079,8 +1081,8 @@ `destination_path` varchar(255) NOT NULL, `hostname` varchar(255) NULL, `username` varchar(255) NULL, - `password` varchar(255) NULL, - `pgp_public_key` varchar(255) NULL, + `password` text, + `pgp_public_key` text, `retention` int(3) NOT NULL DEFAULT 3, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; diff --git a/install/updates/froxlor/update_2.1.inc.php b/install/updates/froxlor/update_2.1.inc.php index 7d412fdbb7..53432dd9e4 100644 --- a/install/updates/froxlor/update_2.1.inc.php +++ b/install/updates/froxlor/update_2.1.inc.php @@ -73,8 +73,8 @@ `destination_path` varchar(255) NOT NULL, `hostname` varchar(255) NULL, `username` varchar(255) NULL, - `password` varchar(255) NULL, - `pgp_public_key` varchar(255) NULL, + `password` text, + `pgp_public_key` text, `retention` int(3) NOT NULL DEFAULT 3, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; @@ -105,6 +105,8 @@ Settings::AddNew('backup.enabled', 0); Settings::AddNew('backup.default_storage', 1); Settings::AddNew('backup.default_customer_access', 1); + Settings::AddNew('backup.default_pgp_public_key', ''); + Settings::AddNew('backup.default_retention', 3); Update::lastStepStatus(0); Update::showUpdateStep("Adjusting cronjobs"); diff --git a/lib/Froxlor/Api/Commands/BackupStorages.php b/lib/Froxlor/Api/Commands/BackupStorages.php index 47aa4098ba..2952162e7e 100644 --- a/lib/Froxlor/Api/Commands/BackupStorages.php +++ b/lib/Froxlor/Api/Commands/BackupStorages.php @@ -73,8 +73,8 @@ public function listing() $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list backup storages"); $query_fields = []; $result_stmt = Database::prepare(" - SELECT * FROM `" . TABLE_PANEL_BACKUP_STORAGES . "` - "); + SELECT * FROM `" . TABLE_PANEL_BACKUP_STORAGES . "` ". $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit() + ); Database::pexecute($result_stmt, $query_fields, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { diff --git a/lib/Froxlor/Api/Commands/Backups.php b/lib/Froxlor/Api/Commands/Backups.php index 7804c416f0..b5f556e116 100644 --- a/lib/Froxlor/Api/Commands/Backups.php +++ b/lib/Froxlor/Api/Commands/Backups.php @@ -95,7 +95,8 @@ public function listing() FROM `" . TABLE_PANEL_BACKUPS . "` `b` LEFT JOIN `" . TABLE_PANEL_ADMINS . "` `a` USING(`adminid`) WHERE `b`.`customerid` IN (" . implode(', ', $customer_ids) . ") - "); + " . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit() + ); Database::pexecute($result_stmt, $query_fields, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { diff --git a/lib/Froxlor/Backup/Backup.php b/lib/Froxlor/Backup/Backup.php new file mode 100644 index 0000000000..005df6820c --- /dev/null +++ b/lib/Froxlor/Backup/Backup.php @@ -0,0 +1,53 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +namespace Froxlor\Backup; + +use Froxlor\Database\Database; +use PDO; + +class Backup +{ + /** + * returns an array of existing backup-storages + * in our database for the settings-array + * + * @return array + */ + public static function getBackupStorages(): array + { + $storages_array = [ + 0 => lng('backup.storage_none') + ]; + // get all storages + $result_stmt = Database::query("SELECT id, type, description FROM `" . TABLE_PANEL_BACKUP_STORAGES . "` ORDER BY type, description"); + while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { + if (!isset($storages_array[$row['id']]) && !in_array($row['id'], $storages_array)) { + $storages_array[$row['id']] = "[" . $row['type'] . "] " . html_entity_decode($row['description']); + } + } + return $storages_array; + } +} diff --git a/lib/Froxlor/Backup/Storages/Ftp.php b/lib/Froxlor/Backup/Storages/Ftp.php new file mode 100644 index 0000000000..3e63065ed7 --- /dev/null +++ b/lib/Froxlor/Backup/Storages/Ftp.php @@ -0,0 +1,98 @@ +sData['storage']['hostname'] ?? ''; + $username = $this->sData['storage']['username'] ?? ''; + $password = $this->sData['storage']['password'] ?? ''; + if (!empty($hostname) && !empty($username) && !empty($password)) { + $tmp = explode(":", $hostname); + $hostname = $tmp[0]; + $port = $tmp[1] ?? 21; + $this->ftp_conn = ftp_connect($hostname, $port); + if ($this->ftp_conn === false) { + throw new Exception('Unable to connect to ftp-server "' . $hostname . ':' . $port . '"'); + } + if (!ftp_login($this->ftp_conn, $username, $password)) { + throw new Exception('Unable to login to ftp-server "' . $hostname . ':' . $port . '"'); + } + return $this->changeToCorrectDirectory(); + } + throw new Exception('Empty hostname for FTP backup storage'); + } + + /** + * @param string $filename + * @param string $tmp_source_directory + * @return bool + * @throws Exception + */ + protected function putFile(string $filename, string $tmp_source_directory): bool + { + $source = FileDir::makeCorrectFile($tmp_source_directory . "/" . $filename); + $target = basename($filename); + if (file_exists($source) && !file_exists($target)) { + return ftp_put($this->ftp_conn, $target, $source, FTP_BINARY); + } + return false; + } + + /** + * @param string $filename + * @return bool + * @throws Exception + */ + protected function rmFile(string $filename): bool + { + $target = basename($filename); + if (ftp_size($this->ftp_conn, $target) >= 0) { + return ftp_delete($this->ftp_conn, $target); + } + return true; + } + + /** + * @return bool + */ + public function shutdown(): bool + { + return ftp_close($this->ftp_conn); + } + + /** + * @return bool + * @throws Exception + */ + private function changeToCorrectDirectory(): bool + { + $dirs = explode("/", $this->getDestinationDirectory()); + array_shift($dirs); + if (count($dirs) > 0 && !empty($dirs[0])) { + foreach ($dirs as $dir) { + if (empty($dir)) { + continue; + } + if (!@ftp_chdir($this->ftp_conn, $dir)) { + ftp_mkdir($this->ftp_conn, $dir); + ftp_chmod($this->ftp_conn, 0700, $dir); + ftp_chdir($this->ftp_conn, $dir); + } + } + return true; + } + return ftp_chdir($this->ftp_conn, "/"); + } +} diff --git a/lib/Froxlor/Backup/Storages/Local.php b/lib/Froxlor/Backup/Storages/Local.php new file mode 100644 index 0000000000..b43df89f74 --- /dev/null +++ b/lib/Froxlor/Backup/Storages/Local.php @@ -0,0 +1,60 @@ +getDestinationDirectory())) { + return mkdir($this->getDestinationDirectory(), 0700, true); + } + return true; + } + + /** + * @param string $filename + * @param string $tmp_source_directory + * @return bool + * @throws Exception + */ + protected function putFile(string $filename, string $tmp_source_directory): bool + { + $source = FileDir::makeCorrectFile($tmp_source_directory . "/" . $filename); + $target = FileDir::makeCorrectFile($this->getDestinationDirectory() . "/" . $filename); + if (file_exists($source) && !file_exists($target)) { + return rename($source, $target); + } + return false; + } + + /** + * @param string $filename + * @return bool + * @throws Exception + */ + protected function rmFile(string $filename): bool + { + $target = FileDir::makeCorrectFile($this->getDestinationDirectory() . "/" . $filename); + if (file_exists($target)) { + return @unlink($target); + } + return true; + } + + /** + * @return bool + */ + public function shutdown(): bool + { + return true; + } +} diff --git a/lib/Froxlor/Backup/Storages/Rsync.php b/lib/Froxlor/Backup/Storages/Rsync.php new file mode 100644 index 0000000000..de37a1cba4 --- /dev/null +++ b/lib/Froxlor/Backup/Storages/Rsync.php @@ -0,0 +1,42 @@ +sData = $storage_data; + $this->tmpDirectory = sys_get_temp_dir(); + } + + /** + * Validate sData, open connection to target storage, etc. + * + * @return bool + */ + abstract public function init(): bool; + + /** + * Disconnect / clean up connection if needed + * + * @return bool + */ + abstract public function shutdown(): bool; + + /** + * prepare files to back up (e.g. create archive or similar) and fill $filesToStore + * + * @return void + */ + public function prepareFiles(): void + { + $this->filesToStore = []; + + // create archive of web, mail and database data + + // create json-info-file + } + + /** + * @param string $filename + * @param string $tmp_source_directory + * @return bool + */ + abstract protected function putFile(string $filename, string $tmp_source_directory): bool; + + /** + * @param string $filename + * @return bool + */ + abstract protected function rmFile(string $filename): bool; + + /** + * @return bool + * @throws Exception + */ + public function removeOld(): bool + { + // retention in days + $retention = $this->sData['storage']['retention'] ?? 3; + // keep date + $keepDate = new \DateTime(); + $keepDate->setTime(0, 0, 0, 1); + // subtract retention days + $keepDate->sub(new \DateInterval('P' . $retention . 'D')); + // select target backups to remove for this storage-id and customer + $sel_stmt = Database::prepare(" + SELECT * FROM `" . TABLE_PANEL_BACKUPS . "` + WHERE `created_at` < :keepdate + AND `storage_id` = :sid + AND `customerid` = :cid + "); + Database::pexecute($sel_stmt, [ + 'keepdate' => $keepDate->format('U'), + 'sid' => $this->sData['backup'], + 'cid' => $this->sData['customerid'] + ]); + while ($oldBackup = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { + $this->rmFile($oldBackup['filename']); + } + } + + /** + * @return string + * @throws Exception + */ + public function getDestinationDirectory(): string + { + return FileDir::makeCorrectDir($this->sData['storage']['destination_path'] ?? "/"); + } + + /** + * Create backup-archive/file from $filesToStore and call putFile() + * + * @return bool + * @throws Exception + */ + public function createFromFiles(): bool + { + if (empty($this->filesToStore)) { + return false; + } + + $filename = FileDir::makeCorrectFile("/backup-" . $this->sData['loginname'] . "-" . date('c') . ".tar.gz"); + + // @todo create archive $filename from $filesToStore + + // determine filesize (use stat locally here b/c files are possibly large and php's filesize() can't handle them) + $sizeCheckFile = FileDir::makeCorrectFile($this->tmpDirectory . "/" . $filename); + $fileSizeOutput = FileDir::safe_exec('/usr/bin/stat -c "%s" ' . escapeshellarg($sizeCheckFile)); + $fileSize = (int)array_shift($fileSizeOutput); + + // add entry to database and upload/store file + $this->addEntry($filename, $fileSize); + return $this->putFile($filename, $this->tmpDirectory); + } + + /** + * @param string $filename + * @param int $fileSize + * @return void + * @throws Exception + */ + private function addEntry(string $filename, int $fileSize): void + { + $ins_stmt = Database::prepare(" + INSERT INTO `" . TABLE_PANEL_BACKUPS . "` SET + `adminid` = :adminid, + `customerid` = :customerid, + `loginname` = :loginname, + `size` = :size, + `storage_id` = :sid, + `filename` = :filename, + `created_at` = UNIX_TIMESTAMP() + "); + Database::pexecute($ins_stmt, [ + 'adminid' => $this->sData['adminid'], + 'customerid' => $this->sData['customerid'], + 'loginname' => $this->sData['loginname'], + 'size' => $fileSize, + 'sid' => $this->sData['backup'], + 'filename' => $filename + ]); + } +} diff --git a/lib/Froxlor/Backup/Storages/StorageFactory.php b/lib/Froxlor/Backup/Storages/StorageFactory.php new file mode 100644 index 0000000000..bd2b7f4e3e --- /dev/null +++ b/lib/Froxlor/Backup/Storages/StorageFactory.php @@ -0,0 +1,12 @@ +logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'BackupCron: disabled - exiting'); return -1; } @@ -71,11 +73,22 @@ public static function run() self::runFork([self::class, 'handle'], $customers); } + /** + * @throws Exception + */ private static function handle(array $userdata) { echo "BackupCron: started - creating customer backup for user " . $userdata['loginname'] . "\n"; - echo json_encode($userdata['storage']) . "\n"; + $backupStorage = StorageFactory::fromType($userdata['storage']['type'], $userdata); + // initialize storage + $backupStorage->init(); + // do what is required to obtain files/archives and move/upload + $backupStorage->prepareFiles(); + // upload/move to target + $backupStorage->createFromFiles(); + // remove by retention + $backupStorage->removeOld(); echo "BackupCron: finished - creating customer backup for user " . $userdata['loginname'] . "\n"; }