├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── composer.json ├── composer.lock ├── config └── bootstrap.php ├── docs ├── Common issues.md ├── Configuration.md ├── Create backup copies.md ├── Export backups as cron jobs.md ├── How to use commands.md ├── How to use the BackupExport utility.md ├── How to use the BackupImport utility.md ├── How to use the BackupManager utility.md └── Send backups via email.md ├── phpcs.xml ├── phpstan.neon ├── resources └── locales │ ├── database_backup.pot │ └── it │ └── database_backup.po ├── src ├── Command │ ├── ExportCommand.php │ └── ImportCommand.php ├── Compression.php ├── Console │ └── Command.php ├── Executor │ ├── AbstractExecutor.php │ ├── MysqlExecutor.php │ ├── PostgresExecutor.php │ └── SqliteExecutor.php ├── OperationType.php ├── Plugin.php ├── TestSuite │ └── TestCase.php └── Utility │ ├── AbstractBackupUtility.php │ ├── BackupExport.php │ ├── BackupImport.php │ └── BackupManager.php └── version /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 0' # weekly 8 | 9 | jobs: 10 | testsuite: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | php-version: ['8.3', '8.4'] 16 | db-type: [sqlite, mysql, pgsql] 17 | prefer-lowest: [''] 18 | include: 19 | - php-version: '8.1' 20 | db-type: 'mysql' 21 | - php-version: '8.2' 22 | db-type: 'mysql' 23 | - php-version: '8.1' 24 | db-type: 'mysql' 25 | prefer-lowest: 'prefer-lowest' 26 | - php-version: '8.3' 27 | db-type: 'mysql' 28 | prefer-lowest: 'prefer-lowest' 29 | - php-version: '8.4' 30 | db-type: 'mysql' 31 | prefer-lowest: 'prefer-lowest' 32 | 33 | steps: 34 | - name: Setup MySQL 8.0 35 | if: matrix.db-type == 'mysql' 36 | run: | 37 | sudo service mysql start 38 | mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE test;' 39 | 40 | - name: Setup PostgreSQL latest 41 | if: matrix.db-type == 'pgsql' 42 | run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=test -p 5432:5432 -d postgres:9.6 43 | 44 | - uses: actions/checkout@v4 45 | 46 | - name: Validate composer.json and composer.lock 47 | run: composer validate --strict 48 | 49 | - name: Setup PHP 50 | uses: shivammathur/setup-php@v2 51 | with: 52 | php-version: ${{ matrix.php-version }} 53 | extensions: mbstring, intl, pdo_${{ matrix.db-type }} 54 | 55 | - name: Cache Composer packages 56 | id: composer-cache 57 | uses: actions/cache@v4 58 | with: 59 | path: vendor 60 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 61 | restore-keys: | 62 | ${{ runner.os }}-php- 63 | 64 | - name: Composer install 65 | run: | 66 | if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then 67 | composer update --prefer-lowest --prefer-stable 68 | else 69 | composer update 70 | fi 71 | 72 | - name: Composer show 73 | run: composer show 74 | 75 | - name: Wait for MySQL 76 | if: matrix.db-type == 'mysql' || matrix.db-type == 'mariadb' 77 | run: while ! `mysqladmin ping -h 127.0.0.1 --silent`; do printf 'Waiting for MySQL...\n'; sleep 2; done; 78 | 79 | - name: Run PHPUnit 80 | run: | 81 | if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export db_dsn='sqlite:////tmp/cakephp-database-backup/test.sq3'; fi 82 | if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export db_dsn='mysql://root:root@127.0.0.1/test?encoding=utf8'; fi 83 | if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export db_dsn='postgres://postgres:postgres@127.0.0.1/test'; fi 84 | 85 | if [[ ${{ matrix.php-version }} == '8.3' ]]; then 86 | export XDEBUG_MODE=coverage 87 | vendor/bin/phpunit --coverage-clover=coverage.xml 88 | else 89 | vendor/bin/phpunit 90 | fi 91 | 92 | - name: Submit code coverage 93 | if: matrix.php-version == '8.3' 94 | uses: codecov/codecov-action@v5 95 | env: 96 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 97 | 98 | cs-stan: 99 | name: Coding Standard & Static Analysis 100 | runs-on: ubuntu-latest 101 | 102 | steps: 103 | - uses: actions/checkout@v4 104 | 105 | - name: Setup PHP 106 | uses: shivammathur/setup-php@v2 107 | with: 108 | php-version: '8.1' 109 | coverage: none 110 | tools: cs2pr 111 | 112 | - name: Composer install 113 | run: composer update 114 | 115 | - name: Run PHP CodeSniffer 116 | continue-on-error: true 117 | run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml 118 | 119 | - name: Show PHPCS results in PR 120 | run: cs2pr ./phpcs-report.xml 121 | 122 | - name: Run phpstan 123 | if: success() || failure() 124 | env: 125 | db_dsn: 'sqlite:///:memory:' 126 | run: vendor/bin/phpstan analyse --error-format=github 127 | 128 | 129 | testsuite-windows: 130 | runs-on: windows-2022 131 | name: Windows - PHP 8.2 & Mysql 8.0 132 | 133 | env: 134 | EXTENSIONS: mbstring, intl, pdo_mysql 135 | PHP_VERSION: '8.2' 136 | 137 | steps: 138 | - uses: actions/checkout@v4 139 | 140 | - name: Setup PHP 141 | uses: shivammathur/setup-php@v2 142 | with: 143 | php-version: ${{ env.PHP_VERSION }} 144 | extensions: ${{ env.EXTENSIONS }} 145 | ini-values: apc.enable_cli = 1, extension = php_fileinfo.dll 146 | coverage: none 147 | 148 | - uses: ankane/setup-mysql@v1 149 | with: 150 | database: test 151 | mysql-version: 8.0 152 | 153 | - name: Cache Composer packages 154 | id: composer-cache 155 | uses: actions/cache@v4 156 | with: 157 | path: vendor 158 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 159 | restore-keys: | 160 | ${{ runner.os }}-php- 161 | 162 | - name: Composer install 163 | run: composer update 164 | 165 | - name: Run PHPUnit 166 | env: 167 | db_dsn: 'mysql://root@localhost/test' 168 | run: | 169 | vendor/bin/phpunit --group mysql 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.x branch 2 | ## 2.15 branch 3 | ### 2.15.0 4 | * `BackupManager::index()` and `BackupManager::rotate()` methods have been deprecated and will be removed in a future 5 | release. At this point, the entire `BackupManager` class is considered deprecated and will be removed; 6 | * the `BackupExport::rotate()` method has been deprecated and will be removed in a future release; 7 | * the `DatabaseBackup.chmod` configuration is deprecated and will be removed in a future release; 8 | * the `IndexCommand` had been deprecated and was removed; 9 | * the `--rotate` option of `ExportCommand` had been deprecated and was removed; 10 | * the magic method `AbstractBackupUtility::__get()` had been deprecated and has been removed; 11 | * `BackupTrait` had been deprecated and has been removed; 12 | * `AbstractBackupUtility::getDriver()` had been deprecated and has been removed. 13 | 14 | ## 2.14 branch 15 | ### 2.14.4 16 | * the `IndexCommand` has been deprecated and will be removed in a future release. Instead, use the commands available to 17 | your system. For example, for Unix you might use `ls -g -A -t -r backups/` (and `ls --help` for any help); 18 | * the `--rotate` option of `ExportCommand` has been deprecated and will be removed in a future release; 19 | * all links to the wiki have been removed (which, remember, has been replaced by the `docs/` folder inside the branch, 20 | since version `2.14.3`). 21 | 22 | ### 2.14.3 23 | * the `Plugin` class no longer implements the `console()` method (which described the available commands), leaving the 24 | application to take care of auto-discovering them; 25 | * the `AbstractBackupUtility::__call()` method now also supports uppercase named properties (bug fixed). This provides 26 | the `getConnection()` magic method; 27 | * `AbstractBackupUtility::makeAbsoluteFilename()` has become `makeAbsolutePath()` and `Command::makeRelativeFilename()` 28 | has become `makeRelativePath()`; 29 | * the signature of `AbstractBackupUtility::__call()` method has changed (from 30 | `__call(string $name, array $arguments = []): mixed` to `__call(string $method, array $args): mixed`); 31 | * increased use of fake backup files for testing, where there is no need to use real backups for testing; 32 | * the entire wiki has been copied to the [docs/](docs) directory. This choice was made to allow the maintenance of information related to old versions (i.e. the branch you are viewing/using). 33 | 34 | ### 2.14.2 35 | * the constructor method `AbstractBackupUtility` now accepts `$Connection` as an optional argument, as string (e.g. 36 | `default` or `test`) or a `ConnectionInterface` instance. This means that by instantiating `BackupExport` or 37 | `BackupImport` (which extend `AbstractBackupUtility`) it is possible to set a connection beyond the default one; 38 | * `ExportCommand` and `ImportCommand` now accept the `--connection` option to set a connection other than the default; 39 | * due to the two new introductions just described, the `DatabaseBackup.connection` configuration name is deprecated 40 | and will be removed in a later release; 41 | * configuration names `DatabaseBackup.mysql`, `DatabaseBackup.postgres` and `DatabaseBackup.sqlite` are now deprecated 42 | and will be removed in a future release (backwards compatibility will be maintained up to branch `2.15.x`). Use the 43 | `DatabaseBackup.Mysql`, `DatabaseBackup.Postgres` and `DatabaseBackup.Sqlite` names instead in your bootstrap (the 44 | driver name begins with a capital letter); 45 | * removed from `ExportCommand::buildOptionParser()` the reference (as a helper) to the old `send` option, which was no 46 | longer present (it had been forgotten); 47 | * starting with CakePHP 5.2, [returning values from event listeners/callbacks is deprecated](https://book.cakephp.org/5/en/appendices/5-2-migration-guide.html#event). 48 | For this reason, the `beforeExport()` and `beforeImport()` event methods provided by `AbstractExecutor`, 49 | `MysqlExecutor` and `SqliteExecutor` no longer return a boolean. Optionally these methods can use the 50 | `$Event->setResult()` method to set a result. Currently, the `void` return type is not explicit to ensure backwards 51 | compatibility, it will be made explicit in a future release; 52 | * removed Psalm (for the same reasons given for CakePHP, see [here](https://github.com/cakephp/cakephp/pull/18340)). 53 | 54 | ### 2.14.1 55 | * many improvements for the `AbstractExecutor` abstract class; 56 | * method `AbstractBackupUtility::getExecutor()` now accepts the optional `$Connection` argument. The `getConnection()` 57 | method has been removed; 58 | * `getExecutable()`, `getExportExecutable()` and `getImportExecutable()` methods provided by `AbstractExecutor` have 59 | been renamed as `getCommand()`, `getExportCommand()` and `getImportCommand()`. This is because they never actually 60 | returned "executables", but rather commands. This makes their name clearer. For `getExportExecutable()` and 61 | `getImportExecutable()` methods backwards compatibility is ensured via the magic method `_call()` (with deprecation); 62 | * significant improvement for `SqlExecutor`. Also, improved related tests; 63 | * the `DriverTestCase` abstract class, used in particular for old, no longer existing "Driver" classes, has been removed. 64 | Tests involving "Executor" classes do not use this class, as they have no need for it. The `PostgresExecutorTest` test 65 | class has also been removed, since the class it covers, by default, does not implement anything that the parent class 66 | does not; 67 | * all PHPUnit deprecations have finally been removed; 68 | * added `symfony/polyfill-php83`. This allowed the `#[Override]` attribute to be added to all affected methods; 69 | * we also begin to apply the `UsesClass` attribute and remove the old `@uses` tag; 70 | * updated for `cakedc/cakephp-phpstan` to `^4.0` (and so `phpstan/phpstan` to `2.1.8`). 71 | 72 | ### 2.14.0 73 | * added new magic `AbstractBackupUtility::__call()` method. This allows `BackupExport` and `BackupImport` to access the 74 | property via the magic methods `getX()` (be careful not to confuse the `getCompression()` method, which now returns 75 | the `$compression` property of `BackupExport`, with the old method provided by `BackupTrait`, which had been 76 | deprecated and has now been removed); 77 | * class `DatabaseBackup\Driver\AbstractDriver` has become `DatabaseBackup\Executor\AbstractExecutor`, class 78 | `DatabaseBackup\Driver\Mysql` has become `DatabaseBackup\Executor\MysqlExecutor`, class `DatabaseBackup\Driver\Postgres` 79 | has become `DatabaseBackup\Executor\PostgresExecutor` and class `DatabaseBackup\Driver\Sqlite` has become 80 | `DatabaseBackup\Executor\SqliteExecutor`. Aliases have been added to old classes for backwards compatibility, but will 81 | be removed in a future release; 82 | * added new `AbstractBackupUtility::getExecutor()` that gets the `Executor` instance (old `Driver` classes now renamed) 83 | according to the connection; 84 | * passing the `$compression` argument as a string or `null` to `BackupExport::compression()` had been deprecated and has 85 | been removed (backwards compatibility removed); 86 | * the `BackupManager::index` methods no longer returns, in the array for each file, the `filename` key; 87 | * the `BackupExport::filename()` and `BackupImport::filename()` methods now throw an `IOException` exception (rather 88 | than a `LogicException`) if the target (directory) is not writable/the filename already exists/the filename is not 89 | readable; 90 | * the `BackupExport::export()` and `BackupImport::import()` methods now throw a `RuntimeException` exception (rather 91 | than a `LogicException`) when export/import fails; 92 | * the `BackupImport::import()` methods now throws a `BadMethodCallException` exception (rather than a `LogicException`) 93 | when the filename has not been set; 94 | * fixed a deprecation regarding a `Finder::sort()` method call; 95 | * the `TestCase::createSomeBackups()` method has been improved; 96 | * tests for `MysqlExecutor` (hence `MysqlExecutorTest`) can be run regardless of the driver in use; 97 | * the `AbstractBackupUtility::getDriver()` method is deprecated and will be removed in a future release. Use instead the 98 | `getExecutor()` method; 99 | * the `AbstractBackupUtility::__get()` method is deprecated and will be removed in a future release; 100 | * `DeleteAllCommand`, `RotateCommand` and `SendCommand` classes had been deprecated and have been removed; 101 | * the `getConnection()` and `getDriverName()` methods provided by `BackupTrait` are deprecated and will be removed in a 102 | future release. The entire `BackupTrait` trait is now deprecated and will be removed in a future release; 103 | * `getAbsolutePath()`, `getCompression()`, `getExtension()` and `getValidCompressions()` methods provided by 104 | `BackupTrait` had been deprecated and has been removed; 105 | * all classes, methods and code related to sending backups via email had been deprecated, and now they have been 106 | removed. So, the `BackupManager::send()` method (and, consequently, the internal `BackupManager::getEmailInstance()` 107 | method), the `BackupExport::send()` method and the `send` option for the `ExportCommand` have been removed; 108 | * the `delete()` and `deleteAll()` methods provided by `BackupManager` had been deprecated and have been removed; 109 | * the `rtr()` global function had been deprecated and has been removed. 110 | 111 | ## 2.13 branch 112 | ### 2.13.6 113 | * now all `Driver` classes (through the `AbstractDriver` class) have `$Connection` as their constructor argument, and 114 | therefore as their property. This argument is automatically passed by the `AbstractBackupUtility::getDriver()` method 115 | (which was already happening previously, but without any consequences). 116 | 117 | ### 2.13.5 118 | * added new `DatabaseBackup\Compression` enum, with some methods useful for the complete management of compressions; 119 | * the `BackupExport::compression()` method now accepts a `Compression` value as its `$compression` argument. String and 120 | `null` values are still supported, but are now deprecated and will be removed in a future release. Additionally, if an 121 | invalid string is now passed as an argument, a `InvalidArgumentException` exception is thrown; 122 | * the `BackupManager::index()` method now returns, in the array for each file, a `Compression` enum value for the 123 | `compression` key (rather than a string or `null`), the absolute path for the `path` key, and the basename for the 124 | `basename` key. The `filename` key is still returned, but will be removed in version 2.14.0 (`basename` is more 125 | efficient). The `extension` key has already been removed, as it is now useless. The `IndexCommand` instead still 126 | carries the compression as a string (it couldn't be otherwise), while it also no longer reports the extension (also 127 | useless in this case); 128 | * the `BackupExport` class no longer directly handles the backup extension, which is automatically deduced from the 129 | value of `Compression`, now set by default to `Compression::None` (no compression) and which can always be changed 130 | with the `compression()` and (indirectly) `filename()` methods. For this reason, the `BackupExport::$extension` 131 | property no longer exists; 132 | * except for `ExportCommand` and `ImportCommand`, all other `Command` classes (including deprecated ones) now directly 133 | extend `Cake\Console\BaseCommand`. This means that they will no longer display connection information by default, but 134 | that makes sense since those classes only work on the filesystem; 135 | * added new `Command::makeRelativeFilename()` method. This will replace the global `rtr()` function, since only 136 | `Command` classes really need it; 137 | * the `BackupImport::filename()` method uses `Compression` to check the validity of the file you want to import (so it 138 | no longer checks its extension). This will throw a `ValueError` exception for invalid files; 139 | * the global test functions `createBackup()` and `createSomeBackups()` are now methods of the `TestCase` class (as they 140 | should be). The `createBackup()` method now has the `$fakeBackup` argument (`false` by default), which allows you to 141 | create a fake backup file (i.e. an empty file) Added tests; 142 | * added new `AbstractBackupUtility::makeAbsoluteFilename()` method. Since the `BackupTrait::getAbsolutePath()` method is 143 | now deprecated (see below), it provides the `BackupExport` and `BackupImport` classes (the only ones that really need 144 | it) with a method to construct absolute paths; 145 | * added `OperationType` enum, which is used by the `AbstractDriver::getExecutable()` private method; 146 | * the `AbstractDriver::getBinary()` method can now accept a `Compression` value as an argument (in addition to a 147 | string). Invalid values will now throw an `InvalidArgumentException` (rather than a `LogicException`); 148 | * the `BackupManager::rotate()` method throws an `InvalidArgumentException` (and no longer `LogicException`) for an 149 | invalid `$rotate` value. The method description has been corrected; 150 | * the code, introduced in version `2.13.3`, that allows paths relative to `ROOT`, has been moved from 151 | `BackupTrait::getAbsolutePath()` method to `ImportCommand::execute()`, since it is the only one that takes advantage 152 | of it; 153 | * the `DATABASE_BACKUP_EXTENSIONS` constant no longer exists, as it is no longer needed due to the `Compression` enum; 154 | * the `rtr()` global function is deprecated and will be removed in a future release; 155 | * the `RotateCommand` class is deprecated and will be removed in a later release. For this reason, the `ExportCommand` 156 | class now uses the `BackupManager::rotate()` method to continue supporting the `--rotate` option; 157 | * `getAbsolutePath()`, `getCompression()`, `getExtension()` and `getValidCompressions()` methods provided by 158 | `BackupTrait` are deprecated. They will be removed in a future release; 159 | * the `delete()` and `deleteAll()` methods provided by `BackupManager` are deprecated. They will be removed in a future 160 | release. The few methods that need to delete files (e.g. rotation methods) implement the necessary code themselves; 161 | * compatibility with the transition from `_cake_core_` to `_cake_translations_` expected in CakePHP 5.1; 162 | * the `BackupExport::$defaultExtension` property no longer exists (by now it had become useless); 163 | * updated for the latest version of psalm. 164 | 165 | ### 2.13.4 166 | * fixed [bug #119](https://github.com/mirko-pagliai/cakephp-database-backup/issues/119): `BackupManager` ignored the timezone of backup files, and consequently also `IndexCommand`; 167 | * fixed [bug #111](https://github.com/mirko-pagliai/cakephp-database-backup/issues/111): for Mysql it first looks for 168 | `mariadb` and `mariadb-dump` executables, otherwise `mysql` and `mysqldump` executables; 169 | * all classes, methods and code related to sending backups via email are now deprecated. So, the `BackupManager::send()` 170 | method (and, consequently, also the internal `BackupManager::getEmailInstance()` method), the `BackupExport::send()` 171 | method, the `SendCommand` class and the `send` option for the `ExportCommand` are deprecated. All of these will be 172 | removed in a later release. No replacement is provided; 173 | * setting the `DatabaseBackup.mailSender` value of the configuration is deprecated (bootstrap checks that the value 174 | has not been set by the user); 175 | * the `DeleteAllCommand` is deprecated. Will be removed in a future release; 176 | * added tests for php 8.4; 177 | * all chainable methods of `BackupExport` and `BackupImport` classes now have the typehint for returning self. Updated 178 | descriptions; 179 | * updated `phpunit` to `^10.5.5 || ^11.1.3`; 180 | * updated `psalm` to `6.x`; 181 | * uses `cakedc/cakephp-phpstan`; 182 | * the old `FrozenTime` classes have been replaced with `DateTime` (which it was an alias for); 183 | * extensive revision of descriptions and tags of all classes and methods; 184 | * removed some errors related to phpcs, phpstan and psalm, previously silenced; 185 | * the `README` file has been updated for compatibility with older versions of CakePHP and PHP (branches have been 186 | removed and older versions are available as tags); 187 | * overall updated `README` file, updated links to CakePHP documentation. Some information has been moved from the 188 | `README` file to the (new) [Common issues](https://github.com/mirko-pagliai/cakephp-database-backup/wiki/Common-issues) wiki page. 189 | 190 | ### 2.13.3 191 | * added `--reverse` option for the `IndexCommand` ([issue #96](https://github.com/mirko-pagliai/cakephp-database-backup/issues/96)); 192 | * the `BackupTrait::getAbsolutePath()` method is now able to recognize a path relative to its `ROOT`, so as to be able 193 | to take advantage of the autocompletion already offered by the bash console when, for example, you use the `import` 194 | command from the `ROOT` and the backup directory is inside it; 195 | * fixed a bug for `IndexCommand`, data was not sorted correctly on individual rows. Improved testing; 196 | * slightly improved backup file sorting for `BackupManager::index()` method (this is useful when you have a lot of files); 197 | * requires at least `symfony/process` `7.1.7`, due to this [security vulnerability](https://github.com/mirko-pagliai/cakephp-database-backup/security/dependabot/1); 198 | * fixed some errors in localizations of some strings; 199 | * replaced deprecated `getMockForAbstractClass()` method in tests. 200 | 201 | ### 2.13.2 202 | * no longer needs `php-tools`; 203 | * removed useless `CommandTestCase`; 204 | * little fixes and updates. 205 | 206 | ### 2.13.1 207 | * updated for `php-tools` 1.10.0. 208 | 209 | ### 2.13.0 210 | * requires at least PHP 8.1, PHPUnit 10 and CakePHP 5.0; 211 | * added tests for PHP 8.3. 212 | 213 | ## 2.12 branch 214 | ### 2.12.3 215 | * updated for `php-tools` 1.8. 216 | 217 | ### 2.12.2 218 | * improved and fixed a bug for `ExportCommand` and `ImportCommand`, in handling some exceptions; 219 | * it no longer needs the `me-tools` package. This removes several (useless) dependencies; 220 | * some, possible changes that prepare it for CakePHP 5 and PHPUnit 10 ([issue #97](https://github.com/mirko-pagliai/cakephp-database-backup/issues/97)); 221 | * little fixes. Fixed some deprecations for CakePHP 4.5 ([issue #97](https://github.com/mirko-pagliai/cakephp-database-backup/issues/97)); 222 | * improved `BackuManager::index()` method, also regarding the correct files sorting. This also solves a small bug for 223 | the `rotate()` method (which precisely affects `index()`). The `index()` method now returns a collection of arrays ( 224 | and no longer a collection of `Entity`); 225 | * some testing methods that have been missing for a long time have been added; 226 | * the `BackupTrait::getDriverName()` method can no longer be static; 227 | * removed (old and useless) `BaseCommandTestCase` class; 228 | * added tests for PHP 8.2. 229 | 230 | ### 2.12.1 231 | * fixed a little bug in the `bootstrap.php` file; 232 | * the `Exceptionist` class provided by me-tools is no longer used (in anticipation of an upcoming deprecation). 233 | 234 | ### 2.12.0 235 | * added `AbstractBackupUtility::timeout()` method, so now `BackupExport`/`BackupImport` utilities have a method to set the 236 | timeout for shell commands at runtime. Added `--timeout` option (short: `-t`) for `ExportCommand`/`ImportCommand`; 237 | * the events (`Backup.beforeExport`, `Backup.afterExport`, `Backup.beforeImport`, `Backup.afterImport`, which remain 238 | implemented by the driver classes) are directly dispatched by the `BackupExport::export()` and `BackupImport::import()` 239 | methods, and no longer by the drivers themselves; 240 | * added the `AbstractBackupUtility` abstract class that provides the code common to `BackupExport` and `BackupImport`, 241 | with the new `AbstractBackupUtility::__get()` magic method for reading `BackupExport`/`BackupImport` properties; 242 | * removed `$Driver` public property for `BackupExport`/`BackupImport` and added `AbstractBackupUtility::getDriver()` method; 243 | * the abstract `Driver` class has become `AbstractDriver` and no longer takes a connection as constructor argument, but 244 | directly uses the one set by the configuration. The old `Driver::_exec()` method has been moved and has become 245 | `AbstractBackupUtility::getProcess()`. The old `Driver::export()` and `Driver::import()` methods no longer exist and 246 | their code has been "absorbed" into the `BackupExport::export()` and `BackupImport::import()` methods; 247 | * `BackupTrait::getDriver()` method has become `AbstractBackupUtility::getDriver()`; 248 | * `BackupTrait::getDriverName()` and `AbstractBackupUtility::getDriver()` no longer accept a connection as argument, but 249 | directly use the one set by the configuration; 250 | * the `BackupExport::export()` and `BackupImport::import()` methods can return the filename path on success or `false` 251 | if the `Backup.beforeExport`/`Backup.beforeImport` events are stopped; 252 | * `Driver::_getExecutable()`, `Driver::_getExportExecutable()` and `Driver::_getImportExecutable()` have become 253 | `Driver::getExecutable()`, `Driver::getExportExecutable()` and `Driver::getImportExecutable()`; 254 | * the `Driver::getConfig()` method no longer accepts `null` as argument, but only a string as key, since there is no 255 | need to return the whole configuration; 256 | * `MySql::getAuthFile()` method has become `getAuthFilePath()`, to be more understandable; 257 | * `MySql::deleteAuthFile()` method returns void (there is no need for it to return anything); 258 | * removed useless `TestCase::getMockForAbstractDriver()` method; 259 | * removed useless `BackupExport::$config` property; 260 | * improved the `ExportCommand` class; 261 | * completely improved the `BackupImportTest` tests. 262 | 263 | ## 2.11 branch 264 | ### 2.11.1 265 | * added the `DatabaseBackup.processTimeout` configuration, which allows you to set a timeout for commands that will be 266 | executed in sub-processes (which by default is 60 seconds) and which can be useful for exporting/importing large 267 | databases (see [issue #88](https://github.com/mirko-pagliai/cakephp-database-backup/issues/88)). Any options to change 268 | this timeout from `ImportCommand`/`ExportCommand` will be implemented later; 269 | * guaranteed to work with all versions of CakePHP 4; 270 | * added all property types to all classes; 271 | * upgraded to the new fixture system; 272 | * updated for `php-tools` 1.7.4; 273 | * tests have been made compatible with Xampp on Windows; 274 | * many, small improvements to the code and tests, also suggested by PhpStorm. 275 | 276 | ### 2.11.0 277 | * requires at least PHP 7.4; 278 | * added `MySql::getAuthFile()` method. So the `MySql::$auth` property is now private; 279 | * `createBackup()` and `createSomeBackups()` are now testing global functions and no longer methods provided by the 280 | `TestCase` class; 281 | * added `CommandTestCase` to test commands; 282 | * many, small tweaks to code and descriptions. 283 | 284 | ## 2.10 branch 285 | ### 2.10.2 286 | * added tests for PHP 8.1; 287 | * little fixes for phpstan, psalm and for the composer.json file. 288 | 289 | ### 2.10.1 290 | * stable version; 291 | * updated for `php-tools` 1.5.8. 292 | 293 | ### 2.10.0-beta1 294 | * now allows to configure and customize via bootstrap the executable commands to 295 | import and export databases, for each driver, with placeholders; 296 | * `__exportExecutableWithCompression()` and `_importExecutableWithCompression()` 297 | methods provided by the `Driver` class have been removed and incorporated 298 | into the new `_getExportExecutable()` and `_getImportExecutable()`; 299 | * `BackupTrait::$validExtensions` has been removed and replaced by the 300 | `DATABASE_BACKUP_EXTENSIONS` constant; 301 | * postgres and sqlite commands are also properly escaped; 302 | * many little fixes and many code simplifications. 303 | 304 | ## 2.9 branch 305 | ### 2.9.2 306 | * added `BackupTrait::getDriverName()` static method; `getConnection()` and 307 | `getDriver()` methods are now static; 308 | * backtrack (compared to version `2.9.0`): all tracks are auto-discovered, 309 | otherwise it would not be possible to change the connection you want to work 310 | on on the fly; 311 | * fixed some tests that produced false positives. 312 | 313 | ### 2.9.1 314 | * all shell arguments are now correctly escaped. 315 | 316 | ### 2.9.0 317 | * now uses `symfony/process` to execute import and export shell commands. This 318 | also allows for better handling of errors reported in the shell. The 319 | `DatabaseBackup.redirectStderrToDevNull` config key has been removed; 320 | * only the binaries needed for the database driver used are auto-discovered; 321 | * tests are now only run for one driver at a time, by default `mysql`. You can 322 | choose another driver by setting `driver_test` or ``db_dsn` environment 323 | variables before running `phpunit`; 324 | * migration to github actions. 325 | 326 | ## 2.8 branch 327 | ### 2.8.7 328 | * fixed a small bug when using the numeric hostname (`127.0.0.1` or `::1`) as 329 | the automatic backup filename; 330 | * export and import error messages are now more specific; 331 | * `BackupExport` returns a more accurate error in case of invalid filename; 332 | * added `Driver::_exec()` method; 333 | * tests no longer fail with `DatabaseBackup.redirectStderrToDevNull` set to `false`. 334 | 335 | ### 2.8.6 336 | * fixed bootstrap, `mkdir` errors are no longer suppressed; 337 | * extensive improvement of function descriptions and tags. The level of `phpstan` 338 | has been raised. 339 | 340 | ### 2.8.5 341 | * ready for php `8.0`; 342 | * extensive improvement of function descriptions and tags. 343 | 344 | ### 2.8.4 345 | * `BackupManager::delete()` returns the full path; 346 | * all methods provided by `BackupManager` can now be called statically, except 347 | for the `send()` method; 348 | * extensive improvement of function descriptions and tags; 349 | * ready for `phpunit` 9. 350 | 351 | ### 2.8.3 352 | * updated for `php-tools` 1.4.5; 353 | * added `phpstan`, so fixed some code. 354 | 355 | ### 2.8.2 356 | * updated for `php-tools` 1.4.1. 357 | 358 | ### 2.8.1 359 | * fixed I18n translations; 360 | * fixed [bug for `Command` class](https://github.com/mirko-pagliai/cakephp-database-backup/pull/54). 361 | 362 | ### 2.8.0 363 | * updated for `cakephp` 4 and `phpunit` 8. 364 | 365 | ## 2.7 branch 366 | ### 2.7.1 367 | * fixed [bug for `Command` class](https://github.com/mirko-pagliai/cakephp-database-backup/pull/54). 368 | 369 | ### 2.7.0 370 | * `BackupTrait::getBinary()` method has been moved to `Driver` abstract class; 371 | * `BackupTrait::getTarget()`, `BackupTrait::getDriverName()` and 372 | `BackupTrait::getValidExtensions()` methods have been removed. 373 | 374 | ## 2.6 branch 375 | ### 2.6.6 376 | * tests have been optimized and speeded up; 377 | * APIs are now generated by `phpDocumentor` and no longer by` apigen`. 378 | 379 | ### 2.6.5 380 | * little fixes. 381 | 382 | ### 2.6.4 383 | * little fixes for `BackupManager` and `BackupExport` classes; 384 | * added tests for lower dependencies; 385 | * improved exception message when no binary file is found; 386 | * no longer uses the `Folder` class. 387 | 388 | ### 2.6.3 389 | * little fixes. 390 | 391 | ### 2.6.2 392 | * added `BackupTrait::getDriverName()` method; 393 | * `BackupExport::compression()` takes a compression type name as string or 394 | `null` to disable compression; 395 | * `BackupExport::send()` takes a recipient's email address as string or `null` 396 | to disable sending backup; 397 | * `BackupTrait::getCompression()` returns `null` with no compression; 398 | * the `DriverTestCase` class now implements `testExportOnFailure()` and 399 | `testImportOnFailure()` test methods; 400 | * improved printing of the backup table for the `IndexCommand`; 401 | * updated for `php-tools` 1.2 and `me-tools` 2.18.7. 402 | * added [API](//mirko-pagliai.github.io/cakephp-database-backup). 403 | 404 | ### 2.6.1 405 | * added `DriverTestCase::getMockForDriver()` method; 406 | * `DriverTestCase::allRecords()` method renamed as `getAllRecords()`; 407 | * many small code fixes; 408 | * requires `me-tools` package for dev; 409 | * removed `ConsoleIntegrationTestTrait`, because it is now sufficient to use the 410 | same trait provided by `me-tools`; 411 | * updated for `php-tools` 1.1.12. 412 | 413 | ### 2.6.0 414 | * `BackupShell` has been replaced with console commands. Every method of the 415 | previous class is now a `Command` class; 416 | * `BackupManager::index()` returns a collection of backups; 417 | * `ConsoleIntegrationTestCase` has been replaced by `ConsoleIntegrationTestTrait`. 418 | `TestCaseTrait` has been removed and its methods moved to `TestCase`; 419 | * removed `DATABASE_BACKUP` constant; 420 | * updated for CakePHP 3.7. 421 | 422 | ## 2.5 branch 423 | ### 2.5.1 424 | * updated for CakePHP 3.6 and 3.7. Added `Plugin` class; 425 | * many small code fixes. 426 | 427 | ### 2.5.0 428 | * now it uses the `mirko-pagliai/php-tools` package. This also replaces 429 | `mirko-pagliai/reflection`; 430 | * removed `BackupTrait::getClassShortName()` method. The 431 | `get_class_short_name()` global function will be used instead. 432 | 433 | ## 2.4 branch 434 | ### 2.4.0 435 | * fixed bug trying to email a nonexistent backup; 436 | * `VALID_COMPRESSIONS` and `VALID_EXTENSIONS` constants have been replaced by 437 | `getValidCompressions()` and `getValidExtensions()` methods provided by the 438 | `BackupTrait` class; 439 | * replaced `InternalErrorException` with `InvalidArgumentException` and 440 | `RuntimeException`. This allows compatibility with CakePHP 3.6 branch. 441 | 442 | ## 2.3 branch 443 | ### 2.3.0 444 | * full support for working under Windows; 445 | * added `Driver::getConfig()` method, removed `Driver::$config` property. This 446 | allows you to get the configuration values safely; 447 | * fixed a bug for export and import executables with Postgres databases; 448 | * before importing a sqlite backup, each table is dropped, rather than deleting 449 | the database file; 450 | * added `isWin()` global function; 451 | * tests can have `onlyUnix` or `onlyWindows` group. 452 | 453 | ## 2.2 branch 454 | ### 2.2.0 455 | * added `ConsoleIntegrationTestCase` and `TestCaseTrait` classes. Console tests 456 | have been simplified; 457 | * updated for CakePHP 3.5. 458 | 459 | ## 2.1 branch 460 | ### 2.1.4 461 | * when a backup is sent by mail, the mimetype is forced; 462 | * fixed some tests. 463 | 464 | ### 2.1.3 465 | * added `createBackup()` and `createSomeBackups()` to the `TestCase` class; 466 | * `BackupManager::_send()` has become `getEmailInstance()`. 467 | 468 | ### 2.1.2 469 | * fixed `composer.json`: the plugin requires at least version 3.4 of CakePHP. 470 | 471 | ### 2.1.1 472 | * `afterExport()`, `afterImport()`, `beforeExport()` and `beforeImport` methods 473 | are now real events; 474 | * now you can choose if you want to redirect stderr to `/dev/null`. This 475 | suppresses the output of executed commands. 476 | 477 | ### 2.1.0 478 | * added support for Postgres databases; 479 | * all `export()` and `import()` methods have been moved to the `Driver` class; 480 | * added `afterExport()`, `afterImport()`, `beforeExport()` and `beforeImport` 481 | methods to the `Driver` class; 482 | * `getCompression()` and `getExtension()` moved from `Driver` to `BackupTrait` 483 | class, because these methods are not strictly related to the database engine 484 | you are using; 485 | * removed `getValidExtensions()` and `getValidCompressions()` methods from 486 | `Driver` class, because extensions and compressions are the same for any 487 | database engine; 488 | * removed `getDefaultExtension()` method from `Driver` class, because the 489 | default extension is the same for any database engine. 490 | 491 | ## 2.0 branch 492 | ### 2.0.0 493 | * the plugin has been renamed as `DatabaseBackup` (`cakephp-database-backup`); 494 | * the code has been completely rewritten to work with drivers, so it can also 495 | work with other database engines; 496 | * added support for Sqlite database; 497 | * checks the return status code when it runs `mysql` and `mysqldump` from the 498 | command line. 499 | 500 | # 1.x branch 501 | ## 1.1 branch 502 | ### 1.1.1 503 | * fixed bugs in the table output for the `BackupShell::index()` method; 504 | * `_storeAuth()` methods from `BackupExport` and `BackupImport` classes are 505 | now private; 506 | * added `BackupTrait::getConnection()` method; 507 | * added `BackupTrait::getValidCompressions()` method. 508 | 509 | ### 1.1.0 510 | * added `BackupShell::send()` and `BackupManager::send()` methods to send backup 511 | files via mail; 512 | * added `--send` option to `BackupShell::export()`; 513 | * added `BackupExport::send()` to set up the email recipient, so by calling the 514 | `export()` method the backup file will be sent via email; 515 | * added `BackupTrait`, that provides some methods used by all other classes; 516 | * added `rtr()` (relative to root) global function. This simplifies the output 517 | of some methods provided by `BackupShell`; 518 | * global functions `compressionFromFile()`, `extensionFromCompression()` and 519 | `extensionFromFile()` have been replaced with the `getCompression()` and 520 | `getExtension()` methods provided by the `BackupTrait`; 521 | * all methods belonging to the `BackupManager` class are no longer static. 522 | 523 | ## 1.0 branch 524 | ### 1.0.3 525 | * the target directory is created automatically, if it does not exist; 526 | * `BackupManager::index()` returns an array of entities; 527 | * added `MYSQL_BACKUP` constant. 528 | 529 | ### 1.0.2 530 | * methods that have been deprecated with CakePHP 3.4 have been replaced; 531 | * updated for CakePHP 3.4. 532 | 533 | ### 1.0.1 534 | * added `BackupManager::deleteAll()` and `BackupShell::deleteAll()` methods; 535 | * improved tests. Also, errors in the shell are checked. 536 | 537 | ### 1.0.0 538 | * first release. 539 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) 4 | Copyright (c) 2005-2017, Cake Software Foundation, Inc. (https://cakefoundation.org) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cakephp-database-backup 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 4 | [![CI](https://github.com/mirko-pagliai/cakephp-database-backup/actions/workflows/ci.yml/badge.svg?branch=2.15.x)](https://github.com/mirko-pagliai/cakephp-database-backup/actions/workflows/ci.yml) 5 | [![codecov](https://codecov.io/gh/mirko-pagliai/cakephp-database-backup/graph/badge.svg?token=nkaJk4nvus)](https://codecov.io/gh/mirko-pagliai/cakephp-database-backup) 6 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/cd12284c1047431c8149e09fa56536bf)](https://app.codacy.com/gh/mirko-pagliai/cakephp-database-backup/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/mirko-pagliai/cakephp-database-backup/badge)](https://www.codefactor.io/repository/github/mirko-pagliai/cakephp-database-backup) 8 | 9 | *DatabaseBackup* is a CakePHP plugin to export, import and manage database backups. 10 | Currently, the plugin supports *MySql*, *Postgres* and *Sqlite* databases. 11 | 12 | Did you like this plugin? Its development requires a lot of time for me. 13 | Please consider the possibility of making [a donation](//paypal.me/mirkopagliai): 14 | even a coffee is enough! Thank you. 15 | 16 | [![Make a donation](https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_carte.jpg)](//paypal.me/mirkopagliai) 17 | 18 | ## Requirements 19 | 20 | *DatabaseBackup* requires: 21 | 22 | * `mariadb` and `mariadb-dump` for *MariaDB*/*MySql* databases (previously `mysql` and `mysqldump`); 23 | * `pg_dump` and `pg_restore` for *Postgres* databases; 24 | * `sqlite3` for *Sqlite* databases. 25 | 26 | **Optionally**, if you want to handle compressed backups, `bzip2` and `gzip` are 27 | also required. 28 | 29 | The installation of these binaries may vary depending on your operating system. 30 | 31 | ## Installation 32 | 33 | You can install the plugin via composer: 34 | 35 | ```bash 36 | $ composer require --prefer-dist mirko-pagliai/cakephp-database-backup 37 | ``` 38 | 39 | Then you have to load the plugin. For more information on how to load the plugin, 40 | please refer to the [CakePHP documentation](https://book.cakephp.org/5/en/plugins.html#loading-a-plugin). 41 | 42 | Simply, you can execute the shell command to enable the plugin: 43 | ```bash 44 | $ bin/cake plugin load DatabaseBackup 45 | ``` 46 | This would update your application's bootstrap method. 47 | 48 | By default, the plugin uses the `APP/backups` directory to save the backups 49 | files. So you have to create the directory and make it writable: 50 | 51 | ```bash 52 | $ mkdir backups/ && chmod 775 backups/ 53 | ``` 54 | 55 | If you want to use a different directory, read the [Configuration](docs/Configuration.md) page. 56 | 57 | ### Installation on older CakePHP and PHP versions 58 | 59 | Compared to the current installation requirements, some tags are provided for those using older versions of CakePHP and 60 | PHP (*until February 7, 2025, they were available as branches, now only as tags*): 61 | 62 | - tag [`cakephp4`](https://github.com/mirko-pagliai/cakephp-database-backup/releases/tag/cakephp4), which requires at 63 | least PHP `>=7.4.0` and CakePHP `^4.0`. 64 | This tag no longer receives any updates as of January 5, 2024, and roughly coincides with what `2.12.3` version was. 65 | - tag [`cakephp3`](https://github.com/mirko-pagliai/cakephp-database-backup/releases/tag/cakephp3), which requires at 66 | least PHP `>=5.6 <7.4` and CakePHP `^3.5.1`. 67 | This tag no longer receives any updates as of April 29, 2021, and roughly coincides with what `2.8.5` version was. 68 | 69 | You can freely use these tags, even by downloading the source codes from the attached assets, but their functioning is 70 | no longer guaranteed, especially regarding old dependencies that may no longer be available. 71 | 72 | ## Configuration and How to use 73 | 74 | See [our wiki](/docs). 75 | 76 | Before opening an issue, check this list of [common issues](docs/Common%20issues.md). 77 | 78 | ## Testing 79 | 80 | Normally tests are only run on one driver at a time, by default `mysql`. 81 | 82 | To run tests with another driver, you can use the scripts defined in `composer.json`: 83 | 84 | For example: 85 | ```bash 86 | $ composer test 87 | $ composer test-postgres 88 | $ composer test-sqlite 89 | ``` 90 | 91 | Instead, to run tests with all drivers, you can use the `test-all` script (individual tests are chained together). 92 | 93 | For individual drivers, alternatively you can set the `db_dsn` environment variable, indicating the connection 94 | parameters. In this case, the driver type will still be detected automatically. 95 | 96 | For example: 97 | ```bash 98 | db_dsn=sqlite:///' . TMP . 'example.sq3 vendor/bin/phpunit 99 | ``` 100 | 101 | ## Versioning 102 | 103 | For transparency and insight into our release cycle and to maintain backward 104 | compatibility, *DatabaseBackup* will be maintained under the 105 | [Semantic Versioning guidelines](http://semver.org). 106 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirko-pagliai/cakephp-database-backup", 3 | "description": "Database Backup plugin for CakePHP", 4 | "homepage": "https://github.com/mirko-pagliai/cakephp-database-backup", 5 | "type": "cakephp-plugin", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Mirko Pagliai", 10 | "email": "mirko.pagliai@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.1", 15 | "ext-fileinfo": "*", 16 | "cakephp/cakephp": "^5.0", 17 | "symfony/filesystem": "^6.0|^7.2", 18 | "symfony/finder": "^6.0|^7.1", 19 | "symfony/polyfill-php83": "^1.31", 20 | "symfony/process": "^6.0|^7.1.7" 21 | }, 22 | "require-dev": { 23 | "cakephp/cakephp-codesniffer": "^5.0", 24 | "cakedc/cakephp-phpstan": "^4.0", 25 | "cakephp/migrations": "^4.0", 26 | "phpunit/phpunit": "^10.5.5 || ^11.1.3" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "DatabaseBackup\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "App\\": "tests/test_app/TestApp/", 36 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests", 37 | "DatabaseBackup\\Test\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "check": [ 42 | "@cs-check", 43 | "@update-lowest", 44 | "@test", 45 | "@composer update", 46 | "@test", 47 | "@stan" 48 | ], 49 | "cs-check": "phpcs --colors -p", 50 | "cs-fix": "phpcbf --colors -p", 51 | "phpstan": "phpstan analyse", 52 | "update-lowest": "@composer update --prefer-lowest", 53 | "test-all": [ 54 | "@test", 55 | "@test-postgres", 56 | "@test-sqlite" 57 | ], 58 | "test": "phpunit --colors=always", 59 | "test-postgres": "driver_test=postgres phpunit --colors=always", 60 | "test-sqlite": "driver_test=sqlite phpunit --colors=always", 61 | "coverage": "XDEBUG_MODE=coverage phpunit --coverage-html=coverage" 62 | }, 63 | "config": { 64 | "allow-plugins": { 65 | "dealerdirect/phpcodesniffer-composer-installer": true 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | ['export' => 'mysqldump', 'import' => 'mysql'], 28 | 'postgres' => ['export' => 'pg_dump', 'import' => 'pg_restore'], 29 | 'sqlite' => ['export' => 'sqlite3', 'import' => 'sqlite3'], 30 | ]); 31 | } 32 | 33 | /** 34 | * Backward compatibility for old configuration names, such as `DatabaseBackup.mysql.export`. 35 | */ 36 | foreach (array_keys(DATABASE_BACKUP_EXECUTABLES) as $driverKey) { 37 | foreach (['export', 'import'] as $operationKey) { 38 | $name = 'DatabaseBackup.' . $driverKey . '.' . $operationKey; 39 | if (!Configure::check($name)) { 40 | continue; 41 | } 42 | $expectedName = 'DatabaseBackup.' . ucfirst($driverKey) . '.' . $operationKey; 43 | Configure::write($expectedName, Configure::consume($name)); 44 | 45 | deprecationWarning('2.14.2', sprintf( 46 | 'The configuration name `%s` is deprecated and will be removed in a future release. Please use `%s` instead.', 47 | $name, 48 | $expectedName 49 | )); 50 | } 51 | } 52 | 53 | if (Configure::check('DatabaseBackup.chmod')) { 54 | deprecationWarning('2.15.0', sprintf( 55 | 'The configuration name `%s` is deprecated and will be removed in a future release.', 56 | 'DatabaseBackup.chmod' 57 | )); 58 | } 59 | if (Configure::check('DatabaseBackup.connection')) { 60 | deprecationWarning('2.14.2', sprintf( 61 | 'The configuration name `%s` is deprecated and will be removed in a future release. If you need to use a connection other than `default`, use the `$Connection` argument to the `BackupExport`/`BackupImport` constructor when instantiating these classes.', 62 | 'DatabaseBackup.connection' 63 | )); 64 | } 65 | 66 | //Writes default configuration values 67 | $defaults = [ 68 | 'DatabaseBackup.processTimeout' => 60, 69 | 'DatabaseBackup.target' => rtrim(ROOT, DS) . DS . 'backups', 70 | 'DatabaseBackup.Mysql.export' => '{{BINARY}} --defaults-file={{AUTH_FILE}} {{DB_NAME}}', 71 | 'DatabaseBackup.Mysql.import' => '{{BINARY}} --defaults-extra-file={{AUTH_FILE}} {{DB_NAME}}', 72 | 'DatabaseBackup.Postgres.export' => '{{BINARY}} --format=c -b --dbname=\'postgresql://{{DB_USER}}{{DB_PASSWORD}}@{{DB_HOST}}/{{DB_NAME}}\'', 73 | 'DatabaseBackup.Postgres.import' => '{{BINARY}} --format=c -c -e --dbname=\'postgresql://{{DB_USER}}{{DB_PASSWORD}}@{{DB_HOST}}/{{DB_NAME}}\'', 74 | 'DatabaseBackup.Sqlite.export' => '{{BINARY}} {{DB_NAME}} .dump', 75 | 'DatabaseBackup.Sqlite.import' => '{{BINARY}} {{DB_NAME}}', 76 | ]; 77 | Configure::write(array_filter($defaults, fn (string $key): bool => !Configure::check($key), ARRAY_FILTER_USE_KEY)); 78 | 79 | /** 80 | * It automatically discovers executables not already set by the user in the configuration. 81 | * 82 | * For `mysql` and `mysqldump` executables, it will first look for `mariadb` and `mariadb-dump` executables. 83 | * It then normally searches all other possible executables canonically. 84 | */ 85 | $ExecutableFinder = new ExecutableFinder(); 86 | foreach (['mariadb' => 'mysql', 'mariadb-dump' => 'mysqldump'] as $executable => $alias) { 87 | if (!Configure::check('DatabaseBackup.binaries.' . $alias)) { 88 | Configure::write('DatabaseBackup.binaries.' . $alias, $ExecutableFinder->find($executable)); 89 | } 90 | } 91 | $executables = array_merge(['bzip2', 'gzip'], ...array_values(array_map('array_values', DATABASE_BACKUP_EXECUTABLES))); 92 | foreach ($executables as $executable) { 93 | if (!Configure::check('DatabaseBackup.binaries.' . $executable)) { 94 | Configure::write('DatabaseBackup.binaries.' . $executable, $ExecutableFinder->find($executable)); 95 | } 96 | } 97 | 98 | //Checks for the target directory 99 | $target = Configure::read('DatabaseBackup.target'); 100 | if (!file_exists($target)) { 101 | mkdir($target, 0777, true); 102 | } 103 | if (!is_dir($target) || !is_writeable($target)) { 104 | trigger_error(sprintf('The directory `%s` is not writable or is not a directory', $target), E_USER_ERROR); 105 | } 106 | 107 | /** 108 | * Aliases for old `Driver` classes. 109 | * 110 | * @todo to be removed in version `2.15` or `2.16` 111 | */ 112 | foreach ( 113 | [ 114 | 'DatabaseBackup\Executor\AbstractExecutor' => 'DatabaseBackup\Driver\AbstractDriver', 115 | 'DatabaseBackup\Executor\MysqlExecutor' => 'DatabaseBackup\Driver\Mysql', 116 | 'DatabaseBackup\Executor\PostgresExecutor' => 'DatabaseBackup\Driver\Postgres', 117 | 'DatabaseBackup\Executor\SqliteExecutor' => 'DatabaseBackup\Driver\Sqlite', 118 | ] as $class => $alias 119 | ) { 120 | if (!class_exists($alias)) { 121 | class_alias(class: $class, alias: $alias); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /docs/Common issues.md: -------------------------------------------------------------------------------- 1 | # Common issues 2 | 3 | Before opening an issue, check this list of common issues. 4 | 5 | Also, make sure [you have configured the plugin correctly](Configuration.md) 6 | 7 | ## Transition from `mysql` and `mysqldump` to `mariadb` and `mariadb-dump` 8 | 9 | As reported in [issue #111](https://github.com/mirko-pagliai/cakephp-database-backup/issues/111) and already reported in [issue #110](https://github.com/mirko-pagliai/cakephp-database-backup/issues/110), the `mysql` and `mysql-dump` binaries have been deprecated and will be replaced with `mariadb` and `mariadb-dump`. 10 | 11 | The deprecation and subsequent replacement may vary from operating system to operating system, but as of today (_February 8, 2025_) the old binaries are generally still present, which simply link to the new ones: 12 | 13 | ```bash 14 | $ whereis mysqldump 15 | mysqldump: /usr/bin/mysqldump /usr/share/man/man1/mysqldump.1.gz 16 | $ ls -l /usr/bin/mysqldump 17 | lrwxrwxrwx 1 root root 12 30 nov 2023 /usr/bin/mysqldump -> mariadb-dump 18 | $ whereis mariadb-dump 19 | mariadb-dump: /usr/bin/mariadb-dump /usr/share/man/man1/mariadb-dump.1.gz 20 | $ whereis mysql 21 | mysql: /usr/bin/mysql /usr/lib/mysql /etc/mysql /usr/share/mysql /usr/share/man/man1/mysql.1.gz 22 | $ whereis mysql^C 23 | $ ls -l /usr/bin/mysql 24 | lrwxrwxrwx 1 root root 7 30 nov 2023 /usr/bin/mysql -> mariadb 25 | ``` 26 | 27 | This, still today, makes it possible to use any pair of these binaries, without any particular problems. At most, some systems may issue a warning about the deprecation: 28 | ```bash 29 | `/usr/bin/mysqldump: Deprecated program name. It will be removed in a future release, use '/usr/bin/mariadb-dump' instead 30 | ``` 31 | 32 | This warns the user that the binaries have already been deprecated and will be replaced in the future, but export and import still work without problems. 33 | 34 | Starting from version `2.13.4`, by default `cakephp-database-backup` will first look for the presence of `mariadb` and `mariadb-dump`, then for `mysql` and `mysqldump` as backwards compatibility. 35 | 36 | Before this version, except that deprecation should not cause problems, it is possible to solve by [manually setting the executables](Configuration.md#binaries), making sure that they point to `mariadb` and `mariadb-dump`: 37 | 38 | ```php 39 | Configure::write('DatabaseBackup.binaries.mysql', '/usr/bin/mariadb'); 40 | Configure::write('DatabaseBackup.binaries.mysqldump', '/usr/bin/mariadb-dump'); 41 | ``` 42 | 43 | ## Errors about certificate verification 44 | 45 | When using MariaDB, you may receive an error (related to certificate verification) like this (see [issue #112](https://github.com/mirko-pagliai/cakephp-database-backup/issues/112) and [issue #110](https://github.com/mirko-pagliai/cakephp-database-backup/issues/110)): 46 | 47 | ```bash 48 | mysqldump: Got error: 2026: "TLS/SSL error: Certificate verification failure: The certificate is NOT trusted." when trying to connect` 49 | ``` 50 | 51 | The problem may be caused by upgrading to MariaDB 11.4 (refer to [this](https://mariadb.com/kb/en/securing-connections-for-client-and-server/#)): 52 | > Starting from 11.4 MariaDB encrypts the transmitted data between the server and clients by default unless the server and client run on the same host. 53 | 54 | So, the error occurs because MariaDB by default wants to use the TLS protocol to transmit data in encrypted form, but at the same time your certificate is not valid. 55 | **The advice is to check and fix the configuration of your system.** 56 | 57 | In any case, it is possible to make the plugin ignore this function when exporting databases, as explained [here](Configuration.md#customize-exportimport-commands) and making the export command use the `--skip-ssl` option. So, before the plugin is loaded: 58 | 59 | ```php 60 | Configure::write('DatabaseBackup.mysql.export', '{{BINARY}} --defaults-file={{AUTH_FILE}} --skip-ssl {{DB_NAME}}'); 61 | ``` 62 | 63 | However, it is important to note that **this can expose your system to malicious attacks if used incorrectly**. 64 | 65 | ## `LOCK TABLES` permission 66 | 67 | When exporting, an error similar to this occurs: 68 | 69 | ```bash 70 | mysqldump: Got error: 1044: "Access denied for user 'myuser'@'localhost' to database 'mydb'" when using LOCK TABLES 71 | ``` 72 | The user accessing the database must have the correct permissions, depending on the driver you're using. 73 | 74 | In this specific case, for `mysql` the user must have the `LOCK TABLES` permission. 75 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The plugin uses some configuration parameters, and you can set them using the `\Cake\Core\Configure` class, **before** loading the plugin. 4 | 5 | For example, you can do this at the bottom of the file `APP/config/app.php` of your application. 6 | 7 | If you want to send backup files by email, remember to set up your application correctly so that it can send emails. 8 | For more information on how to configure your application, see the [CakePHP documentation](https://book.cakephp.org/5/en/core-libraries/email.html#configuring-transports). 9 | 10 | - [Configuration values](#configuration-values) 11 | * [`DatabaseBackup.chmod`](#-databasebackupchmod-) 12 | * [`DatabaseBackup.connection`](#-databasebackupconnection-) 13 | * [~~`DatabaseBackup.mailSender`~~](#---databasebackupmailsender---) 14 | * [`DatabaseBackup.processTimeout`](#-databasebackupprocesstimeout-) 15 | * [`DatabaseBackup.target`](#-databasebackuptarget-) 16 | - [Binaries](#binaries) 17 | - [Customize export and import commands](#customize-export-and-import-commands) 18 | 19 | *** 20 | 21 | ## Configuration values 22 | 23 | ### `DatabaseBackup.chmod` 24 | ```php 25 | Configure::write('DatabaseBackup.chmod', 0664); 26 | ``` 27 | Setting `DatabaseBackup.chmod`, you can choose the permissions that will be applied to new backup files. 28 | Note that you must set an octal value. For more information, please refer to the [PHP manual](http://php.net/manual/en/function.chmod.php). 29 | Note that this works only on Unix systems. 30 | 31 | ### `DatabaseBackup.connection` 32 | ```php 33 | Configure::write('DatabaseBackup.connection', 'default'); 34 | ``` 35 | Setting `DatabaseBackup.connection`, you can choose which database connection you want to use. 36 | For more information about database connections, please refer to the 37 | [CakePHP documentation](https://book.cakephp.org/5/en/orm/database-basics.html#configuration). 38 | 39 | ### ~~`DatabaseBackup.mailSender`~~ 40 | _(deprecated since 2.13.4, removed in 2.14.0)_ 41 | ```php 42 | Configure::write('DatabaseBackup.mailSender', 'sender@example.it'); 43 | ``` 44 | ~~Setting `DatabaseBackup.mailSender`, you can choose the email address that will be the sender when you send the backup files via email.~~ 45 | 46 | ### `DatabaseBackup.processTimeout` 47 | _(from the 2.12.0 version)_ 48 | ```php 49 | Configure::write('DatabaseBackup.processTimeout', 60); 50 | ``` 51 | Setting `DatabaseBackup.processTimeout`, you can choose the general timeout time (in seconds) for commands to be executed in the shell. 52 | 53 | ### `DatabaseBackup.target` 54 | ```php 55 | Configure::write('DatabaseBackup.target', ROOT . DS . 'backups'); 56 | ``` 57 | Setting `DatabaseBackup.target`, you can use another directory where the plugin will save backup files. 58 | 59 | ## Binaries 60 | The plugin uses several binary files: 61 | * `bzip2` 62 | * `gzip` 63 | * `mariadb` (previously `mysql`) 64 | * `mariadb-dump` (previously `mysqldump`) 65 | * `pg_dump` 66 | * `pg_restore` 67 | * `sqlite3` 68 | 69 | By default, all binaries will be detected automatically. 70 | If a binary is not found or if you want to set a different path for the binary, you can use these configuration values. 71 | 72 | Just an example for UNIX: 73 | ```php 74 | Configure::write('DatabaseBackup.binaries.bzip2', '/full/path/to/bzip2'); 75 | ``` 76 | And an example for Windows: 77 | 78 | ```php 79 | Configure::write('DatabaseBackup.binaries.mysql', 'C:\\xampp\\mysql\\bin\\mysql.exe'); 80 | Configure::write('DatabaseBackup.binaries.mysqldump', 'C:\\xampp\\mysql\\bin\\mysqldump.exe'); 81 | ``` 82 | 83 | The same applies to all other binaries, just change the binary name shown in the example. 84 | 85 | ## Customize export and import commands 86 | _(from the 2.10.0 version)_ 87 | 88 | By default, commands (to export/import backups) are executed with generic options that are valid for almost all environments. 89 | However, in some particular environments or conditions it may be necessary to execute commands with particular options. 90 | 91 | The default commands are defined in the `config/bootstrap.php` file and placeholders (such as `{{BINARY}}` or `{{DB_HOST}}`) are replaced and escaped before the command is executed. 92 | 93 | It is therefore possible to use custom commands by acting on the configuration, before loading the plugin. 94 | 95 | An example: suppose you want to run the command to export mysql databases with the `--column-statistics=0` option. Then in the bootstrap of your application: 96 | ```php 97 | Configure::write('DatabaseBackup.Mysql.export', '{{BINARY}} --defaults-file={{AUTH_FILE}} {{DB_NAME}} --column-statistics=0'); 98 | ``` 99 | 100 | However, remember that some values should be escaped and that incorrect customization of the commands could make the plugin unusable or otherwise cause unwanted effects. 101 | -------------------------------------------------------------------------------- /docs/Create backup copies.md: -------------------------------------------------------------------------------- 1 | # Create backup copies 2 | 3 | - [Introduction](#introduction) 4 | - [Create a command to export (`ExportBackupCommand`)](#create-a-command-to-export---exportbackupcommand--) 5 | - [Create a command to rotate backups (`RotateBackupCommand`)](#create-a-command-to-rotate-backups---rotatebackupcommand--) 6 | 7 | ## Introduction 8 | 9 | As we should know, it is never enough to just create a local backup (as this plugin does). 10 | Instead, **it is necessary to also create a copy of the backup, to be stored elsewhere**. 11 | 12 | Since the PHP functions for the filesystem also work perfectly with the ftp protocol, this guide is also valid for creating the copy on another directory (for example an external disk) or better on another machine or an external server (like a ftp server). 13 | 14 | We will integrate two commands into our app: 15 | 1) `ExportBackupCommand`, which will be responsible for exporting the backup and making a copy of it on a remote ftp server; 16 | 2) `RotateBackupCommand`, which will be responsible for rotating both the backups on the same machine and the copies stored remotely. 17 | 18 | In both cases, therefore, we will not use commands directly provided by the plugin, but as mentioned we will implement our own, more effective and customizable, according to our needs. 19 | The second command is essential to maintain only a limited number of backups and copies. 20 | 21 | In my specific case, I perform 5 backups per day of the same database. 22 | So I can safely make sure that the second command, which as mentioned will rotate backups and their copies, is instead executed once a day (it is sufficient). Obviously nothing prevents me from doing everything with a single command, but this setup (two separate commands) is better suited to my case, in addition to allowing me to keep the code separate and tidy (and to write more efficient tests). 23 | 24 | The example will use the [Filesystem](https://symfony.com/doc/current/components/filesystem.html) and [Finder](https://symfony.com/doc/current/components/finder.html) components provided by Symfony. 25 | 26 | It will also use the `DatabaseBackup.copyDirTarget` configuration, which must be set elsewhere, for example: 27 | ```php 28 | Configure::write('DatabaseBackup.copyDirTarget', 'ftp://username:password@example.com:21/my/target/copy/dir'); 29 | ``` 30 | 31 | It can also be the path of a simple directory (for example an external disk), but if it contains the authentication data to an FTP server as plain text (as in my case), **it is best to set this configuration in a safe place**. 32 | 33 | ## Create a command to export (`ExportBackupCommand`) 34 | 35 | Let's see the `src/Command/ExportBackupCommand.php` file: 36 | 37 | ```php 38 | compression(Compression::Gzip) 70 | ->export(); 71 | } catch (Exception $e) { 72 | $io->abort('`BackupExport` failed with message: "' . $e->getMessage() . '"'); 73 | } 74 | 75 | $io->verbose('Created `' . basename($backup) . '` backup file'); 76 | 77 | /** @var string $copyDirTarget */ 78 | $copyDirTarget = Configure::readOrFail('DatabaseBackup.copyDirTarget'); 79 | $targetCopy = rtrim($copyDirTarget, DS) . DS . basename($backup); 80 | 81 | $Filesystem = new Filesystem(); 82 | try { 83 | $Filesystem->copy($backup, $targetCopy); 84 | } catch (Exception $e) { 85 | $io->abort('File copy failed with message: "' . $e->getMessage() . '"'); 86 | } 87 | 88 | $io->verbose('File `' . basename($backup) . '` copied to `' . $targetCopy . '`'); 89 | } 90 | } 91 | ``` 92 | 93 | Now let's understand what this command does. 94 | 95 | First of all it exports a database backup using the `BackupExport` utility ([as explained here](How%20to%20use%20the%20BackupExport%20utility.md)), setting only the compression I want. 96 | This will export the backup to the `backups/` directory of my own app by default. 97 | Thanks to the `try`/`catch` block, I can catch any exceptions during the backup export, properly stop the command and report the eventual error in the console. 98 | 99 | Let's try running this command: 100 | ```bash 101 | $ bin/cake export_backup -v 102 | Created `backup_myapp_20250306165116.sql.gz` backup file 103 | File `backup_myapp_20250306165116.sql.gz` copied to `ftp://username:password@example.com:21/my/target/copy/dir` 104 | ``` 105 | 106 | It works as desired and with the `-v` (`--verbose`) option I can get some useful output. 107 | 108 | As mentioned, in my specific case this command will be executed 5 times a day, thus creating 5 backups and 5 copies. 109 | 110 | ## Create a command to rotate backups (`RotateBackupCommand`) 111 | 112 | Let's see the `src/Command/RotateBackupCommand.php` file: 113 | 114 | ```php 115 | 116 | declare(strict_types=1); 117 | 118 | namespace App\Command; 119 | 120 | use Cake\Command\Command; 121 | use Cake\Console\Arguments; 122 | use Cake\Console\ConsoleIo; 123 | use Cake\Core\Configure; 124 | use Cake\I18n\Date; 125 | use Exception; 126 | use Symfony\Component\Filesystem\Filesystem; 127 | use Symfony\Component\Finder\Finder; 128 | 129 | /** 130 | * RotateBackupCommand. 131 | * 132 | * Rotates database backups and copies of them. 133 | */ 134 | class RotateBackupCommand extends Command 135 | { 136 | /** 137 | * @inheritDoc 138 | */ 139 | public function execute(Arguments $args, ConsoleIo $io): void 140 | { 141 | /** 142 | * Sets a limit to 1 months ago 143 | */ 144 | $limit = Date::today()->subMonths(1)->toDateString(); 145 | 146 | /** @var string $target */ 147 | $target = Configure::readOrFail('DatabaseBackup.target'); 148 | 149 | /** @var string $copyDirTarget */ 150 | $copyDirTarget = Configure::readOrFail('DatabaseBackup.copyDirTarget'); 151 | 152 | /** 153 | * It searches for all files older than `$limit` 154 | */ 155 | $Finder = new Finder(); 156 | $Finder 157 | ->files() 158 | // Only `sql`/`sql.gz`/`sql.bz2` files 159 | ->name('/\.sql(\.(gz|bz2))?$/') 160 | ->in([$target, $copyDirTarget]) 161 | /** @see https://symfony.com/doc/current/components/finder.html#file-date */ 162 | ->date('< ' . $limit); 163 | 164 | $Filesystem = new Filesystem(); 165 | 166 | foreach ($Finder as $File) { 167 | try { 168 | $Filesystem->remove($File->getPathname()); 169 | } catch (Exception $e) { 170 | $io->abort('File deletion failed with message: "' . $e->getMessage() . '"'); 171 | } 172 | 173 | $io->verbose('Removed `' . $File->getPathname() . '` backup file'); 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | Again, let's understand what this command does. 180 | 181 | With the `$limit` variable, we set the limit beyond which to delete backups and copies. In this example, the limit is set to one month ago, but it can be changed as needed. It is important that at the end the `$limit` is a value supported by `strtotime()` (for this reason we start with an instance of `Date` and conclude with the `toDateString()` method). 182 | 183 | Then the two directories we are interested in are set, `$target` and `$copyDirTarget`. 184 | 185 | So thanks to the [Finder component](https://symfony.com/doc/current/components/finder.html) we recover all the files contained in those directories and which have a date prior to `$limit`. 186 | Here it is sufficient to get all the files contained in the two directories and then filter them by date as explained, but for more complex cases it is possible to use many other methods that allow you to filter by name, path, size, depth, etc. (refer to the documentation). 187 | 188 | Then the resulting files are iterated and deleted once, always through a `try`/`catch` block that allows exceptions to be handled. 189 | 190 | Let's try running this command: 191 | 192 | ```bash 193 | $ bin/cake rotate_backup -v 194 | Removed `/var/www/html/myapp/backups/backup_myapp_20250205130001.sql.gz` backup file 195 | Removed `/var/www/html/myapp/backups/backup_myapp_20250205160001.sql.gz` backup file 196 | Removed `/var/www/html/myapp/backups/backup_myapp_20250205080001.sql.gz` backup file 197 | Removed `/var/www/html/myapp/backups/backup_myapp_20250205220001.sql.gz` backup file 198 | Removed `/var/www/html/myapp/backups/backup_myapp_20250205100001.sql.gz` backup file 199 | Removed `ftp://username:password@example.com:21/my/target/copy/dir/backup_myapp_20250205080001.sql.gz` backup file 200 | Removed `ftp://username:password@example.com:21/my/target/copy/dir/backup_myapp_20250205100001.sql.gz` backup file 201 | Removed `ftp://username:password@example.com:21/my/target/copy/dir/backup_myapp_20250205130001.sql.gz` backup file 202 | Removed `ftp://username:password@example.com:21/my/target/copy/dir/backup_myapp_20250205160001.sql.gz` backup file 203 | Removed `ftp://username:password@example.com:21/my/target/copy/dir/backup_myapp_20250205220001.sql.gz` backup file 204 | ``` 205 | 206 | This also produces the desired result, deleting all files from the thirty-first day (which here were the only ones to exceed the imposed limit). 207 | -------------------------------------------------------------------------------- /docs/Export backups as cron jobs.md: -------------------------------------------------------------------------------- 1 | # Export backups as cron jobs 2 | 3 | You can schedule backups by running the plugin shell as cron job. 4 | 5 | Please refer to [How to use commands](How%20to%20use%20commands.md) and [Running Shells as Cron Jobs](https://book.cakephp.org/5/en/console-commands/cron-jobs.html#running-shells-as-cron-jobs) 6 | 7 | Example. 8 | 9 | ```bash 10 | 0 3 * * 1-5 cd /var/www/mysite && bin/cake database_backup.export -c gzip --timeout 120 # Backup for mysite 11 | ``` 12 | 13 | * the backup runs every day from Monday to Friday, at 3 am; 14 | * the backup will be compressed with gzip; 15 | * the timeout for shell commands is 120 seconds. 16 | -------------------------------------------------------------------------------- /docs/How to use commands.md: -------------------------------------------------------------------------------- 1 | # How to use commands 2 | 3 | This plugin provides several commands for database management: 4 | 5 | ``` 6 | Available Commands: 7 | 8 | - database_backup.export 9 | - database_backup.import 10 | ``` 11 | 12 | *** 13 | 14 | # Commands 15 | ## ~~`delete_all`~~ 16 | _(deprecated since 2.13.4, removed in 2.14.0)_ 17 | 18 | Deletes all database backup files. 19 | 20 | **Usage:** 21 | ``` 22 | cake database_backup.delete_all [-h] [-q] [-v] 23 | ``` 24 | 25 | ## ~~`index`~~ 26 | _(deprecated since 2.14.4, removed in 2.15.0)_ 27 | 28 | Lists database backups. 29 | 30 | **Usage:** 31 | ``` 32 | cake database_backup.index [-h] [-q] [--reverse] [-v] 33 | ``` 34 | 35 | **Example:** 36 | ``` 37 | $ bin/cake database_backup.index 38 | 39 | Backup files found: 2 40 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 41 | | Filename | Extension | Compression | Size | Datetime | 42 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 43 | | backup_mydb_20161113110419.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 11:04 | 44 | | backup_mydb_20161113110414.sql | sql | none | 150,93 KB | 13/11/16, 11:04 | 45 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 46 | ``` 47 | 48 | *** 49 | 50 | ## `export` 51 | 52 | Exports a database backup. 53 | 54 | **Usage:** 55 | ``` 56 | cake database_backup.export [options] 57 | ``` 58 | 59 | **Options:** 60 | ``` 61 | --compression, -c Compression type. By default, no compression will be 62 | used (choices: bzip2|gzip) 63 | --filename, -f Filename. It can be an absolute path and may contain 64 | patterns. The compression type will be automatically 65 | set 66 | --help, -h Display this help. 67 | --quiet, -q Enable quiet output. 68 | --timeout, -t Timeout for shell commands. Default value: 60 seconds 69 | --verbose, -v Enable verbose output. 70 | ``` 71 | 72 | **Example:** 73 | ``` 74 | $ bin/cake database_backup.export -c gzip 75 | 76 | Backup `backups/backup_mydb_20161113165059.sql.gz` has been exported 77 | ``` 78 | 79 | *** 80 | 81 | ## `import` 82 | 83 | Imports a database backup. 84 | 85 | **Usage:** 86 | ``` 87 | cake database_backup.import [-h] [-q] [-t] [-v] 88 | ``` 89 | 90 | **Arguments:** 91 | ``` 92 | filename Filename. It can be an absolute path 93 | ``` 94 | 95 | **Options:** 96 | ``` 97 | --help, -h Display this help. 98 | --quiet, -q Enable quiet output. 99 | --timeout, -t Timeout for shell commands. Default value: 60 seconds 100 | --verbose, -v Enable verbose output. 101 | ``` 102 | 103 | **Example:** 104 | ``` 105 | $ bin/cake database_backup.index 106 | 107 | Backup files found: 3 108 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 109 | | Filename | Extension | Compression | Size | Datetime | 110 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 111 | | backup_mydb_20161113165059.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 16:50 | 112 | | backup_mydb_20161113110419.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 11:04 | 113 | | backup_mydb_20161113110414.sql | sql | none | 150,93 KB | 13/11/16, 11:04 | 114 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 115 | 116 | $ bin/cake database_backup.import backup_mydb_20161113165059.sql.gz 117 | Backup `backups/backup_mydb_20161113165059.sql.gz` has been imported 118 | ``` 119 | 120 | *** 121 | 122 | ## ~~`rotate`~~ 123 | 124 | _(deprecated since 2.13.5, removed in 2.14.0)_ 125 | 126 | Rotates backups. 127 | 128 | **Usage:** 129 | ``` 130 | cake database_backup.rotate [-h] [-q] [-v] 131 | ``` 132 | 133 | **Arguments:** 134 | ``` 135 | keep Number of backups you want to keep. So, it will delete all backups 136 | that are older 137 | ``` 138 | 139 | **Example:** 140 | ``` 141 | $ bin/cake database_backup.index 142 | 143 | Backup files found: 3 144 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 145 | | Filename | Extension | Compression | Size | Datetime | 146 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 147 | | backup_mydb_20161113165059.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 16:50 | 148 | | backup_mydb_20161113110419.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 11:04 | 149 | | backup_mydb_20161113110414.sql | sql | none | 150,93 KB | 13/11/16, 11:04 | 150 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 151 | 152 | $ bin/cake database_backup.rotate 1 153 | Deleted backup files: 2 154 | ``` 155 | 156 | *** 157 | 158 | ## ~~`send`~~ 159 | _(deprecated since 2.13.4, removed in 2.14.0)_ 160 | 161 | Sends a backup file via email. 162 | 163 | **Usage:** 164 | ``` 165 | cake database_backup.send [-h] [-q] [-v] 166 | ``` 167 | 168 | **Arguments:** 169 | ``` 170 | filename Filename. It can be an absolute path 171 | recipient Recipient's email address 172 | ``` 173 | 174 | **Example:** 175 | ``` 176 | $ bin/cake database_backup.index 177 | 178 | Backup files found: 3 179 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 180 | | Filename | Extension | Compression | Size | Datetime | 181 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 182 | | backup_mydb_20161113165059.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 16:50 | 183 | | backup_mydb_20161113110419.sql.gz | sql.gz | gzip | 51,05 KB | 13/11/16, 11:04 | 184 | | backup_mydb_20161113110414.sql | sql | none | 150,93 KB | 13/11/16, 11:04 | 185 | +-----------------------------------+-----------+-------------+-----------+-----------------+ 186 | 187 | $ bin/cake database_backup.send backup_mydb_20161113110414.sql mymail@example.com 188 | Backup `backup_mydb_20161113110414.sql` was sent via mail 189 | ``` 190 | -------------------------------------------------------------------------------- /docs/How to use the BackupExport utility.md: -------------------------------------------------------------------------------- 1 | # How to use the BackupExport utility 2 | 3 | - [Basic operation](#basic-operation) 4 | - [Optional methods](#optional-methods) 5 | * [`compression()`](#-compression---) 6 | * [`filename()`](#-filename---) 7 | * [~~`rotate()`~~](#-rotate---) 8 | * [~~`send()`~~](#---send-----) 9 | * [`timeout()`](#-timeout---) 10 | - [Export the database](#export-the-database) 11 | * [`export()`](#-export---) 12 | 13 | *** 14 | 15 | # Basic operation 16 | To export a database, you have to follow three steps: 17 | 18 | **1) Instantiate the class** 19 | Example: 20 | 21 | ```php 22 | $backup = new BackupExport(); 23 | ``` 24 | **2) Call one (or more) optional methods** 25 | Example: 26 | ```php 27 | $backup = new BackupExport(); 28 | $backup->compression('gzip'); 29 | $backup->filename('my_custom_name'); 30 | ``` 31 | Note that all these methods are **optional**. 32 | See the [methods list](#optional-methods) below. 33 | 34 | **3) Export the database, calling the `export()` method** 35 | Example: 36 | ```php 37 | $backup = new BackupExport(); 38 | $backup->compression('gzip'); 39 | $backup->filename('my_custom_name'); 40 | $filename = $backup->export(); 41 | ``` 42 | Note that the `export()` method returns the path of the backup. 43 | See [how to use the `export()` method](#export) below. 44 | 45 | # Optional methods 46 | You can call one (or more) optional methods. 47 | 48 | ## `compression()` 49 | ```php 50 | compression(?string $compression) 51 | ``` 52 | Sets the compression. 53 | 54 | **Parameters for `compression()`** 55 | *null|string $compression* 56 | Compression. Supported values are `bzip2`, `gzip` and `null` (if you don't want to use compression). 57 | 58 | ## `filename()` 59 | ```php 60 | filename(string $filename) 61 | ``` 62 | Sets the filename. 63 | Note that using this method, the compression type will be automatically setted by the filename. 64 | 65 | **Parameters for `filename()`** 66 | *string $filename* 67 | Filename. It can be an absolute path and may contain patterns. 68 | 69 | Patterns are `{$DATABASE}` (database name), `{$DATETIME}` (datetime), `{$HOSTNAME}` (hostname) and `{$TIMESTAMP}` (timestamp). 70 | 71 | ## ~~`rotate()`~~ 72 | _(deprecated since 2.15.0)_ 73 | ```php 74 | rotate(int $rotate) 75 | ``` 76 | Sets the number of backups you want to keep. So, it will delete all backups that are older. 77 | See also [BackupManager::rotate()](How%20to%20use%20the%20BackupManager%20utility.md#rotate). 78 | 79 | **Parameters for `rotate()`** 80 | *int $rotate* 81 | Number of backups you want to keep. 82 | 83 | ## ~~`send()`~~ 84 | _(deprecated since 2.13.4, removed in 2.14.0)_ 85 | ```php 86 | send(?string $recipient = null) 87 | ``` 88 | ~~Sets the recipient's email address to send the backup file via mail.~~ 89 | 90 | **Parameters for `send()`** 91 | *null|string $recipient* 92 | Recipient's email address or `null` to disable. 93 | 94 | ## `timeout()` 95 | ```php 96 | timeout(int $timeout) 97 | ``` 98 | Sets the timeout (in seconds) for commands to be executed in the shell. 99 | This is useful when working with very large databases that take a long time to export and import. 100 | 101 | **Parameters for `timeout()`** 102 | *int $timeout* 103 | Timeout in seconds 104 | 105 | # Export the database 106 | ## `export()` 107 | ```php 108 | export() 109 | ``` 110 | Exports the database and returns its filename path on success or `false` if the `Backup.beforeExport` event is stopped. 111 | -------------------------------------------------------------------------------- /docs/How to use the BackupImport utility.md: -------------------------------------------------------------------------------- 1 | # How to use the BackupImport utility 2 | 3 | * [Basic operation](#basic-operation) 4 | * [Methods](#methods) 5 | * [filename()](#filename) 6 | * [timeout()](#timeout) 7 | * [Import the database](#import-the-database) 8 | * [import()](#import) 9 | 10 | *** 11 | 12 | # Basic operation 13 | To import a database, you have to follow three steps: 14 | 15 | **1) Instantiate the class** 16 | Example: 17 | ```php 18 | $backup = new BackupImport(); 19 | ``` 20 | **2) Set the filename of the backup you want to import** 21 | Example: 22 | ```php 23 | $backup = new BackupImport(); 24 | $backup->filename('my_backup.sql'); 25 | ``` 26 | Note that **you must call** the `filename()` method and set the backup filename. 27 | See [how to use the `filename()` method](#filename) below. 28 | 29 | **3) Import the database, calling the `import()` method** 30 | Example: 31 | ```php 32 | $backup = new BackupImport(); 33 | $backup->filename('my_backup.sql'); 34 | $filename = $backup->import(); 35 | ``` 36 | Note that the `import()` method returns the path of the backup that you have imported. 37 | See [how to use the `import()` method](#import) below. 38 | 39 | # Methods 40 | ## `filename()` 41 | ```php 42 | filename(string $filename) 43 | ``` 44 | It sets the filename of the backup you want to import. 45 | 46 | **Parameters for `filename()`** 47 | *string $filename* 48 | Filename. It can be an absolute path. 49 | 50 | ## `timeout()` 51 | ```php 52 | timeout(int $timeout) 53 | ``` 54 | Sets the timeout (in seconds) for commands to be executed in the shell. 55 | This is useful when working with very large databases that take a long time to export and import. 56 | 57 | **Parameters for `timeout()`** 58 | *int $timeout* 59 | Timeout in seconds 60 | 61 | # Import the database 62 | ## `import()` 63 | ```php 64 | import() 65 | ``` 66 | Imports the database and returns its filename path on success or `false` if the `Backup.beforeImport` event is stopped. 67 | -------------------------------------------------------------------------------- /docs/How to use the BackupManager utility.md: -------------------------------------------------------------------------------- 1 | # How to use the BackupManager utility 2 | 3 | ## Methods 4 | 5 | ### ~~`delete()`~~ 6 | _(deprecated since 2.13.5, removed in 2.14.0)_ 7 | ```php 8 | delete(string $filename) 9 | ``` 10 | ~~It deletes a backup file.~~ 11 | 12 | **Parameters for `delete()`** 13 | 14 | *string $filename* 15 | Filename of the backup that you want to delete. The path can be relative to the backup directory. 16 | 17 | *** 18 | 19 | ### ~~`deleteAll()`~~ 20 | _(deprecated since 2.13.5, removed in 2.14.0)_ 21 | ```php 22 | deleteAll() 23 | ``` 24 | ~~It deletes all backup files.~~ 25 | 26 | *** 27 | 28 | ### ~~`index()`~~ 29 | _(deprecated since 2.15.0)_ 30 | ```php 31 | index() 32 | ``` 33 | It returns a list of database backups. 34 | Note that it lists only the backups in the backup directory. 35 | 36 | Backups are returned as a `Collection` of arrays. 37 | Example (calling `toArray()` method): 38 | 39 | ```php 40 | [ 41 | (int) 0 => [ 42 | 'filename' => 'backup_test_1698229269.sql.bz2', 43 | 'extension' => 'sql.bz2', 44 | 'compression' => 'bzip2', 45 | 'size' => (int) 3175, 46 | 'datetime' => object(Cake\I18n\FrozenTime) id:0 { 47 | 'time' => '2023-10-25 10:21:09.000000+00:00' 48 | 'timezone' => 'UTC' 49 | 'fixedNowTime' => false 50 | } 51 | ], 52 | (int) 1 => [ 53 | 'filename' => 'backup_test_1698229268.sql.gz', 54 | 'extension' => 'sql.gz', 55 | 'compression' => 'gzip', 56 | 'size' => (int) 3725, 57 | 'datetime' => object(Cake\I18n\FrozenTime) id:1 { 58 | 'time' => '2023-10-25 10:21:08.000000+00:00' 59 | 'timezone' => 'UTC' 60 | 'fixedNowTime' => false 61 | } 62 | ] 63 | ] 64 | ``` 65 | Note that up until version `2.12.1`, it returned a collection of `Entities` (and not arrays) 66 | 67 | *** 68 | 69 | ### ~~`rotate()`~~ 70 | _(deprecated since 2.15.0)_ 71 | ```php 72 | rotate(int $rotate) 73 | ``` 74 | It rotates backups. 75 | You must indicate the number of backups you want to keep. So, it will delete all backups that are older. 76 | 77 | **Parameters for `rotate()`** 78 | 79 | *int $rotate* 80 | Number of backups that you want to keep 81 | 82 | *** 83 | 84 | ### ~~`send()`~~ 85 | _(deprecated since 2.13.4, removed in 2.14.0)_ 86 | ```php 87 | send(string $filename, string $to) 88 | ``` 89 | ~~It sends a backup file via email.~~ 90 | 91 | **Parameters for `send()`** 92 | 93 | *string $filename* 94 | Filename of the backup that you want to send via email. The path can be relative to the backup directory. 95 | 96 | *string $recipient* 97 | Recipient's email address 98 | -------------------------------------------------------------------------------- /docs/Send backups via email.md: -------------------------------------------------------------------------------- 1 | # Send backups via email 2 | 3 | Up until version `2.13.3`, the plugin offered various possibilities to send the created backup files via email. 4 | All of these (classes, methods, options) **have been deprecated** with version `2.13.4` and **removed** with version `2.14.0`. 5 | 6 | - [Why were they deprecated](#why-were-they-deprecated) 7 | - [Send backups via email](#send-backups-via-email) 8 | - [Create a command to export and send backups via email](#create-a-command-to-export-and-send-backups-via-email) 9 | 10 | ## Why were they deprecated 11 | 12 | This plugin was born to allow me to quickly perform and manage my MySql database backups. 13 | Over time the plugin has expanded, offering both universal functions always related to backups (for example supporting Postgres and Sqlite, which I do not use), and extra functions such as sending by sending. 14 | Time is short, the desire to continue the development of this plugin (that is updated, functional, correctly written and fast) is strong, for this reason I decided to remove any extra functionality not strictly related to making a backup and delegate these to the end user (who can implement them independently). 15 | **Sorry**. 16 | 17 | ## Send backups via email 18 | 19 | Let's see how to implement a method that takes care of creating backups and sending them via email. 20 | 21 | In these examples the email will be sent from the address `sender@example.com` to the address `recipient@example.com`. The first sender address must of course be properly configured and able to send email. 22 | Please refer to the `Mailer` class described in the [CakePHP documentation](https://book.cakephp.org/5/en/core-libraries/email.html). 23 | 24 | ```php 25 | public function sendBackupWithMail(): void 26 | { 27 | /** 28 | * First I create a backup normally. 29 | * `$filename` will be the full path to the created backup. 30 | */ 31 | $BackupExport = new BackupExport(); 32 | $BackupExport->compression('gzip'); 33 | $filename = $BackupExport->export(); 34 | 35 | /** 36 | * Now I create a `Mailer` instance, which will take care of sending the email. 37 | * @see https://book.cakephp.org/5/en/core-libraries/email.html#namespace-Cake\Mailer 38 | */ 39 | $Mailer = new Mailer(); 40 | $Mailer 41 | ->setFrom('sender@example.com') 42 | ->setTo('recipient@example.com') 43 | ->setSubject('This mail contains my backup') 44 | ->setAttachments([ 45 | basename($filename) => ['file' => $filename, 'mimetype' => mime_content_type($filename)], 46 | ]) 47 | ->send(); 48 | } 49 | ``` 50 | 51 | ## Create a command to export and send backups via email 52 | 53 | However, the previous example could be inconvenient and impractical to use. Much better, instead, to implement a command that does the same thing (and that can be run from the command line, or set regularly with cronjob). 54 | 55 | In your application, create the `src/Command/ExportAndSendBackupCommand.php` file: 56 | 57 | ```php 58 | compression('gzip'); 84 | $filename = $BackupExport->export(); 85 | } catch (Exception $e) { 86 | $io->abort('`BackupExport` reported an error: "' . $e->getMessage() . '"'); 87 | } 88 | 89 | $io->success('Backup file `' .$filename . '`has been created'); 90 | 91 | /** 92 | * Now I create a `Mailer` instance, which will take care of sending the email. 93 | * @see https://book.cakephp.org/5/en/core-libraries/email.html#namespace-Cake\Mailer 94 | */ 95 | $recipient = 'recipient@example.com'; 96 | try { 97 | $Mailer = new Mailer(); 98 | $Mailer 99 | ->setFrom('sender@example.com') 100 | ->setTo($recipient) 101 | ->setSubject('This mail contains my backup') 102 | ->setAttachments([ 103 | basename($filename) => ['file' => $filename, 'mimetype' => mime_content_type($filename)], 104 | ]) 105 | ->send(); 106 | } catch (Exception $e) { 107 | $io->abort('`Mailer` reported an error: "' . $e->getMessage() . '"'); 108 | } 109 | 110 | $io->success('The backup has been sent to `' . $recipient . '`'); 111 | } 112 | } 113 | ``` 114 | 115 | The command also takes care of catching any exceptions thrown by `BackupExport` (during backup creation) or by `Mailer` (during email sending), returning a valid error to the console. 116 | 117 | Now you can run the command in the console: 118 | 119 | ```bash 120 | $ bin/cake export_and_send_backup 121 | ``` 122 | 123 | If everything went well, the output should be: 124 | 125 | ```bash 126 | $ bin/cake export_and_send_backup 127 | Backup file `/home/mirko/Server/my_app/backups/backup_climat_20250213165932.sql.gz`has been created 128 | The backup has been sent to `recipient@example.com` 129 | ``` 130 | 131 | Finally, you can set this command as a cron job, so that it runs at predefined intervals (see [Running Shells as Cron Jobs](https://book.cakephp.org/5/en/console-commands/cron-jobs.html#running-shells-as-cron-jobs)). 132 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | . 7 | 8 | 9 | 10 | 11 | 0 12 | 13 | 14 | 0 15 | 16 | 17 | 0 18 | 19 | 20 | /coverage/ 21 | /vendor 22 | 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/cakedc/cakephp-phpstan/extension.neon 3 | 4 | parameters: 5 | level: 8 6 | 7 | paths: 8 | - config 9 | - src 10 | - tests 11 | 12 | bootstrapFiles: 13 | - tests/bootstrap.php 14 | 15 | excludePaths: 16 | - tests/test_app/ 17 | -------------------------------------------------------------------------------- /resources/locales/database_backup.pot: -------------------------------------------------------------------------------- 1 | # LANGUAGE translation of cakephp-database-backup plugin 2 | # Copyright Mirko Pagliai 3 | # 4 | #, fuzzy 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PROJECT VERSION\n" 8 | "POT-Creation-Date: 2025-04-15 17:48+0200\n" 9 | "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n" 10 | "Last-Translator: NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=utf-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 16 | 17 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:45 18 | msgid "Exports a database backup" 19 | msgstr "" 20 | 21 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:55 22 | msgid "Compression type. By default, no compression will be used" 23 | msgstr "" 24 | 25 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:59 26 | msgid "Filename. It can be an absolute path and may contain patterns. The compression type will be automatically set" 27 | msgstr "" 28 | 29 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:64 30 | msgid "Rotates backups. You have to indicate the number of backups you want to keep. So, it will delete all backups that are older. By default, no backup will be deleted" 31 | msgstr "" 32 | 33 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:70 34 | msgid "Sends the backup file via email. You have to indicate the recipient's email address" 35 | msgstr "" 36 | 37 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:75 38 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:50 39 | msgid "Timeout for shell commands. Default value: {0} seconds" 40 | msgstr "" 41 | 42 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:137 43 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:116 44 | msgid "The `{0}` event stopped the operation" 45 | msgstr "" 46 | 47 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:140 48 | msgid "Backup `{0}` has been exported" 49 | msgstr "" 50 | 51 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:147 52 | msgid "Backup `{0}` has been deleted" 53 | msgstr "" 54 | 55 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:150 56 | msgid "Deleted backup files: {0}" 57 | msgstr "" 58 | 59 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:152 60 | msgid "No backup has been deleted" 61 | msgstr "" 62 | 63 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:44 64 | msgid "Imports a database backup" 65 | msgstr "" 66 | 67 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:46 68 | msgid "Filename. It can be an absolute path" 69 | msgstr "" 70 | 71 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:119 72 | msgid "Backup `{0}` has been imported" 73 | msgstr "" 74 | 75 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:42 76 | msgid "Lists database backups" 77 | msgstr "" 78 | 79 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:45 80 | msgid "List database backups in reverse order (oldest first, then newest)" 81 | msgstr "" 82 | 83 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:60 84 | msgid "Backup files found: {0}" 85 | msgstr "" 86 | 87 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:67 88 | msgid "Filename" 89 | msgstr "" 90 | 91 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:68 92 | msgid "Compression" 93 | msgstr "" 94 | 95 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:69 96 | msgid "Size" 97 | msgstr "" 98 | 99 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:70 100 | msgid "Datetime" 101 | msgstr "" 102 | 103 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:61 104 | msgid "Connection: {0}" 105 | msgstr "" 106 | 107 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:62 108 | msgid "Driver: {0}" 109 | msgstr "" 110 | 111 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:66 112 | msgid "Timeout for shell commands: {0} seconds" 113 | msgstr "" 114 | 115 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Executor/AbstractExecutor.php:241 116 | msgid "Binary for `{0}` could not be found. You have to set its path manually" 117 | msgstr "" 118 | 119 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/AbstractBackupUtility.php:153 120 | msgid "The Executor class for the `{0}` driver does not exist" 121 | msgstr "" 122 | 123 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:96 124 | msgid "File or directory `{0}` is not writable" 125 | msgstr "" 126 | 127 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:101 128 | msgid "File `{0}` already exists" 129 | msgstr "" 130 | 131 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:162 132 | msgid "Export failed with error message: `{0}`" 133 | msgstr "" 134 | 135 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:45 136 | msgid "File or directory `{0}` is not readable" 137 | msgstr "" 138 | 139 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:73 140 | msgid "You must first set the filename" 141 | msgstr "" 142 | 143 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:92 144 | msgid "Import failed with error message: `{0}`" 145 | msgstr "" 146 | 147 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupManager.php:74 148 | msgid "Invalid `$keep` value" 149 | msgstr "" 150 | 151 | -------------------------------------------------------------------------------- /resources/locales/it/database_backup.po: -------------------------------------------------------------------------------- 1 | # LANGUAGE translation of cakephp-database-backup plugin 2 | # Copyright Mirko Pagliai 3 | # 4 | # Mirko Pagliai , 2025. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PROJECT VERSION\n" 8 | "POT-Creation-Date: 2025-04-15 17:48+0200\n" 9 | "PO-Revision-Date: 2025-04-15 17:50+0200\n" 10 | "Last-Translator: Mirko Pagliai \n" 11 | "Language-Team: Italian \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | "Language: it_IT\n" 17 | "X-Generator: Lokalize 22.12.3\n" 18 | 19 | msgid "Name of the alternative connection to use, for example if you are not using the default connection" 20 | msgstr "Nome della connessione alternativa da utilizzare, ad esempio se non si sta utilizzando la connessione di default" 21 | 22 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:45 23 | msgid "Exports a database backup" 24 | msgstr "Esporta un backup del database" 25 | 26 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:55 27 | msgid "Compression type. By default, no compression will be used" 28 | msgstr "Tipo di compressione. Di default, nessuna compressione sarà utilizzata" 29 | 30 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:59 31 | msgid "" 32 | "Filename. It can be an absolute path and may contain patterns. The" 33 | " compression type will be automatically set" 34 | msgstr "" 35 | "Filename. Può essere una path assoluta e può contenere pattern. Il tipo di" 36 | " compressione sarà impostato automaticamente" 37 | 38 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:64 39 | msgid "" 40 | "Rotates backups. You have to indicate the number of backups you want to keep." 41 | " So, it will delete all backups that are older. By default, no backup will be" 42 | " deleted" 43 | msgstr "" 44 | "Ruota i backup. Devi indicare il numero di backup che vuoi mantenere. Quindi," 45 | " cancellerà tutti i backup più vecchi. Di default, nessun backup verrà" 46 | " cancellato" 47 | 48 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:70 49 | msgid "" 50 | "Sends the backup file via email. You have to indicate the recipient's email" 51 | " address" 52 | msgstr "" 53 | "Invia il backup via mail. Devi indicare l'indirizzo email del destinatario" 54 | 55 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:75 56 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:50 57 | msgid "Timeout for shell commands. Default value: {0} seconds" 58 | msgstr "Timeout per i comandi della shell. Valore di default: {0} secondi" 59 | 60 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:137 61 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:116 62 | msgid "The `{0}` event stopped the operation" 63 | msgstr "L'evento `{0}` ha stoppato l'operazione" 64 | 65 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:140 66 | msgid "Backup `{0}` has been exported" 67 | msgstr "Il backup `{0}` è stato esportato" 68 | 69 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:147 70 | msgid "Backup `{0}` has been deleted" 71 | msgstr "Il backup `{0}` è stato cancellato" 72 | 73 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:150 74 | msgid "Deleted backup files: {0}" 75 | msgstr "File di backup cancellati: {0}" 76 | 77 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ExportCommand.php:152 78 | msgid "No backup has been deleted" 79 | msgstr "Nessun backup è stato cancellato" 80 | 81 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:44 82 | msgid "Imports a database backup" 83 | msgstr "Importa un backup del database" 84 | 85 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:46 86 | msgid "Filename. It can be an absolute path" 87 | msgstr "Filename. Può essere un percorso assoluto" 88 | 89 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/ImportCommand.php:119 90 | msgid "Backup `{0}` has been imported" 91 | msgstr "Il backup `{0}` è stato importato" 92 | 93 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:42 94 | msgid "Lists database backups" 95 | msgstr "Elenca i backup del database" 96 | 97 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:45 98 | msgid "List database backups in reverse order (oldest first, then newest)" 99 | msgstr "" 100 | "Elenca i backup dei database in ordine inverso (prima i più vecchi, dopo i" 101 | " più recenti)" 102 | 103 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:60 104 | msgid "Backup files found: {0}" 105 | msgstr "File di backup trovati: {0}" 106 | 107 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:67 108 | msgid "Filename" 109 | msgstr "Filename" 110 | 111 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:68 112 | msgid "Compression" 113 | msgstr "Compressione" 114 | 115 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:69 116 | msgid "Size" 117 | msgstr "Dimensione" 118 | 119 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Command/IndexCommand.php:70 120 | msgid "Datetime" 121 | msgstr "Data e orario" 122 | 123 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:61 124 | msgid "Connection: {0}" 125 | msgstr "Connessione: {0}" 126 | 127 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:62 128 | msgid "Driver: {0}" 129 | msgstr "Driver: {0}" 130 | 131 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Console/Command.php:66 132 | msgid "Timeout for shell commands: {0} seconds" 133 | msgstr "Timeout per i comandi della shell: {0} secondi" 134 | 135 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Executor/AbstractExecutor.php:241 136 | msgid "Binary for `{0}` could not be found. You have to set its path manually" 137 | msgstr "" 138 | "Il binario per `{0}` non può essere trovato. Devi impostare manualmente la" 139 | " sua path" 140 | 141 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/AbstractBackupUtility.php:153 142 | msgid "The Executor class for the `{0}` driver does not exist" 143 | msgstr "La classe Executor for il driver `{0}` non esiste" 144 | 145 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:96 146 | msgid "File or directory `{0}` is not writable" 147 | msgstr "Il file o la directory `{0}` non è scrivibile" 148 | 149 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:101 150 | msgid "File `{0}` already exists" 151 | msgstr "Il file `{0}` già esiste" 152 | 153 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupExport.php:162 154 | msgid "Export failed with error message: `{0}`" 155 | msgstr "Esportazione fallita con messaggio di errore: `{0}`" 156 | 157 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:45 158 | msgid "File or directory `{0}` is not readable" 159 | msgstr "Il file o la directory `{0}` non è leggibile" 160 | 161 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:73 162 | msgid "You must first set the filename" 163 | msgstr "Devi prima impostare il filename" 164 | 165 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupImport.php:92 166 | msgid "Import failed with error message: `{0}`" 167 | msgstr "Importazione fallita con messaggio di errore: `{0}`" 168 | 169 | #: ./home/mirko/Libs/Plugins/cakephp-database-backup/src/Utility/BackupManager.php:74 170 | msgid "Invalid `$keep` value" 171 | msgstr "Valore `$keep` non valido" 172 | 173 | -------------------------------------------------------------------------------- /src/Command/ExportCommand.php: -------------------------------------------------------------------------------- 1 | setDescription(__d('database_backup', 'Exports a database backup')) 44 | ->addOptions([ 45 | 'compression' => [ 46 | 'choices' => array_map(callback: 'lcfirst', array: array_column( 47 | array: array_filter( 48 | array: Compression::cases(), 49 | callback: fn (Compression $Compression): bool => $Compression != Compression::None, 50 | ), 51 | column_key: 'name', 52 | )), 53 | 'help' => __d('database_backup', 'Compression type. By default, no compression will be used'), 54 | 'short' => 'c', 55 | ], 56 | 'filename' => [ 57 | 'help' => __d('database_backup', 'Filename. It can be an absolute path and may contain ' . 58 | 'patterns. The compression type will be automatically set'), 59 | 'short' => 'f', 60 | ], 61 | ]); 62 | } 63 | 64 | /** 65 | * Internal method to get a `BackupExport` instance. 66 | * 67 | * @return \DatabaseBackup\Utility\BackupExport 68 | */ 69 | protected function getBackupExport(): BackupExport 70 | { 71 | return new BackupExport(Connection: $this->Connection); 72 | } 73 | 74 | /** 75 | * Exports a database backup. 76 | * 77 | * This command uses the `SendCommand`. 78 | * 79 | * @param \Cake\Console\Arguments $args The command arguments 80 | * @param \Cake\Console\ConsoleIo $io The console io 81 | * @return void 82 | * @throws \Cake\Console\Exception\StopException 83 | */ 84 | #[Override] 85 | public function execute(Arguments $args, ConsoleIo $io): void 86 | { 87 | parent::execute($args, $io); 88 | 89 | try { 90 | $BackupExport = $this->getBackupExport(); 91 | 92 | /** 93 | * Sets the output filename or the compression type. 94 | * 95 | * Regarding the `rotate` option, the `BackupManager::rotate()` method will be called at the end. 96 | */ 97 | if ($args->getOption('filename')) { 98 | $BackupExport->filename((string)$args->getOption('filename')); 99 | } elseif ($args->getOption('compression')) { 100 | /** @see https://stackoverflow.com/a/77859717/1480263 */ 101 | $compression = (string)$args->getOption('compression'); 102 | 103 | $BackupExport->compression(constant(Compression::class . '::' . ucfirst($compression))); 104 | } 105 | //Sets the timeout 106 | if ($args->getOption('timeout')) { 107 | $BackupExport->timeout((int)$args->getOption('timeout')); 108 | } 109 | 110 | $filename = $BackupExport->export(); 111 | if (!$filename) { 112 | throw new StopException( 113 | __d('database_backup', 'The `{0}` event stopped the operation', 'Backup.beforeExport') 114 | ); 115 | } 116 | $io->success(__d('database_backup', 'Backup `{0}` has been exported', $this->makeRelativePath($filename))); 117 | } catch (Exception $e) { 118 | $io->abort($e->getMessage()); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Command/ImportCommand.php: -------------------------------------------------------------------------------- 1 | setDescription(__d('database_backup', 'Imports a database backup')) 44 | ->addArgument('filename', [ 45 | 'help' => __d('database_backup', 'Filename. It can be an absolute path'), 46 | 'required' => true, 47 | ]); 48 | } 49 | 50 | /** 51 | * Internal method to get a `BackupImport` instance. 52 | * 53 | * @return \DatabaseBackup\Utility\BackupImport 54 | */ 55 | protected function getBackupImport(): BackupImport 56 | { 57 | return new BackupImport(Connection: $this->Connection); 58 | } 59 | 60 | /** 61 | * Makes the absolute path for a filename. 62 | * 63 | * This allows you to use a path relative to ROOT, thus taking advantage of the shell's autocompletion. 64 | * 65 | * For example: 66 | * ``` 67 | * $ bin/cake database_backup.import backups/backup_myapp_20250305160001.sql.gz 68 | * ``` 69 | * 70 | * @param string $filename 71 | * @return string 72 | * @since 2.13.5 73 | */ 74 | public static function makeAbsoluteFilename(string $filename): string 75 | { 76 | if (Path::isRelative($filename) && is_readable(Path::makeAbsolute($filename, ROOT))) { 77 | $filename = Path::makeAbsolute($filename, ROOT); 78 | } 79 | 80 | return $filename; 81 | } 82 | 83 | /** 84 | * Imports a database backup. 85 | * 86 | * @param \Cake\Console\Arguments $args The command arguments 87 | * @param \Cake\Console\ConsoleIo $io The console io 88 | * @return void 89 | */ 90 | #[Override] 91 | public function execute(Arguments $args, ConsoleIo $io): void 92 | { 93 | parent::execute($args, $io); 94 | 95 | try { 96 | $BackupImport = $this->getBackupImport() 97 | ->filename($this->makeAbsoluteFilename((string)$args->getArgument('filename'))); 98 | 99 | //Sets the timeout 100 | if ($args->getOption('timeout')) { 101 | $BackupImport->timeout((int)$args->getOption('timeout')); 102 | } 103 | 104 | $filename = $BackupImport->import(); 105 | if (!$filename) { 106 | throw new StopException( 107 | __d('database_backup', 'The `{0}` event stopped the operation', 'Backup.beforeImport') 108 | ); 109 | } 110 | $io->success(__d('database_backup', 'Backup `{0}` has been imported', $this->makeRelativePath($filename))); 111 | } catch (Exception $e) { 112 | $io->abort($e->getMessage()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Compression.php: -------------------------------------------------------------------------------- 1 | value)) { 50 | return $Compression; 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | addOptions([ 48 | 'connection' => [ 49 | 'default' => 'default', 50 | 'help' => __d( 51 | 'database_backup', 52 | 'Name of the alternative connection to use, for example if you are not using the default connection' 53 | ), 54 | ], 55 | 'timeout' => [ 56 | 'help' => __d( 57 | 'database_backup', 58 | 'Timeout for shell commands. Default value: {0} seconds', 59 | Configure::readOrFail('DatabaseBackup.processTimeout') 60 | ), 61 | 'short' => 't', 62 | ], 63 | ]); 64 | } 65 | 66 | /** 67 | * Makes the relative path (relative to `ROOT`). 68 | * 69 | * @param string $path 70 | * @return string 71 | * @since 2.13.5 72 | */ 73 | public function makeRelativePath(string $path): string 74 | { 75 | return Path::isBasePath(ROOT, $path) ? Path::makeRelative($path, ROOT) : $path; 76 | } 77 | 78 | /** 79 | * @inheritDoc 80 | */ 81 | #[Override] 82 | public function execute(Arguments $args, ConsoleIo $io): void 83 | { 84 | try { 85 | /** @var string $connectionName */ 86 | $connectionName = $args->getOption('connection'); 87 | $this->Connection = ConnectionManager::get($connectionName); 88 | } catch (MissingDatasourceConfigException $E) { 89 | $io->abort($E->getMessage()); 90 | } 91 | 92 | $io->out(__d('database_backup', 'Connection: {0}', $this->Connection->config()['name'])); 93 | $io->out(__d('database_backup', 'Driver: {0}', $this->Connection->config()['driver'])); 94 | 95 | if ($args->getOption('timeout')) { 96 | $io->verbose( 97 | __d('database_backup', 'Timeout for shell commands: {0} seconds', $args->getOption('timeout')) 98 | ); 99 | } 100 | 101 | $io->hr(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Executor/AbstractExecutor.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | use EventDispatcherTrait; 41 | 42 | protected string $name; 43 | 44 | /** 45 | * Constructor. 46 | * 47 | * @param \Cake\Datasource\ConnectionInterface $Connection 48 | * @param string|null $name Driver name. By default, it will be automatically obtained from `$Connection` 49 | */ 50 | public function __construct(protected ConnectionInterface $Connection, ?string $name = null) 51 | { 52 | /** 53 | * For example, for `Cake\Database\Driver\Mysql` driver the name will be `MySql`. 54 | */ 55 | $this->name = $name ?: substr($Connection->getDriver()::class, strlen('Cake\\Database\\Driver\\')); 56 | 57 | //Attaches the object to the event manager 58 | $this->getEventManager()->on($this); 59 | } 60 | 61 | /** 62 | * Magic `__call()` method. 63 | * 64 | * @param string $name 65 | * @param array $arguments 66 | * @return string 67 | * @phpstan-ignore missingType.iterableValue 68 | */ 69 | public function __call(string $name, array $arguments): string 70 | { 71 | $replacements = [ 72 | 'getExportExecutable' => 'getExportCommand', 73 | 'getImportExecutable' => 'getImportCommand', 74 | ]; 75 | 76 | $replacement = $replacements[$name] ?? null; 77 | if ($replacement) { 78 | deprecationWarning('2.14.1', sprintf( 79 | 'The `AbstractExecutor::%s()` method is deprecated and will be removed in a future release. Use instead `%s()`', 80 | $name, 81 | $replacement 82 | )); 83 | 84 | return $this->{$replacement}(...$arguments); 85 | } 86 | 87 | throw new BadMethodCallException('Method `' . $this::class . '::' . $name . '()` does not exist.'); 88 | } 89 | 90 | /** 91 | * List of events this object is implementing. When the class is registered in an event manager, each individual 92 | * method will be associated with the respective event. 93 | * 94 | * @return array Associative array or event key names pointing to the function that should be called 95 | * in the object when the respective event is fired 96 | * @since 2.1.1 97 | */ 98 | #[Override] 99 | final public function implementedEvents(): array 100 | { 101 | return [ 102 | 'Backup.afterExport' => 'afterExport', 103 | 'Backup.afterImport' => 'afterImport', 104 | 'Backup.beforeExport' => 'beforeExport', 105 | 'Backup.beforeImport' => 'beforeImport', 106 | ]; 107 | } 108 | 109 | /** 110 | * Gets and parses commands from the configuration, according to the type of requested `OperationType` and the 111 | * connection driver. 112 | * 113 | * These commands are not yet final: use instead `getExportCommand()` and `getImportCommand()` methods to 114 | * have the final commands. 115 | * 116 | * @param \DatabaseBackup\OperationType $OperationType 117 | * @return string 118 | */ 119 | protected function getCommand(OperationType $OperationType): string 120 | { 121 | $replacements = [ 122 | '{{BINARY}}' => escapeshellarg($this->getBinary(DATABASE_BACKUP_EXECUTABLES[lcfirst($this->name)][$OperationType->value])), 123 | '{{AUTH_FILE}}' => method_exists($this, 'getAuthFilePath') && $this->getAuthFilePath() ? escapeshellarg($this->getAuthFilePath()) : '', 124 | '{{DB_USER}}' => $this->getConfig('username'), 125 | '{{DB_PASSWORD}}' => $this->getConfig('password') ? ':' . $this->getConfig('password') : '', 126 | '{{DB_HOST}}' => $this->getConfig('host'), 127 | '{{DB_NAME}}' => $this->getConfig('database'), 128 | ]; 129 | 130 | /** @var string $exec */ 131 | $exec = Configure::readOrFail('DatabaseBackup.' . $this->name . '.' . $OperationType->value); 132 | 133 | return str_replace(array_keys($replacements), $replacements, $exec); 134 | } 135 | 136 | /** 137 | * Gets the command to export the database, with compression if requested. 138 | * 139 | * @param string $filename Filename where you want to export the database 140 | * @return string 141 | * @throws \LogicException 142 | * @throws \ValueError With a filename that does not match any supported compression. 143 | */ 144 | public function getExportCommand(string $filename): string 145 | { 146 | $exec = $this->getCommand(OperationType::Export); 147 | 148 | $Compression = Compression::fromFilename($filename); 149 | if ($Compression !== Compression::None) { 150 | $exec .= ' | ' . escapeshellarg($this->getBinary($Compression)); 151 | } 152 | 153 | return $exec . ' > ' . escapeshellarg($filename); 154 | } 155 | 156 | /** 157 | * Gets the command to import the database, with compression if requested. 158 | * 159 | * @param string $filename Filename from which you want to import the database 160 | * @return string 161 | * @throws \LogicException 162 | */ 163 | public function getImportCommand(string $filename): string 164 | { 165 | $exec = $this->getCommand(OperationType::Import); 166 | 167 | $Compression = Compression::fromFilename($filename); 168 | if ($Compression !== Compression::None) { 169 | return sprintf( 170 | '%s -dc %s | ', 171 | escapeshellarg($this->getBinary($Compression)), 172 | escapeshellarg($filename) 173 | ) . $exec; 174 | } 175 | 176 | return $exec . ' < ' . escapeshellarg($filename); 177 | } 178 | 179 | /** 180 | * Called after export. 181 | * 182 | * @codeCoverageIgnore 183 | * @return void 184 | * @since 2.1.0 185 | */ 186 | public function afterExport(): void 187 | { 188 | } 189 | 190 | /** 191 | * Called after import. 192 | * 193 | * @codeCoverageIgnore 194 | * @return void 195 | * @since 2.1.0 196 | */ 197 | public function afterImport(): void 198 | { 199 | } 200 | 201 | /** 202 | * Called before export. 203 | * 204 | * @param \Cake\Event\EventInterface $Event 205 | * @return void 206 | * @since 2.1.0 207 | * @codeCoverageIgnore 208 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 209 | */ 210 | public function beforeExport(EventInterface $Event) 211 | { 212 | } 213 | 214 | /** 215 | * Called before import. 216 | * 217 | * @param \Cake\Event\EventInterface $Event 218 | * @return void 219 | * @since 2.1.0 220 | * @codeCoverageIgnore 221 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 222 | */ 223 | public function beforeImport(EventInterface $Event) 224 | { 225 | } 226 | 227 | /** 228 | * Gets a binary path. 229 | * 230 | * @param \DatabaseBackup\Compression|string $binaryName Binary name 231 | * @return string 232 | * @throws \InvalidArgumentException 233 | */ 234 | public function getBinary(Compression|string $binaryName): string 235 | { 236 | if ($binaryName instanceof Compression) { 237 | $binaryName = lcfirst($binaryName->name); 238 | } 239 | 240 | $binary = Configure::read('DatabaseBackup.binaries.' . $binaryName); 241 | if (!$binary) { 242 | throw new InvalidArgumentException(__d( 243 | 'database_backup', 244 | 'Binary for `{0}` could not be found. You have to set its path manually', 245 | $binaryName 246 | )); 247 | } 248 | 249 | return $binary; 250 | } 251 | 252 | /** 253 | * Gets a config value of the connection. 254 | * 255 | * @param string $key Config key 256 | * @return mixed Config value or `null` if the key doesn't exist 257 | * @since 2.3.0 258 | */ 259 | public function getConfig(string $key): mixed 260 | { 261 | return $this->Connection->config()[$key] ?? null; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Executor/MysqlExecutor.php: -------------------------------------------------------------------------------- 1 | deleteAuthFile(); 53 | } 54 | 55 | /** 56 | * Called after import. 57 | * 58 | * @return void 59 | * @since 2.1.0 60 | */ 61 | #[Override] 62 | public function afterImport(): void 63 | { 64 | $this->deleteAuthFile(); 65 | } 66 | 67 | /** 68 | * Called before export. 69 | * 70 | * It stores the authentication data, to be used to export the database, in a temporary file. 71 | * 72 | * For security reasons, it's recommended to specify the password in a configuration file and not in the command (a 73 | * user can execute a `ps aux | grep mysqldump` and see the password). 74 | * So it creates a temporary file to store the configuration options. 75 | * 76 | * @param \Cake\Event\EventInterface $Event 77 | * @return void 78 | * @since 2.1.0 79 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 80 | */ 81 | #[Override] 82 | public function beforeExport(EventInterface $Event) 83 | { 84 | $result = $this->writeAuthFile('[mysqldump]' . PHP_EOL . 85 | 'user={{USER}}' . PHP_EOL . 86 | 'password="{{PASSWORD}}"' . PHP_EOL . 87 | 'host={{HOST}}'); 88 | 89 | $Event->setResult($result); 90 | } 91 | 92 | /** 93 | * Called before export. 94 | * 95 | * It stores the authentication data, to be used to import the database, in a temporary file. 96 | * 97 | * For security reasons, it's recommended to specify the password in a configuration file and not in the command (a 98 | * user can execute a `ps aux | grep mysqldump` and see the password). 99 | * So it creates a temporary file to store the configuration options. 100 | * 101 | * @param \Cake\Event\EventInterface $Event 102 | * @return void 103 | * @since 2.1.0 104 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 105 | */ 106 | #[Override] 107 | public function beforeImport(EventInterface $Event) 108 | { 109 | $result = $this->writeAuthFile('[client]' . PHP_EOL . 110 | 'user={{USER}}' . PHP_EOL . 111 | 'password="{{PASSWORD}}"' . PHP_EOL . 112 | 'host={{HOST}}'); 113 | 114 | $Event->setResult($result); 115 | } 116 | 117 | /** 118 | * Internal method to get the auth file path. 119 | * 120 | * This method returns only the path that will be used and does not verify that the file already exists. 121 | * 122 | * @return string 123 | * @since 2.11.0 124 | */ 125 | protected function getAuthFilePath(): string 126 | { 127 | if (empty($this->auth)) { 128 | $this->auth = TMP . uniqid('auth'); 129 | } 130 | 131 | return $this->auth; 132 | } 133 | 134 | /** 135 | * Internal method to write an auth file. 136 | * 137 | * @param string $content Content 138 | * @return bool 139 | * @since 2.3.0 140 | */ 141 | protected function writeAuthFile(string $content): bool 142 | { 143 | $content = str_replace( 144 | [ 145 | '{{USER}}', 146 | '{{PASSWORD}}', 147 | '{{HOST}}', 148 | ], 149 | [ 150 | (string)$this->getConfig('username'), 151 | (string)$this->getConfig('password'), 152 | (string)$this->getConfig('host'), 153 | ], 154 | $content 155 | ); 156 | 157 | $Filesystem = $this->getFilesystem(); 158 | $Filesystem->dumpFile($this->getAuthFilePath(), $content); 159 | 160 | return $Filesystem->exists($this->getAuthFilePath()); 161 | } 162 | 163 | /** 164 | * Deletes the temporary file with the database authentication data. 165 | * 166 | * @return void 167 | * @since 2.1.0 168 | */ 169 | protected function deleteAuthFile(): void 170 | { 171 | $this->getFilesystem()->remove($this->getAuthFilePath()); 172 | 173 | unset($this->auth); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Executor/PostgresExecutor.php: -------------------------------------------------------------------------------- 1 | Connection; 37 | 38 | $SchemaCollection = $Connection->getSchemaCollection(); 39 | foreach ($SchemaCollection->listTables() as $tableName) { 40 | /** @var \Cake\Database\Schema\TableSchema $TableSchema */ 41 | $TableSchema = $SchemaCollection->describe($tableName); 42 | 43 | foreach ($TableSchema->dropSql($Connection) as $dropSql) { 44 | $Connection->execute($dropSql); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | #[Override] 53 | public function beforeImport(EventInterface $Event) 54 | { 55 | /** @var \Cake\Database\Connection $Connection */ 56 | $Connection = $this->Connection; 57 | 58 | //For each table, drops the table 59 | $this->dropAllTables(); 60 | 61 | //Needs disconnect and re-connect because the database schema has changed 62 | $Connection->getDriver()->disconnect(); 63 | $Connection->getDriver()->connect(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/OperationType.php: -------------------------------------------------------------------------------- 1 | files() 42 | ->in(Configure::readOrFail('DatabaseBackup.target')) 43 | ->name('/\.sql(\.(gz|bz2))?$/'); 44 | 45 | foreach ($Finder as $File) { 46 | unlink($File->getPathname()); 47 | } 48 | } 49 | 50 | /** 51 | * @return \DatabaseBackup\Utility\BackupExport 52 | * @codeCoverageIgnore 53 | */ 54 | protected function getBackupExport(): BackupExport 55 | { 56 | return new BackupExport(); 57 | } 58 | 59 | /** 60 | * Creates a backup file for tests. 61 | * 62 | * @param string $filename 63 | * @param bool $fakeBackup With `true`, it will create a fake file (i.e. with empty content). 64 | * @return string 65 | */ 66 | public function createBackup(string $filename = 'backup.sql', bool $fakeBackup = false): string 67 | { 68 | if ($fakeBackup) { 69 | if (Path::isRelative($filename)) { 70 | $filename = TMP . 'backups' . DS . $filename; 71 | } 72 | file_put_contents($filename, ''); 73 | 74 | return $filename; 75 | } 76 | 77 | return $this->getBackupExport() 78 | ->filename($filename) 79 | ->export() ?: ''; 80 | } 81 | 82 | /** 83 | * Creates some backup files for tests. 84 | * 85 | * @return array 86 | */ 87 | public function createSomeBackups(?int $timestamp = null): array 88 | { 89 | $timestamp = $timestamp ?: time(); 90 | 91 | return array_map( 92 | callback: function (Compression $Compression) use (&$timestamp): string { 93 | $timestamp = $timestamp - 60; 94 | $file = $this->createBackup(filename: 'backup_test_' . $timestamp . '.' . $Compression->value, fakeBackup: true); 95 | touch($file, $timestamp); 96 | 97 | return $file; 98 | }, 99 | array: Compression::cases() 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Utility/AbstractBackupUtility.php: -------------------------------------------------------------------------------- 1 | Connection = $Connection; 72 | } 73 | 74 | /** 75 | * Magic `__call()` method. 76 | * 77 | * It provides all `getX()` methods to get properties. 78 | * 79 | * @param string $method 80 | * @param array $args 81 | * @return mixed 82 | * @since 2.14.0 83 | * @phpstan-ignore missingType.iterableValue 84 | */ 85 | public function __call(string $method, array $args): mixed 86 | { 87 | if (str_starts_with($method, 'get')) { 88 | $property = substr($method, 3); 89 | foreach ([$property, lcfirst($property)] as $property) { 90 | if (property_exists($this, $property)) { 91 | return $this->{$property}; 92 | } 93 | } 94 | } 95 | 96 | throw new BadMethodCallException('Method `' . $this::class . '::' . $method . '()` does not exist.'); 97 | } 98 | 99 | /** 100 | * Makes the absolute path. 101 | * 102 | * @param string $path 103 | * @return string 104 | * @since 2.13.5 105 | */ 106 | public function makeAbsolutePath(string $path): string 107 | { 108 | return Path::makeAbsolute($path, Configure::readOrFail('DatabaseBackup.target')); 109 | } 110 | 111 | /** 112 | * Sets the filename. 113 | * 114 | * @param string $filename Filename. It can be an absolute path 115 | * @return self 116 | */ 117 | abstract public function filename(string $filename): self; 118 | 119 | /** 120 | * Sets the timeout for shell commands. 121 | * 122 | * @param int $timeout Timeout in seconds 123 | * @return self 124 | * @since 2.12.0 125 | */ 126 | public function timeout(int $timeout): self 127 | { 128 | $this->timeout = $timeout; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Gets the `Executor` instance according to the connection. 135 | * 136 | * @return \DatabaseBackup\Executor\AbstractExecutor 137 | * @since 2.14.0 138 | */ 139 | public function getExecutor(): AbstractExecutor 140 | { 141 | if (empty($this->Executor)) { 142 | //For example `$driverName` is `Mysql` 143 | $driverName = substr(strrchr($this->Connection->getDriver()::class, '\\') ?: '', 1); 144 | 145 | /** @var class-string<\DatabaseBackup\Executor\AbstractExecutor> $executorClassName */ 146 | $executorClassName = App::classname('DatabaseBackup.' . $driverName . 'Executor', 'Executor'); 147 | if (!$executorClassName) { 148 | throw new InvalidArgumentException(__d('database_backup', 'The Executor class for the `{0}` driver does not exist', $driverName)); 149 | } 150 | 151 | $this->Executor = new $executorClassName(Connection: $this->Connection); 152 | } 153 | 154 | return $this->Executor; 155 | } 156 | 157 | /** 158 | * Internal method to run and get a `Process` instance as a command-line to be run in a shell wrapper. 159 | * 160 | * @param string $command The command line to pass to the shell of the OS 161 | * @return \Symfony\Component\Process\Process 162 | * @see https://symfony.com/doc/current/components/process.html 163 | * @since 2.8.7 164 | */ 165 | protected function getProcess(string $command): Process 166 | { 167 | $Process = Process::fromShellCommandline(command: $command); 168 | $Process->setTimeout(timeout: $this->getTimeout() ?: Configure::readOrFail('DatabaseBackup.processTimeout')); 169 | $Process->run(); 170 | 171 | return $Process; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Utility/BackupExport.php: -------------------------------------------------------------------------------- 1 | compression = $Compression; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Sets the filename. 69 | * 70 | * The compression type will be automatically set by the filename. 71 | * 72 | * @param string $filename Filename. It can be an absolute path and may contain patterns 73 | * @return self 74 | * @throws \Symfony\Component\Filesystem\Exception\IOException If the target (directory) is not writable or if the filename already exists. 75 | * @throws \ValueError With a filename that does not match any supported compression. 76 | */ 77 | #[Override] 78 | public function filename(string $filename): self 79 | { 80 | $Executor = $this->getExecutor(); 81 | 82 | //Replaces patterns 83 | $filename = str_replace(['{$DATABASE}', '{$DATETIME}', '{$HOSTNAME}', '{$TIMESTAMP}'], [ 84 | pathinfo($Executor->getConfig('database'), PATHINFO_FILENAME), 85 | date('YmdHis'), 86 | str_replace(['127.0.0.1', '::1'], 'localhost', $Executor->getConfig('host') ?? 'localhost'), 87 | (string)time(), 88 | ], $filename); 89 | 90 | $filename = $this->makeAbsolutePath($filename); 91 | if (!is_writable(dirname($filename))) { 92 | throw new IOException( 93 | __d('database_backup', 'File or directory `{0}` is not writable', dirname($filename)) 94 | ); 95 | } 96 | if (file_exists($filename)) { 97 | throw new IOException( 98 | __d('database_backup', 'File `{0}` already exists', $filename) 99 | ); 100 | } 101 | 102 | //Sets the compression 103 | $this->compression = Compression::fromFilename($filename); 104 | 105 | $this->filename = $filename; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Sets the number of backups you want to keep. So, it will delete all backups that are older. 112 | * 113 | * @param int $keep Number of backups you want to keep 114 | * @return self 115 | * @deprecated `BackupExport::rotate()` has been deprecated and will be removed in a future release 116 | */ 117 | public function rotate(int $keep): self 118 | { 119 | deprecationWarning( 120 | '2.15.0', 121 | '`BackupExport::rotate()` has been deprecated and will be removed in a future release.' 122 | ); 123 | 124 | $this->rotate = $keep; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Exports the database. 131 | * 132 | * When exporting, this method will trigger these events (implemented by the `Executor` class): 133 | * - `Backup.beforeExport`: will be triggered before export; 134 | * - `Backup.afterExport`: will be triggered after export. 135 | * 136 | * @return string|false Filename path on success or `false` if the `Backup.beforeExport` event is stopped 137 | * @throws \RuntimeException When export fails 138 | * @see \DatabaseBackup\Executor\AbstractExecutor::afterExport() 139 | * @see \DatabaseBackup\Executor\AbstractExecutor::beforeExport() 140 | */ 141 | public function export(): string|false 142 | { 143 | if (empty($this->filename)) { 144 | $this->filename('backup_{$DATABASE}_{$DATETIME}.' . $this->getCompression()->value); 145 | } 146 | 147 | //This allows the filename to be set again with the next call of this method 148 | $filename = $this->getFilename(); 149 | $this->filename = ''; 150 | 151 | $Executor = $this->getExecutor(); 152 | 153 | //Dispatches the `Backup.beforeExport` event implemented by the `Executor` class 154 | $BeforeExport = $Executor->dispatchEvent('Backup.beforeExport'); 155 | if ($BeforeExport->isStopped()) { 156 | return false; 157 | } 158 | 159 | //Exports 160 | $Process = $this->getProcess($Executor->getExportCommand($filename)); 161 | if (!$Process->isSuccessful()) { 162 | throw new RuntimeException( 163 | __d('database_backup', 'Export failed with error message: `{0}`', rtrim($Process->getErrorOutput())) 164 | ); 165 | } 166 | 167 | $this->getFilesystem()->chmod($filename, Configure::read('DatabaseBackup.chmod', 0664)); 168 | 169 | //Dispatches the `Backup.afterExport` event implemented by the `Executor` class 170 | $Executor->dispatchEvent('Backup.afterExport'); 171 | 172 | if ($this->getRotate()) { 173 | BackupManager::rotate($this->getRotate()); 174 | } 175 | 176 | return $filename; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Utility/BackupImport.php: -------------------------------------------------------------------------------- 1 | makeAbsolutePath($filename); 41 | if (!is_readable($filename)) { 42 | throw new IOException( 43 | __d('database_backup', 'File or directory `{0}` is not readable', $filename) 44 | ); 45 | } 46 | 47 | Compression::fromFilename($filename); 48 | 49 | $this->filename = $filename; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Imports the database. 56 | * 57 | * When importing, this method will trigger these events (implemented by the `Executor` class): 58 | * - `Backup.beforeImport`: will be triggered before import; 59 | * - `Backup.afterImport`: will be triggered after import. 60 | * 61 | * @return string|false Filename path on success or `false` if the `Backup.beforeImport` event is stopped 62 | * @throws \BadMethodCallException When the filename has not been set 63 | * @throws \RuntimeException When import fails 64 | * @see \DatabaseBackup\Executor\AbstractExecutor::afterImport() 65 | * @see \DatabaseBackup\Executor\AbstractExecutor::beforeImport() 66 | */ 67 | public function import(): string|false 68 | { 69 | if (empty($this->filename)) { 70 | throw new BadMethodCallException(__d('database_backup', 'You must first set the filename')); 71 | } 72 | 73 | //This allows the filename to be set again with a next call of this method 74 | $filename = $this->getFilename(); 75 | $this->filename = ''; 76 | 77 | $Executor = $this->getExecutor(); 78 | 79 | //Dispatches the `Backup.beforeImport` event implemented by the `Executor` class 80 | $BeforeImport = $Executor->dispatchEvent('Backup.beforeImport'); 81 | if ($BeforeImport->isStopped()) { 82 | return false; 83 | } 84 | 85 | //Imports 86 | $Process = $this->getProcess($Executor->getImportCommand($filename)); 87 | if (!$Process->isSuccessful()) { 88 | throw new RuntimeException( 89 | __d('database_backup', 'Import failed with error message: `{0}`', rtrim($Process->getErrorOutput())) 90 | ); 91 | } 92 | 93 | //Dispatches the `Backup.afterImport` event implemented by the `Executor` class 94 | $Executor->dispatchEvent('Backup.afterImport'); 95 | 96 | return $filename; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Utility/BackupManager.php: -------------------------------------------------------------------------------- 1 | files() 49 | ->in(Configure::readOrFail('DatabaseBackup.target')) 50 | ->name('/\.sql(\.(gz|bz2))?$/') 51 | //Sorts in descending order by the last modified date 52 | ->sort(fn (SplFileInfo $a, SplFileInfo $b): int => $b->getMTime() - $a->getMTime()); 53 | 54 | $DateTimeZone = DateTime::now()->getTimezone(); 55 | 56 | return (new Collection($Finder)) 57 | ->map(fn (SplFileInfo $File): array => [ 58 | 'basename' => $File->getBasename(), 59 | 'path' => $File->getPathname(), 60 | 'compression' => Compression::fromFilename($File->getFilename()), 61 | 'size' => $File->getSize(), 62 | 'datetime' => DateTime::createFromTimestamp($File->getMTime(), $DateTimeZone), 63 | ]) 64 | ->compile(false); 65 | } 66 | 67 | /** 68 | * Rotates backups. 69 | * 70 | * You must indicate the number of backups you want to keep. So, it will delete all backups that are older. 71 | * 72 | * @param int $keep Number of backups that you want to keep 73 | * @return array 74 | * @throws \InvalidArgumentException With an Invalid rotate value. 75 | * @deprecated `BackupManager::rotate()` has been deprecated and will be removed in a future release 76 | */ 77 | public static function rotate(int $keep): array 78 | { 79 | deprecationWarning( 80 | '2.15.0', 81 | '`BackupManager::rotate()` has been deprecated and will be removed in a future release' 82 | ); 83 | 84 | if ($keep < 1) { 85 | throw new InvalidArgumentException(__d('database_backup', 'Invalid `$keep` value')); 86 | } 87 | 88 | return self::index() 89 | ->skip($keep) 90 | ->each(fn (array $file) => unlink($file['path'])) 91 | ->toList(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 2.15.0 2 | --------------------------------------------------------------------------------