├── .ddev └── config.yaml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE.txt ├── README.rst ├── UPGRADE.rst ├── composer.json ├── deployer └── db │ ├── config │ ├── host.php │ ├── options.php │ └── set.php │ └── task │ ├── db_backup.php │ ├── db_compress.php │ ├── db_copy.php │ ├── db_decompress.php │ ├── db_download.php │ ├── db_dumpclean.php │ ├── db_export.php │ ├── db_import.php │ ├── db_process.php │ ├── db_pull.php │ ├── db_push.php │ ├── db_rmdump.php │ ├── db_truncate.php │ └── db_upload.php └── src ├── Driver └── EnvDriver.php └── Utility ├── ArrayUtility.php ├── ConsoleUtility.php ├── DatabaseUtility.php ├── FileUtility.php ├── OptionUtility.php └── RsyncUtility.php /.ddev/config.yaml: -------------------------------------------------------------------------------- 1 | name: deployer-extended-database 2 | type: php 3 | docroot: "" 4 | php_version: "8.0" 5 | webserver_type: nginx-fpm 6 | xdebug_enabled: false 7 | additional_hostnames: [] 8 | additional_fqdns: [] 9 | database: 10 | type: mariadb 11 | version: "10.11" 12 | use_dns_when_possible: true 13 | composer_version: "2" 14 | web_environment: [] 15 | corepack_enable: false 16 | 17 | # Key features of DDEV's config.yaml: 18 | 19 | # name: # Name of the project, automatically provides 20 | # http://projectname.ddev.site and https://projectname.ddev.site 21 | 22 | # type: # backdrop, cakephp, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress 23 | # See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more 24 | # information on the different project types 25 | 26 | # docroot: # Relative path to the directory containing index.php. 27 | 28 | # php_version: "8.3" # PHP version to use, "5.6" through "8.4" 29 | 30 | # You can explicitly specify the webimage but this 31 | # is not recommended, as the images are often closely tied to DDEV's' behavior, 32 | # so this can break upgrades. 33 | 34 | # webimage: # nginx/php docker image. 35 | 36 | # database: 37 | # type: # mysql, mariadb, postgres 38 | # version: # database version, like "10.11" or "8.0" 39 | # MariaDB versions can be 5.5-10.8, 10.11, and 11.4. 40 | # MySQL versions can be 5.5-8.0. 41 | # PostgreSQL versions can be 9-17. 42 | 43 | # router_http_port: # Port to be used for http (defaults to global configuration, usually 80) 44 | # router_https_port: # Port for https (defaults to global configuration, usually 443) 45 | 46 | # xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" 47 | # Note that for most people the commands 48 | # "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, 49 | # as leaving Xdebug enabled all the time is a big performance hit. 50 | 51 | # xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart" 52 | # Note that for most people the commands 53 | # "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better, 54 | # as leaving Xhprof enabled all the time is a big performance hit. 55 | 56 | # webserver_type: nginx-fpm or apache-fpm 57 | 58 | # timezone: Europe/Berlin 59 | # If timezone is unset, DDEV will attempt to derive it from the host system timezone 60 | # using the $TZ environment variable or the /etc/localtime symlink. 61 | # This is the timezone used in the containers and by PHP; 62 | # it can be set to any valid timezone, 63 | # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 64 | # For example Europe/Dublin or MST7MDT 65 | 66 | # composer_root: 67 | # Relative path to the Composer root directory from the project root. This is 68 | # the directory which contains the composer.json and where all Composer related 69 | # commands are executed. 70 | 71 | # composer_version: "2" 72 | # You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1 73 | # to use the latest major version available at the time your container is built. 74 | # It is also possible to use each other Composer version channel. This includes: 75 | # - 2.2 (latest Composer LTS version) 76 | # - stable 77 | # - preview 78 | # - snapshot 79 | # Alternatively, an explicit Composer version may be specified, for example "2.2.18". 80 | # To reinstall Composer after the image was built, run "ddev debug rebuild". 81 | 82 | # nodejs_version: "22" 83 | # change from the default system Node.js version to any other version. 84 | # See https://ddev.readthedocs.io/en/stable/users/configuration/config/#nodejs_version for more information 85 | # and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation, 86 | # Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use, 87 | # can specify any version, and is more robust than using 'nvm'. 88 | 89 | # corepack_enable: false 90 | # Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm 91 | 92 | # additional_hostnames: 93 | # - somename 94 | # - someothername 95 | # would provide http and https URLs for "somename.ddev.site" 96 | # and "someothername.ddev.site". 97 | 98 | # additional_fqdns: 99 | # - example.com 100 | # - sub1.example.com 101 | # would provide http and https URLs for "example.com" and "sub1.example.com" 102 | # Please take care with this because it can cause great confusion. 103 | 104 | # upload_dirs: "custom/upload/dir" 105 | # 106 | # upload_dirs: 107 | # - custom/upload/dir 108 | # - ../private 109 | # 110 | # would set the destination paths for ddev import-files to /custom/upload/dir 111 | # When Mutagen is enabled this path is bind-mounted so that all the files 112 | # in the upload_dirs don't have to be synced into Mutagen. 113 | 114 | # disable_upload_dirs_warning: false 115 | # If true, turns off the normal warning that says 116 | # "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" 117 | 118 | # ddev_version_constraint: "" 119 | # Example: 120 | # ddev_version_constraint: ">= 1.22.4" 121 | # This will enforce that the running ddev version is within this constraint. 122 | # See https://github.com/Masterminds/semver#checking-version-constraints for 123 | # supported constraint formats 124 | 125 | # working_dir: 126 | # web: /var/www/html 127 | # db: /home 128 | # would set the default working directory for the web and db services. 129 | # These values specify the destination directory for ddev ssh and the 130 | # directory in which commands passed into ddev exec are run. 131 | 132 | # omit_containers: [db, ddev-ssh-agent] 133 | # Currently only these containers are supported. Some containers can also be 134 | # omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit 135 | # the "db" container, several standard features of DDEV that access the 136 | # database container will be unusable. In the global configuration it is also 137 | # possible to omit ddev-router, but not here. 138 | 139 | # performance_mode: "global" 140 | # DDEV offers performance optimization strategies to improve the filesystem 141 | # performance depending on your host system. Should be configured globally. 142 | # 143 | # If set, will override the global config. Possible values are: 144 | # - "global": uses the value from the global config. 145 | # - "none": disables performance optimization for this project. 146 | # - "mutagen": enables Mutagen for this project. 147 | # - "nfs": enables NFS for this project. 148 | # 149 | # See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs 150 | # See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen 151 | 152 | # fail_on_hook_fail: False 153 | # Decide whether 'ddev start' should be interrupted by a failing hook 154 | 155 | # host_https_port: "59002" 156 | # The host port binding for https can be explicitly specified. It is 157 | # dynamic unless otherwise specified. 158 | # This is not used by most people, most people use the *router* instead 159 | # of the localhost port. 160 | 161 | # host_webserver_port: "59001" 162 | # The host port binding for the ddev-webserver can be explicitly specified. It is 163 | # dynamic unless otherwise specified. 164 | # This is not used by most people, most people use the *router* instead 165 | # of the localhost port. 166 | 167 | # host_db_port: "59002" 168 | # The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic 169 | # unless explicitly specified. 170 | 171 | # mailpit_http_port: "8025" 172 | # mailpit_https_port: "8026" 173 | # The Mailpit ports can be changed from the default 8025 and 8026 174 | 175 | # host_mailpit_port: "8025" 176 | # The mailpit port is not normally bound on the host at all, instead being routed 177 | # through ddev-router, but it can be bound directly to localhost if specified here. 178 | 179 | # webimage_extra_packages: [php7.4-tidy, php-bcmath] 180 | # Extra Debian packages that are needed in the webimage can be added here 181 | 182 | # dbimage_extra_packages: [telnet,netcat] 183 | # Extra Debian packages that are needed in the dbimage can be added here 184 | 185 | # use_dns_when_possible: true 186 | # If the host has internet access and the domain configured can 187 | # successfully be looked up, DNS will be used for hostname resolution 188 | # instead of editing /etc/hosts 189 | # Defaults to true 190 | 191 | # project_tld: ddev.site 192 | # The top-level domain used for project URLs 193 | # The default "ddev.site" allows DNS lookup via a wildcard 194 | # If you prefer you can change this to "ddev.local" to preserve 195 | # pre-v1.9 behavior. 196 | 197 | # ngrok_args: --basic-auth username:pass1234 198 | # Provide extra flags to the "ngrok http" command, see 199 | # https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h" 200 | 201 | # disable_settings_management: false 202 | # If true, DDEV will not create CMS-specific settings files like 203 | # Drupal's settings.php/settings.ddev.php or TYPO3's additional.php 204 | # In this case the user must provide all such settings. 205 | 206 | # You can inject environment variables into the web container with: 207 | # web_environment: 208 | # - SOMEENV=somevalue 209 | # - SOMEOTHERENV=someothervalue 210 | 211 | # no_project_mount: false 212 | # (Experimental) If true, DDEV will not mount the project into the web container; 213 | # the user is responsible for mounting it manually or via a script. 214 | # This is to enable experimentation with alternate file mounting strategies. 215 | # For advanced users only! 216 | 217 | # bind_all_interfaces: false 218 | # If true, host ports will be bound on all network interfaces, 219 | # not the localhost interface only. This means that ports 220 | # will be available on the local network if the host firewall 221 | # allows it. 222 | 223 | # default_container_timeout: 120 224 | # The default time that DDEV waits for all containers to become ready can be increased from 225 | # the default 120. This helps in importing huge databases, for example. 226 | 227 | #web_extra_exposed_ports: 228 | #- name: nodejs 229 | # container_port: 3000 230 | # http_port: 2999 231 | # https_port: 3000 232 | #- name: something 233 | # container_port: 4000 234 | # https_port: 4000 235 | # http_port: 3999 236 | # Allows a set of extra ports to be exposed via ddev-router 237 | # Fill in all three fields even if you don’t intend to use the https_port! 238 | # If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. 239 | # 240 | # The port behavior on the ddev-webserver must be arranged separately, for example 241 | # using web_extra_daemons. 242 | # For example, with a web app on port 3000 inside the container, this config would 243 | # expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 244 | # web_extra_exposed_ports: 245 | # - name: myapp 246 | # container_port: 3000 247 | # http_port: 9998 248 | # https_port: 9999 249 | 250 | #web_extra_daemons: 251 | #- name: "http-1" 252 | # command: "/var/www/html/node_modules/.bin/http-server -p 3000" 253 | # directory: /var/www/html 254 | #- name: "http-2" 255 | # command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" 256 | # directory: /var/www/html 257 | 258 | # override_config: false 259 | # By default, config.*.yaml files are *merged* into the configuration 260 | # But this means that some things can't be overridden 261 | # For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge 262 | # and you can't erase existing hooks or all environment variables. 263 | # However, with "override_config: true" in a particular config.*.yaml file, 264 | # 'use_dns_when_possible: false' can override the existing values, and 265 | # hooks: 266 | # post-start: [] 267 | # or 268 | # web_environment: [] 269 | # or 270 | # additional_hostnames: [] 271 | # can have their intended affect. 'override_config' affects only behavior of the 272 | # config.*.yaml file it exists in. 273 | 274 | # Many DDEV commands can be extended to run tasks before or after the 275 | # DDEV command is executed, for example "post-start", "post-import-db", 276 | # "pre-composer", "post-composer" 277 | # See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more 278 | # information on the commands that can be extended and the tasks you can define 279 | # for them. Example: 280 | #hooks: 281 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.php_cs.cache 2 | /vendor 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | --------- 4 | 5 | 20.1.0 6 | ------ 7 | 8 | 1) [FEATURE] Add dbCodeFilter option to filter database (credits to `SimonVanacco `_ ) 9 | 2) [BUGFIX] SSL options in drop tables command (credits to `maikschneider `_ ) 10 | 3) [BUGFIX] Make strict boolean conditions for ``post_sql_in_markers`` and ``post_sql_in``. 11 | 4) [BUGFIX] Fix the logic for resolving PHP binary. If the binary specified by ``php_version`` or ``composer.json`` 12 | is not found, it now gracefully falls back to search for ``php`` binary and finally defaults to plain ``php``. 13 | 14 | 20.0.1 15 | ------ 16 | 17 | 1) [BUGFIX] Fix the way the ``local/bin/php`` is determined. 18 | 19 | 20.0.0 20 | ------ 21 | 22 | 1) [TASK][BREAKING] Use new version of ``sourcebroker/deployer-instance``. This version has fallback when no .env (or .env.local) 23 | file exists. It will take the system variables then. This should not break anything, but anyway a major version just to be sure. 24 | 25 | 19.0.0 26 | ------ 27 | 28 | 1) [BUGFIX] Fix wrong use of ``bin/php`` for local context. Deployer ``bin/php``, when not a explicitly set as string, 29 | is using ``which()`` function, that is using ``run()`` in background, which is trying to make connection to local ssh. 30 | This fix uses ``local/bin/php`` instead which will check locally: first ``php_version`` setting of host, then if not 31 | found it will check ``composer.json`` for the php version. If php version is found, then the binary will be searched first 32 | for X.Y (like 8.4) and then for XY (84). Finally if ``php_version`` or ``composer.json`` do not give answer about 33 | php version it will fallback just to search ``php`` binary. 34 | 35 | 2) [BUGFIX] Fix some edge cases for replacing the task names in nested outputs. 36 | 37 | 3) [BUGFIX] Remove using "stat" command in ``db:download`` and ``db:upload`` commands because not available on some systems. Use ``filesize()`` 38 | from php as command is run locally, use ``glob()`` instead of ``ls`` as for the same reason. Fix the files size calculation. 39 | 40 | 4) [FEATURE] Add options to disable task infos: ``db_download_info_enable``, ``db_upload_info_enable``, 41 | ``db_import_big_table_info_enable``, ``db_pull_from_local_storage_info_enable``. By default, they all are enabled. 42 | 43 | 5) [BUGFIX][BREAKING] Fix wrong name of option in db:import task. It was ``db_import_show_ignore_tables_output_max_line_length`` 44 | but should be ``db_import_big_table_output_max_line_length``. Rename option ``db_export_mysqldump_show_ignore_tables_out`` 45 | to ``db_export_mysqldump_show_ignore_tables_out_info_enable`` in db:export. Change default value of 46 | ``db_export_mysqldump_show_ignore_tables_out_max_line_length`` from 250 to 120. 47 | 48 | 6) [TASK] Add ``localhost('local')`` by default. 49 | 50 | 18.2.0 51 | ------ 52 | 53 | 1) [TASK] Extend the dependency to v5 of ``sourcebroker/deployer-loader``. 54 | 55 | 18.1.1 56 | ------ 57 | 1) [BUGFIX] Last line of ignored tables at db:export task was not shown. This change fix this and also make refactor for this part. 58 | 59 | 18.1.0 60 | ------ 61 | 1) [FEATURE] Allow to add custom options to ``--options=``. If option has prefix ``tx`` is not validated. 62 | For example ``--options=txMyOption:myValue``. 63 | 64 | 18.0.3 65 | ------ 66 | 67 | 1) [BUGFIX] The constant GLOB_BRACE is not defined on all systems, like fe Alpine Linux. After this fix GLOB_BRACE 68 | is no longer used. 69 | 2) [BUGFIX] Fix the condition for checking if release_path exists. It was failing when ``db:backup`` at localhost was 70 | invoked because ``release_path`` is not set then. Replace with usage of ``deploy_path`` and hardcoded ``release``. 71 | 72 | 18.0.2 73 | ------ 74 | 75 | 1) [BUGFIX] Fix wrong condition on ``db:backup``. After this fix tags "release" and "release_X" will be automatically added 76 | when task ``db:backup`` is run during deploy before ``deploy:symlink``. 77 | 78 | 18.0.1 79 | ------ 80 | 81 | 1) [BUGFIX] PHP 7.4 compatibility fixes. 82 | 83 | 18.0.0 84 | ------ 85 | 86 | 1) [TASK][BREAKING] Change the default charset for mysql/mysqldump operations from utf8 to utf8mb4. 87 | 2) [TASK] Add output formatting helper functions in ConsoleUtility. 88 | 3) [FEATURE] Add way to validate value of single ``--options`` with preg_match in ConsoleUtility->getOption. 89 | 4) [TASK][BREAKING] Add preg_match for dumpcode ``/^[a-zA-Z0-9_]+$/`` 90 | 5) [TASK] Add support for resolving home in deploy_path 91 | 6) [TASK][BREAKING] Remove autocreation of dumpcode on db:export. Since now you need to add your dumpcode 92 | with --options=dumpcode:mydumpcode 93 | 7) [TASK] Move db credentials to temporary file and use it with ``--defaults-file=``. Reason is edge case that ``MYSQL_PWD`` 94 | was overwritten by ``~/.my.cnf`` files. For example: https://github.com/ddev/ddev/pull/6851 95 | 8) [FEATURE] Add info about big tables on db:import. Useful to fast check if we import some not needed big tables. 96 | Default limit is 50MB. You can change it to your value in ``db_import_big_table_size_threshold``. 97 | 9) [FEATURE] Add info about ignored tables on mysqldump in db:export. You can configure max_line in 98 | ``db_export_mysqldump_show_ignore_tables_out_max_line_length``. 99 | 10) [TASK][BREAKING] Change bahaviour of ``db:download`` and ``db:upload``. Option ``--remove-source-files`` has been 100 | removed from rsync and downloaded, uploaded files are not longer automatically removed. Add short info about size 101 | of downloaded or uploaded file. 102 | 11) [TASK] Show ``db:dumpclean`` output only when verbosity higher than regular. 103 | 12) [TASK][BREAKING] Change behaviour of ``db:copy``, ``db:pull``, ``db:push``. No longer copy of imported database is 104 | kept in database storage. This is clean up before implementing a backup of replaced database. Add nice formatting 105 | of tasks with info about ignored tables, size od downloaded/uploaded database and too big tables. 106 | 13) [FEATURE] Add command ``db:import:last`` to import last downloaded database. Bring back keeping last imported databases 107 | and rotation of those. 108 | 14) [FEATURE] Add validation for name of options. If name is wrong task will stop. 109 | 15) [TASK][BREAKING] Move normalizeFolder functionality into 'db_storage_path_local' function as it was used only in 110 | this context. Remove FileUtility->normalizeFolder 111 | 16) [TASK][BREAKING] Do not throw error in db:decompress task if dump is already decompressed. 112 | 17) [TASK] Add new OptionUtility for managing "--options". Add new option "tags" that allow to set tags for dump filename. 113 | 18) [TASK] Refactor db:copy, db:pull, db:push. Store copies of dumps that will be imported and store dump of local database 114 | before import. This will allow to recover when database overwritten by accident. Add tags to dumps. 115 | 19) [TASK][BREAKING] Remove db:import:last task in favour of option for db:pull. 116 | 20) [TASK][BREAKING] Remove dependency to symfony/dotenv. 117 | 118 | 17.0.0 119 | ~~~~~~ 120 | 121 | 1) [FEATURE] Add support for SSL connection to database. 122 | 2) [FEATURE] Add missing variable for mysql options for post sql import ``db_import_mysql_options_post_sql_in``. 123 | 124 | 16.1.0 125 | ~~~~~~ 126 | 127 | 1) [FEATURE] Add possibility to overwrite ``db_databases`` by setting ``db_databases_global`` and ``db_databases`` 128 | (on host level). See more on UPGRADE file. 129 | 130 | 16.0.1 131 | ~~~~~~ 132 | 133 | 1) [BUGFIX] Fix connectionOptionsString() is already escapeshellarg'ed on RsyncUtility->getSshOptions. 134 | 135 | 16.0.0 136 | ~~~~~~ 137 | 138 | 1) [TASK][BREAKING] Bump dependency to ``sourcebroker/deployer-instance``. 139 | 2) [TASK] Code cleanup / update readme. 140 | 3) [BUGFIX] Fix wrong function used to output text. 141 | 142 | 143 | 15.0.0 144 | ~~~~~~ 145 | 146 | 1) [TASK][BREAKING] Refactor to Deployer 7. 147 | 2) [TASK] Extend dependency to internal packages to dev-master. 148 | 149 | 14.0.0 150 | ~~~~~~ 151 | 152 | 1) [TASK][BREAKING] Update dependency to ``sourcebroker/deployer-loader`` which introduce load folder/files 153 | alphabetically. 154 | 155 | 13.0.2 156 | ~~~~~~ 157 | 158 | 1) [BUGFIX] Use port-parameter in mysqli_connect (tnx to mavolkmer) 159 | 2) [TASK] Drop styleci. 160 | 3) [TASK] Drop date from licence. 161 | 162 | 13.0.1 163 | ~~~~~~ 164 | 165 | 1) [BUGFIX] Add dependency to sourcebroker/deployer-instance (fix compatibility with symfony/dotenv 5.0) 166 | 167 | 13.0.0 168 | ~~~~~~ 169 | 170 | 1) [TASK] Add ddev config. 171 | 2) [TASK][BREAKING] Fix compatibility with symfony/dotenv 5.0 which do not use getenv() by default. 172 | 173 | 12.2.1 174 | ~~~~~~ 175 | 176 | 1) [BUGFIX] Fix changelog typo. 177 | 178 | 12.2.0 179 | ~~~~~~ 180 | 181 | 1) [TASK] Increase `symfony/dotenv` version. 182 | 183 | 12.1.0 184 | ~~~~~~ 185 | 186 | 1) [FEATURE] Use loadEnv function from Symfony\Dotenv if possible. 187 | 2) [BUGFIX] Documentation bugfixes. 188 | 189 | 12.0.0 190 | ~~~~~~ 191 | 192 | 1) [TASK][BREAKING] Add new default option for mysqldump '--no-tablespaces' . https://dba.stackexchange.com/questions/271981/access-denied-you-need-at-least-one-of-the-process-privileges-for-this-ope 193 | 194 | 11.0.2 195 | ~~~~~~ 196 | 197 | 1) [BUGFIX] Fix for normalize file regexp. 198 | 199 | 11.0.1 200 | ~~~~~~ 201 | 202 | 1) [BUGFIX] Force dumpcode to be only a-z, A-Z, 0-9, _. 203 | 204 | 11.0.0 205 | ~~~~~~ 206 | 207 | 1) [TASK][BREAKING] Add dependency to deployer-extended-loader. 208 | 209 | 10.0.1 210 | ~~~~~~ 211 | 212 | 1) [BUGFIX] Force dumpcode to be only a-z, A-Z, 0-9. 213 | 2) [BUGFIX] Fix for normalize file regexp. 214 | 215 | 10.0.0 216 | ~~~~~~ 217 | 218 | 1) [FEATURE] Add db:push command. 219 | 2) [FEATURE] Add FileUtility->locateLocalBinaryPath. 220 | 3) [TASK][BREAKING] Remove not needed dependency to deployer-extended-loader. 221 | 4) [TASK][BREAKING] Cleanup variables naming. 222 | 5) [TASK] Protect copying/pushing/pulling database to top level instance. 223 | 6) [TASK] Disable default command for db_process_commands. 224 | 225 | 9.0.0 226 | ~~~~~~ 227 | 228 | 1) [TASK][BREAKING] Compatibility with Deployer 6.4+ 229 | 2) [TASK][BREAKING] Refactor options to single option --options=key:value,key:value 230 | 3) [TASK] Use $host->getSshArguments()->getCliArguments() for creating rsync ssh parameters. 231 | 232 | 8.0.0 233 | ~~~~~ 234 | 235 | 1) [FEATURE] Add option exportTaskAddIgnoreTablesToStructureDump to allow to add ignore tables when exporting structure. 236 | 2) [FEATURE] Add option importTaskDoNotDropAllTablesBeforeImport to prevent dropping all tables before import. 237 | 3) [TASK] Add vendor and composer.lock to .gitignore. 238 | 4) [FEATURE][BREAKING] Implement sourcebroker/deployer-instance for instance management. 239 | 5) [BUGFIX] Remove colon from file names because if Windows compatibility. 240 | 6) [TASK] Replace RuntimeException with GracefulShutdownException. 241 | 7) [TASK] Increase version of sourcebroker/deployer-instance. 242 | 8) [TASK] Replace hardcoded instance name with var. 243 | 9) [TASK] Normalize use of dots at the end of task description. 244 | 245 | 7.0.2 246 | ~~~~~ 247 | 248 | 1) [BUGFIX] Replace ":" with "=" because Windows compatibility - date separated by ":". 249 | 250 | 7.0.1 251 | ~~~~~ 252 | 253 | 1) [BUGFIX] Replace ":" with "=" because Windows compatibility. 254 | 255 | 7.0.0 256 | ~~~~~ 257 | 258 | 1) [TASK][BREAKING] Possible breaking change for those using global ``dep`` instead of that one in ``./vendor/bin/dep`` as 259 | ``local/bin/deployer`` is set now to ``./vendor/bin/dep``. 260 | 261 | 6.2.1 262 | ~~~~~ 263 | 264 | 1) [BUGFIX] If publicUrl is with port then this port should be also used for post_sql_in_markers. 265 | 266 | 6.2.0 267 | ~~~~~ 268 | 269 | 1) [FEATURE] Add confirmation for command db:copy (tnx to Michał Jankiewicz) 270 | 2) [FEATURE] Add default option to confirmation for command db:copy so it can be used also with -q option for 271 | unattended. 272 | 273 | 6.1.2 274 | ~~~~~ 275 | 276 | 1) [BUGFIX] Fix $dbDumpCleanKeep calculation in db:dumpclean. 277 | 278 | 6.1.1 279 | ~~~~~ 280 | 281 | 1) [BUGFIX] Move count() out of for so its not calculated each time. 282 | 283 | 6.1.0 284 | ~~~~~ 285 | 286 | 1) [FEATURE] Add ``db:dumpclean`` task. Add ``db:dumpclean`` as last task to ``db:backup`` and ``db:pull``. Add docs. 287 | 288 | 6.0.0 289 | ~~~~~ 290 | 291 | 1) [BREAKING] Remove ``db_deployer_version`` config var as its not needed for deployer/distribution based version now. 292 | 2) [DOCS] Change to number ordered list on CHANGELOG.rst. 293 | 3) [TASK] Rename ``type`` to ``absolutePath`` in $mysqlDumpArgs of db:export so it have more meaning. 294 | 4) [TASK] Improve tasks descriptions. 295 | 5) [FEATURE] Add db:compress and db:decompress tasks and extend docs. 296 | 6) [TASK] Cleanup for db:upload, db:download tasks. 297 | 7) [FEATURE] Compress local dumps after importing them with ``db:pull [instance]``. 298 | 8) [FEATURE] Add db:rmdump task and documentation. 299 | 9) [FEATURE] Add db:rmdump task at the end of ``db:copy [source] [target]`` task. 300 | 10) [FEATURE] Add db:backup task. 301 | 302 | 303 | 5.0.4 304 | ~~~~~ 305 | 306 | 1) [BUGFIX] Fix styles ci. 307 | 308 | 309 | 5.0.3 310 | ~~~~~ 311 | 312 | 1) [BUGFIX] Do not show error on database pull if ``public_urls`` are not set. 313 | 314 | 5.0.2 315 | ~~~~~ 316 | 317 | 1) [BUGFIX] Remove not needeed exeption throws as the truncate_tables value can be 318 | not set or return empty value from regexp. 319 | 320 | 5.0.1 321 | ~~~~~ 322 | 323 | 1) [BUGFIX] Add missing dependency to sourcebroker/deployer-loader 324 | 325 | 5.0.0 326 | ~~~~~ 327 | 328 | 1) [TASK] Add dependency to sourcebroker/deployer-loader 329 | 2) [TASK][!!!BREAKING] Remove SourceBroker\DeployerExtendedDatabase\Loader.php in favour of using sourcebroker/deployer-loader 330 | 3) [TASK][!!!BREAKING] Remove SourceBroker\DeployerExtendedDatabase\Utility\FileUtility->requireFilesFromDirectoryReqursively 331 | because it was used only in SourceBroker\DeployerExtendedDatabase\Loader.php 332 | 333 | 4.0.5 334 | ~~~~~ 335 | 336 | 1) [BUGFIX] Fix wrongly prepared marker domainsSeparatedByComma when more than one domain 337 | 338 | 4.0.4 339 | ~~~~~ 340 | 341 | 1) [TASK] Make dependency to deployer/deployer-dist. 342 | 343 | 4.0.3 344 | ~~~~~ 345 | 346 | 1) [TASK] Make ``bin/deployer`` use of vendor/bin/dep from deployer-dist. 347 | 348 | 4.0.2 349 | ~~~~~ 350 | 351 | 1) [BUGFIX] Fix rebulding symlink to deployer.phar 352 | 353 | 4.0.1 354 | ~~~~~ 355 | 356 | 1) [BUGFIX] Fix wrong path set for db:copy 357 | 358 | 4.0.0 359 | ~~~~~ 360 | 361 | 1) [TASK] db:export refactor: add possibility to call command on remote instance, add ``db_export_mysqldump_options_structure`` and ``db_export_mysqldump_options_data`` env. 362 | 2) [BUGFIX] Fix wrong changlog address in main docs. 363 | 3) [TASK] db:truncate refactor add escapeshellargs 364 | 4) [TASK] Escapeshellargs for all commands 365 | 5) [TASK][BREAKING] Change static utilities method calls to regular objects method call. 366 | 6) [TASK] Cleanup ``db:download`` and ``db:upload`` tasks with RsyncUtility 367 | 7) [TASK][BREAKING] Rename var ``bin/mysql`` to ``local/bin/mysql`` 368 | 8) [TASK] Refactor db:import 369 | 9) [TASK] db:import refactor add possibility to call command on remote instance 370 | 10) [TASK] Enable duplication check for scrutinizer. 371 | 11) [TASK] Pass verbosity to commands run locally in db:pull task. 372 | 12) [TASK] Move mysql options from db:import task to variables. 373 | 13) [TASK] Pass verbosity to commands run locally with use of ConsoleUtility. 374 | 14) [TASK] Implement optionRequired() in ConsoleUtility. 375 | 376 | 3.0.0 377 | ~~~~~ 378 | 379 | 1) Set ``default_stage`` as callable. This way ``default_stage`` can be now overwritten in higher level packages. 380 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © SourceBroker Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | deployer-extended-database 2 | ========================== 3 | 4 | .. image:: https://poser.pugx.org/sourcebroker/deployer-extended-database/v/stable 5 | :target: https://packagist.org/packages/sourcebroker/deployer-extended-database 6 | 7 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat 8 | :target: https://packagist.org/packages/sourcebroker/deployer-extended-database 9 | 10 | | 11 | 12 | .. contents:: :local: 13 | 14 | What does it do? 15 | ---------------- 16 | 17 | The package provides additional tasks for deployer (deployer.org) for synchronizing databases between instances. 18 | Most useful are tasks: 19 | 20 | 1. task "`db:pull`_ [source-instance]" task which allows you to pull database from remote instance to local instance, 21 | 22 | 2. task "`db:push`_ [target-instance]" task which allows you to push database from local to remote instance, 23 | 24 | 2. task "`db:copy`_ [source-instance] --options=target:[target-instance]" which allows to copy database between remote instances. 25 | 26 | Installation 27 | ------------ 28 | 29 | 1) Install package with composer: 30 | :: 31 | 32 | composer require sourcebroker/deployer-extended-database 33 | 34 | 35 | 2) Put following lines in your deploy.php: 36 | :: 37 | 38 | require_once(__DIR__ . '/vendor/autoload.php'); 39 | new \SourceBroker\DeployerLoader\Load([ 40 | ['path' => 'vendor/sourcebroker/deployer-instance/deployer'], 41 | ['path' => 'vendor/sourcebroker/deployer-extended-database/deployer'], 42 | ]); 43 | 44 | 45 | 4) Create ".env" file in your project root (where you store deploy.php file). The .env file should be out of 46 | git because you need to store here information about instance name. Additionally put there info about database 47 | you want to synchronise. You can move the info about database data to other file later but for the tests its better 48 | to put it in .env file. Remember to protect .env file from downloading with https request (if the root folder 49 | is readable from WWW server level). 50 | :: 51 | 52 | INSTANCE="local" 53 | 54 | DATABASE_HOST="127.0.0.1" 55 | DATABASE_NAME="database_name" 56 | DATABASE_USER="database_user" 57 | DATABASE_PASSWORD="password" 58 | 59 | The INSTANCE must correspond to ``host()`` name. You need to put the .env file with proper INSTANCE name and 60 | database access data on on each of you instances. 61 | 62 | 5) Define "local" localhost and set the "db_databases" for it. Use following code: 63 | :: 64 | 65 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 66 | 67 | which will read database data from .env file. 68 | :: 69 | 70 | localhost('local') 71 | ->set('deploy_path', getcwd()) 72 | ->set('bin/php', 'php') 73 | //->set('local/bin/php', 'php') // this is auto detected but if you have problems with it you can set it manually. This path must be the same as ``bin/php``. 74 | ->set('db_databases', [ 75 | 'database_default' => [ 76 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 77 | ] 78 | ]) 79 | 80 | The ``local/bin/php`` is used to run deployer binary at remote. The value for ``local/bin/php`` is 81 | auto detected, based on ``composer.json``, but if you have non standard php binary location you can set it manually. 82 | It should have the same value as ``bin/php`. If you want ``bin/php`` to be also auto detected then you need to 83 | install https://github.com/sourcebroker/deployer-extended and add ``'path' => 'vendor/sourcebroker/deployer-extended/includes/settings/bin_php.php'`` 84 | to loader from point 2. Then for standard hosting config you do not need to set either `bin/php` or `local/bin/php`. 85 | 86 | 6) Add "db_databases" var for all other hosts. For example for live host it can be: 87 | :: 88 | 89 | host('live') 90 | ->setHostname('my-server.example.com') 91 | ->setRemoteUser('deploy') 92 | ->set('bin/php', '/usr/bin/php82') 93 | //->set('local/bin/php', '/usr/bin/php82') // this is auto detected but if you have problems with it you can set it manually. This path must be the same as ``bin/php``. 94 | ->set('deploy_path', '/var/www/myapplication/') 95 | ->set('db_databases', [ 96 | 'database_default' => [ 97 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 98 | ] 99 | ]) 100 | 101 | 7) Make sure all instances have the same ``/vendors`` folder with ``deployer-extended-database`` and the same ``deploy.php`` file. 102 | Most problems are because of differences in ``deploy.php`` file between instances. 103 | 104 | 8) Run ``dep db:pull live`` to test if all works. 105 | 106 | Options 107 | ------- 108 | 109 | - | **db_databases** 110 | | *default value:* null 111 | | 112 | | Databases to be synchronized. You can define more than one database to be synchronized. See `db_databases`_ for 113 | options available inside db_databases. Look for `Examples`_ for better understanding of structure. 114 | 115 | | 116 | - | **db_storage_path_relative** 117 | | *default value:* .dep/database/dumps 118 | | 119 | | Path relative to "deploy_path" where you want to store database dumps produced during database synchro commands. 120 | 121 | | 122 | - | **db_export_mysqldump_options_structure** 123 | | *default value:* --no-data=true --default-character-set=utf8mb4 --no-tablespaces 124 | | 125 | | Options `mysqldump` used for exporting the database structure. 126 | 127 | | 128 | - | **db_export_mysqldump_options_data** 129 | | *default value:* --opt --skip-lock-tables --single-transaction --no-create-db --default-character-set=utf8mb4 --no-tablespaces 130 | | 131 | | Options `mysqldump` used for exporting the database data. 132 | 133 | | 134 | - | **db_import_mysql_options_structure** 135 | | *default value:* --default-character-set=utf8mb4 136 | | 137 | | Options `mysql` used for importing the database structure. 138 | 139 | | 140 | - | **db_import_mysql_options_data** 141 | | *default value:* --default-character-set=utf8mb4 142 | | 143 | | Options `mysql` used for importing the database data. 144 | 145 | | 146 | - | **db_import_mysql_options_post_sql_in** 147 | | *default value:* --default-character-set=utf8mb4 148 | | 149 | | Options `mysql` used for importing the additional SQL from ``post_sql_in``. 150 | 151 | 152 | NOTE: watch that ``utf8mb4`` is forced for all mysql/mysqldump operations. 153 | 154 | 155 | .. _db\_databases: 156 | 157 | Options for "db_databases" 158 | -------------------------- 159 | 160 | "db_databases" is an array of "database configurations" and "database configuration" is array of configuration parts. 161 | Configuration part can be array or string. If its string then its treated as absolute path to file which should 162 | return array of configuration. Each or array configuration parts is merged. Look for `Examples`_ for better 163 | understanding. 164 | 165 | - | **host** 166 | | *default value:* null 167 | | 168 | | Database host. 169 | 170 | | 171 | - | **user** 172 | | *default value:* null 173 | | 174 | | Database user. 175 | 176 | | 177 | - | **password** 178 | | *default value:* null 179 | | 180 | | Database user password. 181 | 182 | | 183 | - | **dbname** 184 | | *default value:* null 185 | | 186 | | Database name. 187 | 188 | | 189 | - | **truncate_tables** 190 | | *default value:* null 191 | | 192 | | Array of tables names that will be truncated with task `db:truncate`_. Usually it should be some caching tables that 193 | will be truncated while deployment. The value is put between ^ and $ and treated as preg_match. For example 194 | you can write "cf\_.*" to truncate all tables that starts with "cf\_". The final preg_match checked is /^cf\_.*$/i 195 | 196 | | 197 | - | **ignore_tables_out** 198 | | *default value:* null 199 | | 200 | | Array of tables names that will be ignored while pulling database from target instance with task `db:pull`_ 201 | The value is put between ^ and $ and treated as preg_match. For example you can write "cf\_.*" to truncate all 202 | tables that starts with "cf\_". The final preg_match checked is /^cf\_.*$/i 203 | 204 | | 205 | - | **post_sql_in** 206 | | *default value:* null 207 | | 208 | | SQL that will be executed after importing database on local instance. 209 | 210 | | 211 | - | **post_sql_in_markers** 212 | | *default value:* null 213 | | 214 | | SQL that will be executed after importing database on local instance. The diffrence over "post_sql_in" 215 | is that you can use some predefined markers. For now only marker is {{domainsSeparatedByComma}} which consist of all 216 | domains defined in ``->set('public_urls', ['https://live.example.com']);`` and separated by comma. Having such 217 | marker allows to change active domain in database after import to other instance as some frameworks keeps domain 218 | names in database. 219 | 220 | 221 | Examples 222 | -------- 223 | 224 | Below examples should illustrate how you should build your database configuration. 225 | 226 | Config with one database and database data read from .env file 227 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 228 | 229 | deploy.php file: 230 | :: 231 | 232 | set('db_default', [ 233 | 'ignore_tables_out' => [ 234 | 'caching_.*' 235 | ] 236 | ]); 237 | 238 | host('live') 239 | ->setHostname('my-server.example.com') 240 | ->setRemoteUser('deploy') 241 | ->set('bin/php', '/usr/bin/php82') 242 | ->set('deploy_path', '/var/www/myapplication') 243 | ->set('db_databases', 244 | [ 245 | 'database_foo' => [ 246 | get('db_default'), 247 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 248 | ], 249 | ] 250 | ); 251 | 252 | localhost('local') 253 | ->set('deploy_path', getcwd()) 254 | ->set('bin/php', 'php') 255 | ->set('db_databases', 256 | [ 257 | 'database_foo' => [ 258 | get('db_default'), 259 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 260 | ], 261 | ] 262 | ); 263 | 264 | Mind that because the db_* settings for all hosts will be the same then you can make the 'db_databases' setting global 265 | and put it out of host configurations. Look for below example where we simplified the config. 266 | 267 | deploy.php file: 268 | :: 269 | 270 | set('db_databases', 271 | [ 272 | 'database_foo' => [ 273 | 'ignore_tables_out' => [ 274 | 'caching_.*' 275 | ] 276 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig() 277 | ], 278 | ] 279 | ); 280 | 281 | host('live') 282 | ->setHostname('my-server.example.com') 283 | ->setRemoteUser('deploy') 284 | ->set('bin/php', '/usr/bin/php82') 285 | ->set('deploy_path', '/var/www/myapplication/'); 286 | 287 | localhost('local') 288 | ->set('bin/php', 'php') 289 | ->set('deploy_path', getcwd()); 290 | 291 | 292 | The .env file should look then like: 293 | :: 294 | 295 | INSTANCE="[instance name]" 296 | 297 | DATABASE_HOST="127.0.0.1" 298 | DATABASE_NAME="database_name" 299 | DATABASE_USER="database_user" 300 | DATABASE_PASSWORD="password" 301 | 302 | Config with two databases and database data read from .env file 303 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 304 | 305 | deploy.php file: 306 | :: 307 | 308 | set('db_databases', 309 | [ 310 | 'database_application1' => [ 311 | 'ignore_tables_out' => [ 312 | 'caching_.*' 313 | ] 314 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig('APP1_') 315 | ], 316 | 'database_application2' => [ 317 | 'ignore_tables_out' => [ 318 | 'cf_.*' 319 | ] 320 | (new \SourceBroker\DeployerExtendedDatabase\Driver\EnvDriver())->getDatabaseConfig('APP2_') 321 | ], 322 | ] 323 | ); 324 | 325 | host('live') 326 | ->setHostname('my-server.example.com') 327 | ->setRemoteUser('deploy') 328 | ->set('bin/php', '/usr/bin/php82') 329 | ->set('deploy_path', '/var/www/myapplication/'); 330 | 331 | localhost('local') 332 | ->set('bin/php', 'php') 333 | ->set('deploy_path', getcwd()); 334 | 335 | The .env file should look then like: 336 | :: 337 | 338 | INSTANCE="[instance name]" 339 | 340 | APP1_DATABASE_HOST="127.0.0.1" 341 | APP1_DATABASE_NAME="database_name" 342 | APP1_DATABASE_USER="database_user" 343 | APP1_DATABASE_PASSWORD="password" 344 | 345 | APP2_DATABASE_HOST="127.0.0.1" 346 | APP2_DATABASE_NAME="database_name" 347 | APP2_DATABASE_USER="database_user" 348 | APP2_DATABASE_PASSWORD="password" 349 | 350 | Config with one database and database config read from from different sources 351 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 352 | 353 | In example we will use: 354 | 355 | 1) array, 356 | :: 357 | 358 | 'ignore_tables_out' => [ 359 | 'caching_*' 360 | ] 361 | 362 | 2) get() which returns array with database options, 363 | ``get('db_default')`` 364 | 365 | 3) direct file include which returns array with database options 366 | ``__DIR__ . '/.dep/database/config/additional_db_config.php`` 367 | 368 | 4) class/method which returns array with database options 369 | ``(new \YourVendor\YourPackage\Driver\MyDriver())->getDatabaseConfig()`` 370 | 371 | 5) closure which returns array with database options 372 | ``function() { return (new \YourVendor\YourPackage\Driver\MyDriver())->getDatabaseConfig()`` } 373 | 374 | Each of this arrays are merged to build final configuration for database synchro. 375 | 376 | deploy.php file: 377 | :: 378 | 379 | set('db_default', [ 380 | 'post_sql_in' => 'UPDATE sys_domains SET hidden=1;' 381 | ]); 382 | 383 | set('db_databases', 384 | [ 385 | 'database_foo' => [ 386 | [ 387 | 'ignore_tables_out' => [ 388 | 'caching_.*' 389 | ] 390 | ], 391 | get('db_default'), 392 | __DIR__ . '/databases/config/additional_db_config.php', 393 | (new \YourVendor\YourPackage\Driver\MyDriver())->getDatabaseConfig(), 394 | function() { 395 | return (new \YourVendor\YourPackage\Driver\MyDriver())->getDatabaseConfig(); 396 | } 397 | ], 398 | ] 399 | ); 400 | 401 | host('live') 402 | ->setHostname('my-server.example.com') 403 | ->setRemoteUser('deploy') 404 | ->set('bin/php', '/usr/bin/php82') 405 | ->set('deploy_path', '/var/www/myapplication/'); 406 | 407 | localhost('local') 408 | ->set('bin/php', 'php') 409 | ->set('deploy_path', getcwd()); 410 | 411 | 412 | Config with one database and database config read from "my framework" file 413 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 414 | 415 | Its advisable that you create you own special method that will return you framework database data. In below example 416 | its call to ``\YourVendor\YourPackage\Driver\MyDriver()``. This way you do not need to repeat the data of database 417 | in .env file. In that case .env file should hold only INSTANCE. 418 | :: 419 | 420 | set('db_databases', 421 | [ 422 | 'database_default' => [ 423 | (new \YourVendor\YourPackage\Driver\MyDriver())->getDatabaseConfig() 424 | ], 425 | ] 426 | ); 427 | 428 | 429 | Config of truncate_tables, ignore_tables_out, post_sql_in_markers 430 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 431 | 432 | Real life example for CMS TYPO3: 433 | :: 434 | 435 | set('db_default', [ 436 | 'truncate_tables' => [ 437 | 'cf_.*' 438 | ], 439 | 'ignore_tables_out' => [ 440 | 'cf_.*', 441 | 'cache_.*', 442 | 'be_sessions', 443 | 'fe_sessions', 444 | 'sys_file_processedfile', 445 | 'tx_devlog', 446 | ], 447 | ]); 448 | 449 | 450 | Tasks 451 | ----- 452 | 453 | db:backup 454 | +++++++++ 455 | 456 | Backup database. In background, on target instance, three tasks are executed `db:export`_, `db:compress`_ and `db:dumpclean`_. 457 | Results are stored in ``{{deploy_path}}/.dep/databases/dumps/``. 458 | 459 | If ``release`` folder will be detected on ``deploy_path`` (means we are in process of deploy) then it adds two tags to dump name: 460 | ``release`` and ``release_[number]`` like in example below: 461 | ``2025-03-01_23-36-25#server=local#dbcode=database_default#dumpcode=e8cd33191dffe1642d3e9fb6bf99090f#tags=release+release_91#type=structure.sql.gz`` 462 | 463 | You can add you own tags with ``--options=tags:tag1+tag2``. Example: 464 | ``2025-03-01_23-36-25#server=local#dbcode=database_default#dumpcode=e8cd33191dffe1642d3e9fb6bf99090f#tags=tag1+tag2#type=structure.sql.gz`` 465 | 466 | **Example** 467 | :: 468 | 469 | dep db:backup local 470 | dep db:backup live 471 | dep db:backup live --options=dumpcode:mycode 472 | dep db:backup live --options=dumpcode:mycode,tags:tag1+tag2 473 | 474 | db:compress 475 | +++++++++++ 476 | 477 | Compress dumps with given dumpcode stored in folder ``{{deploy_path}}/.dep/databases/dumps/``"`` on target instance. 478 | There is required option ``--options=dumpcode:[value]`` to be passed. 479 | 480 | Look for config vars ``db_compress_suffix``, ``db_compress_command``, ``db_uncompress_command`` for possible ways to overwrite 481 | standard gzip compression with your own. 482 | 483 | **Example** 484 | :: 485 | 486 | dep db:compress live --options=dumpcode:0772a8d396911951022db5ea385535f6 487 | 488 | 489 | db:copy 490 | +++++++ 491 | 492 | This command allows you to copy database between instances. 493 | :: 494 | 495 | dep db:copy [source-instance] --options=target:[target-instance] 496 | 497 | In the background it runs several other tasks to accomplish this. Lets assume we want to copy database from live 498 | to dev instance. We will run following command on you local instance: 499 | 500 | :: 501 | 502 | dep db:copy live --options=target:dev 503 | 504 | Here are the tasks that will be run in background: 505 | 506 | In below description: 507 | * source instance = live 508 | * target instance = dev 509 | * local instance = local 510 | 511 | 1) First it runs ``dep db:export live --options=dumpcode:123456`` task on source instance. The dumps from export task are stored 512 | in folder ``{{deploy_path}}/.dep/databases/dumps/`` on target instance. 513 | 514 | 2) Then it runs ``db:download live --options=dumpcode:123456`` on local instance to download dump files from live instance from 515 | folder ``{{deploy_path}}/.dep/databases/dumps/`` to local instance to folder ``{{deploy_path}}/.dep/databases/dumps/``. The dump at 516 | source instance is deleted at this point because already downloaded. 517 | 518 | 3) Then it runs ``db:process local --options=dumpcode:123456`` on local instance to make some operations directly on SQL dumps files. 519 | 520 | 4) Then it runs ``db:upload dev --options=dumpcode:123456`` on local instance. This task takes dump files with ``code:123456`` 521 | and send it to dev instance and store it in folder ``{{deploy_path}}/.dep/databases/dumps/``. The dump at 522 | local instance is deleted at this point because already uploaded to target instance. 523 | 524 | 5) Then it make backup of target database before importing the dump. 525 | 526 | 6) Finally it runs ``db:import dev --options=dumpcode:123456`` on target instance. This task reads dumps with ``code:123456`` from folder 527 | ``{{deploy_path}}/.dep/databases/dumps/`` on dev instance and import it to database. 528 | 529 | 7) At the very end it runs `db:dumpclean`_ to clean up old dumps on target instance. 530 | 531 | Copy to instance defined in ``instance_live_name`` (default ``live``) is special case. 532 | If you copy to highest instance then by default you will be asked twice if you really want to. 533 | You can disable asking by setting ``db_allow_copy_live_force`` to ``true``. 534 | You can also forbid copy to live instance by setting ``db_allow_copy_live`` to ``false``. 535 | 536 | db:decompress 537 | +++++++++++++ 538 | 539 | Decompress dumps with given dumpcode stored in folder ``{{deploy_path}}/.dep/databases/dumps/`` on target instance. 540 | There is required option ``--options=dumpcode:[value]`` to be passed. 541 | 542 | Look for config vars ``db_compress_suffix``, ``db_compress_command``, ``db_uncompress_command`` for possible ways to overwrite 543 | standard gzip compression with your own. 544 | 545 | **Example** 546 | :: 547 | 548 | dep db:decompress live --options=dumpcode:0772a8d396911951022db5ea385535f6 549 | 550 | db:download 551 | +++++++++++ 552 | 553 | Download database dumps with selected dumpcode from folder ``{{deploy_path}}/.dep/databases/dumps/`` on target instance 554 | and store it in folder ``{{deploy_path}}/.dep/databases/dumps/`` on local instance. 555 | There is required option ``--options=dumpcode:[value]`` to be passed. 556 | 557 | **Example** 558 | :: 559 | 560 | dep db:download live --options=dumpcode:0772a8d396911951022db5ea385535f6 561 | 562 | db:dumpclean 563 | ++++++++++++ 564 | 565 | Clean database dump storage on target instance. By default it removes all dumps except last five but you can set your 566 | values and also change the values depending on instance. 567 | 568 | **Example** 569 | :: 570 | 571 | set('db_dumpclean_keep', 10); // keep last 10 dumps for all instances 572 | 573 | set('db_dumpclean_keep', [ 574 | 'live' => 10 // keep last 10 dumps for live instance dumps 575 | 'dev' => 5 // keep last 5 dumps for dev instance dumps 576 | '*' => 2 // keep last 5 dumps for all other instances dumps 577 | ]); 578 | 579 | dep db:dumpclean live 580 | 581 | db:export 582 | +++++++++ 583 | 584 | Dump database to folder on local instance located by default in ``{{deploy_path}}/.dep/databases/dumps/``. 585 | Dumps will be stored in two separate files. One with tables structure. The second with data only. 586 | There is required option ``--options=dumpcode:[value]`` to be passed. 587 | 588 | **Example** 589 | 590 | Example task call: 591 | :: 592 | 593 | dep db:export live --options=dumpcode:362d7ca0ff065f489c9b79d0a73720f5 594 | 595 | Example output files located in folder {{deploy_path}}/.dep/databases/dumps/: 596 | :: 597 | 598 | 2025-02-26_14:56:08#server=live#dbcode=database_default#type=data#dumpcode=362d7ca0ff065f489c9b79d0a73720f5.sql 599 | 2025-02-26_14:56:08#server=live#dbcode=database_default#type=structure#dumpcode=362d7ca0ff065f489c9b79d0a73720f5.sql 600 | 601 | Example, if you want to export only some databases : 602 | :: 603 | 604 | dep db:export live --options=dbCodeFilter:db1+db2 605 | 606 | db:import 607 | +++++++++ 608 | 609 | Import database dump files to target database(s). Files are taken from folder ``{{deploy_path}}/.dep/databases/dumps/`` 610 | on target instance. There is required option ``--options=dumpcode:[value]`` to be passed. 611 | 612 | **Example** 613 | :: 614 | 615 | dep db:import dev --options=dumpcode:0772a8d396911951022db5ea385535f66 616 | 617 | Example, if you want to import only some databases : 618 | :: 619 | 620 | dep db:import live --options=dbCodeFilter:db1+db2 621 | 622 | 623 | db:process 624 | ++++++++++ 625 | 626 | This command will run some defined commands on pure sql file as its sometimes needed to remove or replace some strings 627 | directly on sql file before importing. There is required option ``--options=dumpcode:[value]`` to be passed. 628 | 629 | **Example** 630 | :: 631 | 632 | dep db:process local --options=dumpcode:0772a8d396911951022db5ea385535f66 633 | 634 | db:pull 635 | +++++++ 636 | 637 | This command allows you to pull database from target instance to local instance. 638 | In the background it runs several other tasks to accomplish this. 639 | 640 | Pull to instance defined in ``instance_live_name`` (default ``live``) is special case. 641 | If you pull to highest instance then by default you will be asked twice if you really want to. 642 | You can disable asking by setting ``db_allow_pull_live_force`` to ``true``. 643 | You can also forbid pull to live instance by setting ``db_allow_pull_live`` to ``false``. 644 | 645 | When option ``--options=fromLocalStorage`` is set the it does not export from remote host but use local files from 646 | ``{{deploy_path}}/.dep/databases/dumps/`` folder. Useful to repeat import of database (for example to test upgrade process) 647 | without getting it again and again from remote host. 648 | 649 | **Example** 650 | :: 651 | 652 | # export from live and import on current host 653 | dep db:pull live 654 | 655 | # import from database storage of current host 656 | dep db:pull live --options=fromLocalStorage 657 | 658 | # export and import only some databases 659 | dep db:pull live --options=dbCodeFilter:db1+db2 660 | 661 | db:push 662 | +++++++ 663 | 664 | This command allows you to push database from local instance to remote instance. 665 | In the background it runs several other tasks to accomplish this. 666 | 667 | Here is the list of tasks that will be done when you execute "db:push": 668 | 669 | 1) First it runs `db:export`_ task on local instance. 670 | 2) Then it runs `db:upload`_ on local instance with remote as argument. 671 | 3) Then it runs `db:process`_ on remote instance. 672 | 4) Then it runs `db:backup`_ on remote instance to make backup before import. 673 | 5) Then it runs `db:import`_ on remote instance. 674 | 675 | Push to instance defined in ``instance_live_name`` (default ``live``) is special case. 676 | If you push to highest instance then by default you will be asked twice if you really want to. 677 | You can disable asking by setting ``db_allow_push_live_force`` to ``true``. 678 | You can also forbid push to live instance by setting ``db_allow_push_live`` to ``false``. 679 | 680 | **Example** 681 | :: 682 | 683 | dep db:push live 684 | 685 | # Only push some databases 686 | dep db:push live --options=dbCodeFilter:db1+db2 687 | 688 | db:rmdump 689 | +++++++++ 690 | 691 | This command will remove all dumps with given dumpcode (compressed and uncompressed). 692 | There is required option ``--options=dumpcode:[value]`` to be passed. 693 | 694 | **Example** 695 | :: 696 | 697 | dep db:rmdump live --options=dumpcode:0772a8d396911951022db5ea385535f66 698 | 699 | db:truncate 700 | +++++++++++ 701 | 702 | This command allows you to truncate database tables defined in database config var "truncate_tables". 703 | No dumpcode is needed because it operates directly on database. 704 | 705 | **Example** 706 | Truncate local instance databases tables. 707 | :: 708 | 709 | dep db:truncate 710 | 711 | Truncate live instance databases tables. 712 | :: 713 | 714 | dep db:truncate live 715 | 716 | db:upload 717 | +++++++++ 718 | 719 | Upload database dumps with selected dumpcode from folder ``{{deploy_path}}/.dep/databases/dumps/`` on local instance and 720 | store it in folder ``{{deploy_path}}/.dep/databases/dumps/`` on target instance. 721 | There is required option ``--options=dumpcode:[value]`` to be passed. 722 | 723 | **Example** 724 | :: 725 | 726 | dep db:upload live --options=dumpcode:0772a8d396911951022db5ea385535f6 727 | 728 | 729 | Changelog 730 | --------- 731 | 732 | See https://github.com/sourcebroker/deployer-extended-database/blob/master/CHANGELOG.rst 733 | -------------------------------------------------------------------------------- /UPGRADE.rst: -------------------------------------------------------------------------------- 1 | 2 | Upgrades 3 | -------- 4 | 5 | 6 | **18.0.0 -> 19.0.0** 7 | -------------------- 8 | 9 | Setting ``bin/php`` to ``localhost('local')`` is not longer required. 10 | PHP is now automatically detected as ``local/bin/php`` is now used. 11 | 12 | Host ``localhost('local')`` is now added by default in very base form: 13 | 14 | :: 15 | 16 | localhost('local') 17 | ->set('deploy_path', getcwd()); 18 | 19 | **17.0.0 -> 18.0.0** 20 | --------------------- 21 | 22 | Change definitions of local host from ``host("local")`` to ``localhost("local")``. 23 | Add "bin/php" to localhost. 24 | 25 | :: 26 | 27 | OLD 28 | 29 | host('local') 30 | ->set('deploy_path', getcwd()); 31 | 32 | NEW 33 | 34 | localhost('local') 35 | ->set('deploy_path', getcwd()) 36 | ->set('bin/php', 'php'); 37 | 38 | **Deployer 6 -> 7** 39 | ------------------- 40 | 41 | If you were modifying ``db_databases`` on host level with ``array_merge_recursive`` and in ``db_databases`` there 42 | were some closures then since Deployer 7 this will no longer work. You can get the same result when using 43 | ``db_databases_overwrite``. You can also use ``db_databases_overwrite_global`` to overwrite with similar way on 44 | global level. 45 | 46 | :: 47 | 48 | OLD 49 | 50 | host('local') 51 | ->set('deploy_path', getcwd()) 52 | ->set('db_databases', array_merge_recursive(get('db_databases'), 53 | [ 54 | 'database_default' => 55 | [ 56 | [ 57 | 'post_sql_in' => 58 | ' 59 | UPDATE table ..... 60 | ' 61 | ] 62 | ] 63 | ])); 64 | 65 | 66 | 67 | NEW 68 | 69 | host('local') 70 | ->set('deploy_path', getcwd()) 71 | ->set('db_databases_overwrite', 72 | [ 73 | 'database_default' => 74 | [ 75 | [ 76 | 'post_sql_in' => 77 | ' 78 | UPDATE table ..... 79 | ' 80 | ] 81 | ] 82 | ]); 83 | 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sourcebroker/deployer-extended-database", 3 | "description": "Deployer tasks to manage database synchronization between application instances.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "deployer", 7 | "deployer.org", 8 | "database", 9 | "synchronization" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Krystian Szymukowicz", 14 | "email": "k.szymukowicz@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "deployer/deployer": "^7.0.0 ", 19 | "sourcebroker/deployer-instance": "^7.0.0 || dev-master", 20 | "sourcebroker/deployer-loader": "^4.0.0 || ^5.0.0 || dev-master" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "SourceBroker\\DeployerExtendedDatabase\\": "src/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /deployer/db/config/host.php: -------------------------------------------------------------------------------- 1 | set('deploy_path', getcwd()); 7 | -------------------------------------------------------------------------------- /deployer/db/config/options.php: -------------------------------------------------------------------------------- 1 | 'sed --version >/dev/null 2>&1 ' . 27 | // '&& sed -i -- \'s/DEFINER=[^*]*\*/\*/g\' {{databaseStorageAbsolutePath}}/*dumpcode={{dumpcode}}*.sql ' . 28 | // '|| sed -i \'\' \'s/DEFINER=[^*]*\*/\*/g\' {{databaseStorageAbsolutePath}}/*dumpcode={{dumpcode}}*.sql' 29 | ]); 30 | 31 | set('db_compress_suffix', '.gz'); 32 | 33 | set('db_compress_command', [ 34 | '{{local/bin/gzip}} --force --name {{databaseStorageAbsolutePath}}/*dumpcode={{dumpcode}}*.sql --suffix ' . get('db_compress_suffix'), 35 | ]); 36 | 37 | set('db_decompress_command', [ 38 | '{{local/bin/gzip}} --force --name --uncompress ' . ' --suffix ' . get('db_compress_suffix') . ' {{databaseStorageAbsolutePath}}/*dumpcode={{dumpcode}}*' . get('db_compress_suffix'), 39 | ]); 40 | 41 | // Returns "db_databases" merged for direct use. 42 | set('db_databases_merged', function () { 43 | $arrayUtility = new ArrayUtility(); 44 | $dbConfigsMerged = []; 45 | $dbDatabases = get('db_databases', []); 46 | $dbDatabasesOverwriteGlobal = get('db_databases_overwrite_global'); 47 | if ($dbDatabasesOverwriteGlobal) { 48 | $dbDatabases = $arrayUtility->arrayMergeRecursiveDistinct($dbDatabases, $dbDatabasesOverwriteGlobal); 49 | } 50 | $dbDatabasesOverwrite = get('db_databases_overwrite'); 51 | if ($dbDatabasesOverwrite) { 52 | $dbDatabases = $arrayUtility->arrayMergeRecursiveDistinct($dbDatabases, $dbDatabasesOverwrite); 53 | } 54 | foreach ($dbDatabases as $dbIdentifier => $dbConfigs) { 55 | $dbConfigsMerged[$dbIdentifier] = []; 56 | 57 | foreach ($dbConfigs as $dbConfig) { 58 | if (is_array($dbConfig)) { 59 | $dbConfigsMerged[$dbIdentifier] 60 | = $arrayUtility->arrayMergeRecursiveDistinct($dbConfigsMerged[$dbIdentifier], $dbConfig); 61 | continue; 62 | } 63 | if ($dbConfig instanceof Closure) { 64 | $mergeArray = $dbConfig(); 65 | $dbConfigsMerged[$dbIdentifier] = $arrayUtility->arrayMergeRecursiveDistinct( 66 | $dbConfigsMerged[$dbIdentifier], 67 | $mergeArray 68 | ); 69 | } 70 | if (is_string($dbConfig)) { 71 | if (file_exists($dbConfig)) { 72 | $mergeArray = include($dbConfig); 73 | $dbConfigsMerged[$dbIdentifier] = $arrayUtility->arrayMergeRecursiveDistinct( 74 | $dbConfigsMerged[$dbIdentifier], 75 | $mergeArray 76 | ); 77 | } else { 78 | throw new GracefulShutdownException('The config file does not exists: ' . $dbConfig); 79 | } 80 | } 81 | } 82 | } 83 | return $dbConfigsMerged; 84 | }); 85 | 86 | // Returns path to store database dumps on local stage. 87 | set('db_storage_path_local', function () { 88 | if (Configuration::getLocalHost()->get('db_storage_path_relative', false) === false) { 89 | $dbStoragePathLocal = Configuration::getLocalHost()->get('deploy_path') . '/.dep/database/dumps'; 90 | } else { 91 | $dbStoragePathLocal = Configuration::getLocalHost()->get('deploy_path') . '/' 92 | . Configuration::getLocalHost()->get('db_storage_path_relative'); 93 | } 94 | $dbStoragePathLocal = (new FileUtility())->resolveHomeDirectoryLocal($dbStoragePathLocal); 95 | runLocally('[ -d ' . $dbStoragePathLocal . ' ] || mkdir -p ' . $dbStoragePathLocal); 96 | 97 | return rtrim($dbStoragePathLocal, '/') . '/'; 98 | }); 99 | 100 | // Returns path to store database dumps on remote stage. 101 | set('db_storage_path', function () { 102 | if (get('db_storage_path_relative', false) === false) { 103 | $dbStoragePath = get('deploy_path') . '/.dep/database/dumps'; 104 | } else { 105 | $dbStoragePath = get('deploy_path') . '/' . get('db_storage_path_relative'); 106 | } 107 | $dbStoragePath = (new FileUtility())->resolveHomeDirectory($dbStoragePath); 108 | run('[ -d ' . $dbStoragePath . ' ] || mkdir -p ' . $dbStoragePath); 109 | 110 | return rtrim($dbStoragePath, '/') . '/'; 111 | }); 112 | 113 | set('bin/deployer', function () { 114 | $deployerBin = get('release_or_current_path') . '/vendor/bin/dep'; 115 | if (!test('[ -e ' . $deployerBin . ' ]')) { 116 | throw new GracefulShutdownException('There must be ' . $deployerBin . ' phar file, but it could not be found.'); 117 | } 118 | 119 | return $deployerBin; 120 | }); 121 | 122 | set('local/bin/deployer', function () { 123 | return './vendor/bin/dep'; 124 | }); 125 | 126 | set('local/bin/mysqldump', function () { 127 | return (new FileUtility())->locateLocalBinaryPath('mysqldump'); 128 | }); 129 | 130 | set('local/bin/mysql', function () { 131 | return (new FileUtility())->locateLocalBinaryPath('mysql'); 132 | }); 133 | 134 | set('local/bin/gzip', function () { 135 | return (new FileUtility())->locateLocalBinaryPath('gzip'); 136 | }); 137 | 138 | set('local/bin/php', function () { 139 | $rawPhpVersion = null; 140 | if (currentHost()->hasOwn('php_version')) { 141 | $rawPhpVersion = get('php_version'); 142 | } 143 | 144 | if (empty($rawPhpVersion) && file_exists('composer.json')) { 145 | try { 146 | $composerJson = json_decode(file_get_contents('composer.json'), true); 147 | if (is_array($composerJson)) { 148 | if (isset($composerJson['config']['platform']['php'])) { 149 | $rawPhpVersion = $composerJson['config']['platform']['php']; 150 | } 151 | if (empty($rawPhpVersion) && isset($composerJson['require']['php'])) { 152 | $rawPhpVersion = $composerJson['require']['php']; 153 | } 154 | } 155 | } catch (\Throwable $e) { 156 | // Silently handle any errors reading composer.json 157 | } 158 | } 159 | 160 | $phpVersionMajorMinor = null; 161 | if (!empty($rawPhpVersion) && preg_match('/[^0-9]*(\d+)(?:\.(\d+))?(?:\.\d+)?/', $rawPhpVersion, $matches)) { 162 | if (isset($matches[1]) && is_numeric($matches[1])) { 163 | $phpVersionMajorMinor = $matches[1] . (isset($matches[2]) && is_numeric($matches[2]) ? '.' . $matches[2] : '.0'); 164 | } 165 | } 166 | 167 | $fileUtility = new FileUtility(); 168 | if ($phpVersionMajorMinor !== null) { 169 | try { 170 | return $fileUtility->locateLocalBinaryPath('php' . $phpVersionMajorMinor); 171 | } catch (\Throwable $e) { 172 | try { 173 | return $fileUtility->locateLocalBinaryPath('php' . str_replace('.', '', $phpVersionMajorMinor)); 174 | } catch (\Throwable $e) { 175 | output()->writeln( 176 | 'PHP binary with version ' . $phpVersionMajorMinor . ' not found, falling back to search for "php"', 177 | \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERBOSE 178 | ); 179 | } 180 | } 181 | } 182 | 183 | try { 184 | $phpBinaryPath = $fileUtility->locateLocalBinaryPath('php'); 185 | $actualVersionMajorMinor = trim(runLocally($phpBinaryPath . ' -r "echo PHP_MAJOR_VERSION.\".\" . PHP_MINOR_VERSION;"')); 186 | 187 | if ($phpVersionMajorMinor !== null && $actualVersionMajorMinor !== $phpVersionMajorMinor) { 188 | $phpVersionStrict = get('php_version_strict', false); 189 | if ($phpVersionStrict) { 190 | throw new \RuntimeException(sprintf( 191 | 'PHP version mismatch: required %s, found %s', 192 | $phpVersionMajorMinor, 193 | $actualVersionMajorMinor 194 | ), 1715438658); 195 | } 196 | output()->writeln( 197 | 'Found PHP binary version (' . $phpBinaryPath . ' ' . $actualVersionMajorMinor . ') does not match required version (' . $phpVersionMajorMinor . ')', 198 | \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL 199 | ); 200 | } 201 | return $phpBinaryPath; 202 | } catch (\Throwable $e) { 203 | if ($e->getCode() === 1715438658) { 204 | throw $e; 205 | } 206 | 207 | output()->writeln( 208 | '"php" command not found in PATH, using just "php" directly', 209 | \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_VERBOSE 210 | ); 211 | $phpBinaryPath = 'php'; 212 | if ($phpVersionMajorMinor !== null) { 213 | $actualVersionMajorMinor = trim(runLocally($phpBinaryPath . ' -r "echo PHP_MAJOR_VERSION.\".\" . PHP_MINOR_VERSION;"')); 214 | 215 | if ($actualVersionMajorMinor !== $phpVersionMajorMinor) { 216 | $phpVersionStrict = get('php_version_strict', false); 217 | if ($phpVersionStrict) { 218 | throw new \RuntimeException(sprintf( 219 | 'PHP version mismatch: required %s, found %s', 220 | $phpVersionMajorMinor, 221 | $actualVersionMajorMinor 222 | ), 1715438658); 223 | } 224 | output()->writeln( 225 | 'PHP version found when running just "php" directly ( ' . $actualVersionMajorMinor . ') does not match required version (' . $phpVersionMajorMinor . ')', 226 | \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL 227 | ); 228 | } 229 | } 230 | return $phpBinaryPath; 231 | } 232 | }); 233 | -------------------------------------------------------------------------------- /deployer/db/task/db_backup.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 14 | $dumpCode = $optionUtility->getOption('dumpcode', false); 15 | if (!$dumpCode) { 16 | $optionUtility->setOption('dumpcode', $consoleUtility->getDumpCode()); 17 | } 18 | 19 | $params = [ 20 | 'argument_host' => get('argument_host'), 21 | 'verbose' => $consoleUtility->getVerbosityAsParameter(), 22 | ]; 23 | 24 | if (get('is_argument_host_the_same_as_local_host')) { 25 | if (testLocally('cd {{deploy_path}} && [ -d release ]')) { 26 | $latestRelease = (int)runLocally('cd {{deploy_path}} && cat .dep/latest_release || echo 0'); 27 | $tags = $optionUtility->getOption('tags', false); 28 | if ($latestRelease > 0) { 29 | $tags[] = 'release'; 30 | $tags[] = 'release_' . $latestRelease; 31 | $optionUtility->setOption('tags', $tags); 32 | } 33 | } 34 | $params['options'] = $optionUtility->getOptionsString(); 35 | $dl = host(get('local_host'))->get('local/bin/php') . ' ' . get('local/bin/deployer'); 36 | runLocally($dl . ' db:export ' . implode(' ', $params)); 37 | runLocally($dl . ' db:compress ' . implode(' ', $params)); 38 | runLocally($dl . ' db:dumpclean ' . implode(' ', $params)); 39 | } else { 40 | $params['options'] = $optionUtility->getOptionsString(); 41 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:backup ' 42 | . implode(' ', array_values($params))); 43 | } 44 | })->desc('Do backup of database (export, compress, dumpclean)'); 45 | -------------------------------------------------------------------------------- /deployer/db/task/db_compress.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 13 | $dumpCode = $optionUtility->getOption('dumpcode', true); 14 | if (get('is_argument_host_the_same_as_local_host')) { 15 | $markersArray = []; 16 | $markersArray['{{databaseStorageAbsolutePath}}'] = get('db_storage_path_local'); 17 | $markersArray['{{dumpcode}}'] = $dumpCode; 18 | if (get('db_compress_command', false) !== false) { 19 | foreach (get('db_compress_command') as $dbProcessCommand) { 20 | runLocally(str_replace( 21 | array_keys($markersArray), 22 | $markersArray, 23 | $dbProcessCommand 24 | )); 25 | } 26 | } 27 | } else { 28 | $params = [ 29 | get('argument_host'), 30 | (new ConsoleUtility())->getVerbosityAsParameter(), 31 | $optionUtility->getOptionsString(), 32 | ]; 33 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:compress ' . implode(' ', $params)); 34 | } 35 | })->desc('Compress dumps with given dumpcode'); 36 | -------------------------------------------------------------------------------- /deployer/db/task/db_copy.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 15 | $targetInstanceName = $optionUtility->getOption('target'); 16 | if (null === $targetInstanceName) { 17 | throw new GracefulShutdownException( 18 | "The target instance is not set in options. You must set the target instance as '--options=target:[target-name]'" 19 | ); 20 | } 21 | 22 | $doNotAskAgainForLive = false; 23 | if ($targetInstanceName === get('instance_live_name', 'live')) { 24 | if (!get('db_allow_copy_live', true)) { 25 | throw new GracefulShutdownException( 26 | 'FORBIDDEN: For security its forbidden to copy database to top instance: "' . $targetInstanceName . '"!' 27 | ); 28 | } 29 | if (!get('db_allow_copy_live_force', false)) { 30 | $doNotAskAgainForLive = true; 31 | writeln("\n\n"); 32 | writeln(sprintf( 33 | "You going to copy database form instance: \"%s\" to top instance: \"%s\". ", 34 | get('argument_host'), 35 | $targetInstanceName 36 | )); 37 | writeln("This can be destructive.\n\n"); 38 | writeln(""); 39 | if (!askConfirmation('Do you really want to continue?', false)) { 40 | throw new GracefulShutdownException('Process aborted.'); 41 | } 42 | if (!askConfirmation('Are you sure?', false)) { 43 | throw new GracefulShutdownException('Process aborted.'); 44 | } 45 | } 46 | } 47 | 48 | if ($targetInstanceName === get('instance_local_name', 'local')) { 49 | throw new GracefulShutdownException( 50 | "FORBIDDEN: For synchro local database use: \ndep db:pull live" 51 | ); 52 | } 53 | 54 | if (!$doNotAskAgainForLive && !askConfirmation(sprintf( 55 | "Do you really want to copy database from instance %s to instance %s", 56 | get('argument_host'), 57 | $targetInstanceName 58 | ), true)) { 59 | throw new GracefulShutdownException( 60 | "Process aborted" 61 | ); 62 | } 63 | 64 | $verbosity = $consoleUtility->getVerbosityAsParameter(); 65 | $sourceInstance = get('argument_host'); 66 | $local = get('local_host'); 67 | $dl = host($local)->get('local/bin/php') . ' ' . get('local/bin/deployer'); 68 | $optionUtility->setOption('dumpcode', $consoleUtility->getDumpCode()); 69 | $optionUtility->setOption('tags', ['copy']); 70 | $options = $optionUtility->getOptionsString(); 71 | if (get('is_argument_host_the_same_as_local_host')) { 72 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:export ' . $local . ' ' . $options . ' ' . $verbosity))); 73 | } else { 74 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:export ' . $sourceInstance . ' ' . $options . ' ' . $verbosity))); 75 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:download ' . $sourceInstance . ' ' . $options . ' ' . $verbosity))); 76 | runLocally($dl . ' db:rmdump ' . $sourceInstance . ' ' . $options . ' ' . $verbosity); 77 | } 78 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:upload ' . $targetInstanceName . ' ' . $options . ' ' . $verbosity))); 79 | runLocally($dl . ' db:rmdump ' . $local . ' ' . $options . ' ' . $verbosity); 80 | runLocally($dl . ' db:process ' . $targetInstanceName . ' ' . $options . ' ' . $verbosity); 81 | 82 | // Make backup of database before import 83 | $backupOptions = new OptionUtility(); 84 | $backupOptions->setOption('dumpcode', $consoleUtility->getDumpCode()); 85 | $backupOptions->setOption('tags', ['copy', 'import_backup']); 86 | runLocally($dl . ' db:backup ' . $targetInstanceName . ' ' . $backupOptions->getOptionsString() . ' ' . $verbosity); 87 | 88 | $importOutput = runLocally($dl . ' db:import ' . $targetInstanceName . ' ' . $options . ' ' . $verbosity); 89 | output()->writeln($consoleUtility->formattingSubtaskTree(preg_replace('/^task db:export\n?/', '', $importOutput))); 90 | runLocally($dl . ' db:compress ' . $targetInstanceName . ' ' . $options . ' ' . $verbosity); 91 | runLocally($dl . ' db:dumpclean ' . $targetInstanceName . ' ' . $verbosity); 92 | 93 | })->desc('Synchronize database between instances'); 94 | -------------------------------------------------------------------------------- /deployer/db/task/db_decompress.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 15 | $dumpCode = $optionUtility->getOption('dumpcode', true); 16 | if (get('is_argument_host_the_same_as_local_host')) { 17 | $decompressedDumpFile = (new DatabaseUtility())->getDumpFile( 18 | get('db_storage_path_local'), ['dumpcode' => $dumpCode], ['sql'] 19 | ); 20 | if ($decompressedDumpFile !== null) { 21 | writeln( 22 | 'The .sql file with the given dumpCode already exists, skipping decompression.', 23 | OutputInterface::VERBOSITY_VERBOSE 24 | ); 25 | return; 26 | } 27 | 28 | $markersArray = []; 29 | $markersArray['{{databaseStorageAbsolutePath}}'] = get('db_storage_path_local'); 30 | $markersArray['{{dumpcode}}'] = $dumpCode; 31 | if (get('db_decompress_command', false) !== false) { 32 | foreach (get('db_decompress_command') as $dbProcessCommand) { 33 | runLocally(str_replace( 34 | array_keys($markersArray), 35 | $markersArray, 36 | $dbProcessCommand 37 | )); 38 | } 39 | } 40 | } else { 41 | $params = [ 42 | get('argument_host'), 43 | (new ConsoleUtility())->getVerbosityAsParameter(), 44 | $optionUtility->getOptionsString(), 45 | ]; 46 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:decompress ' . implode(' ', $params)); 47 | } 48 | })->desc('Compress dumps with given dumpcode'); 49 | -------------------------------------------------------------------------------- /deployer/db/task/db_download.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 16 | $dumpCode = $optionUtility->getOption('dumpcode', true); 17 | $localPath = get('db_storage_path_local'); 18 | 19 | runLocally(sprintf( 20 | 'rsync -rz %s --include=%s --exclude=* %s %s', 21 | $rsyncUtility->getSshOptions(get('argument_host')), 22 | escapeshellarg('*dumpcode=' . $dumpCode . '*'), 23 | escapeshellarg($rsyncUtility->getHostWithDbStoragePath(get('argument_host'))), 24 | escapeshellarg($localPath) 25 | )); 26 | 27 | if (get('db_download_info_enable', true)) { 28 | $filePathPattern = $localPath . '/*dumpcode=' . $dumpCode . '*'; 29 | $files = glob($filePathPattern); 30 | if (!empty($files)) { 31 | $totalSizeBytes = 0; 32 | foreach ($files as $filePath) { 33 | $totalSizeBytes += filesize($filePath); 34 | } 35 | $totalSizeMB = number_format($totalSizeBytes / (1024 * 1024), 2); 36 | output()->write($consoleUtility->formattingTaskOutputHeader("Transferred files size: ")); 37 | output()->write($consoleUtility->formattingTaskOutputContent(sprintf("%s MB", $totalSizeMB), false)); 38 | } 39 | } 40 | 41 | })->desc('Download the database dumps with given dumpcode from remote to local database dumps storage'); 42 | -------------------------------------------------------------------------------- /deployer/db/task/db_dumpclean.php: -------------------------------------------------------------------------------- 1 | getDumpFiles( 18 | get('db_storage_path_local'), [], ['sql', 'gz'], ['dateTime' => 'desc'] 19 | ); 20 | 21 | $dumpsStorage = []; 22 | foreach ($dumpFiles as $file) { 23 | $dumpcode = $fileUtility->getDumpFilenamePart($file, 'dumpcode'); 24 | $server = $fileUtility->getDumpFilenamePart($file, 'server'); 25 | 26 | if (empty($server) || empty($dumpcode)) { 27 | writeln('Note: "server" or "dumpcode" can not be detected for file dump: "' 28 | . get('db_storage_path_local') 29 | . $file); 30 | writeln('Seems like this file was not created by deployer-extended-database or was created by previous version of deployer-extended-database. Please remove this file manually to get rid of this notice.'); 31 | writeln(''); 32 | continue; 33 | } 34 | $dumpsStorage[$server][$dumpcode] = $dumpcode; 35 | } 36 | 37 | $dbDumpCleanKeep = get('db_dumpclean_keep', 5); 38 | foreach ($dumpsStorage as $server => $serverDumps) { 39 | $serverDumps = array_values($serverDumps); 40 | 41 | if (is_array($dbDumpCleanKeep)) { 42 | if (!empty($dbDumpCleanKeep[$server])) { 43 | $keepCount = $dbDumpCleanKeep[$server]; 44 | } elseif (!empty($dbDumpCleanKeep['*'])) { 45 | $keepCount = $dbDumpCleanKeep['*']; 46 | } else { 47 | $keepCount = 5; 48 | } 49 | } else { 50 | $keepCount = $dbDumpCleanKeep; 51 | } 52 | 53 | if (count($serverDumps) > $keepCount) { 54 | $serverDumpsCount = count($serverDumps); 55 | for ($i = $keepCount; $i < $serverDumpsCount; $i++) { 56 | writeln('Removing old dump with code: ' . $serverDumps[$i], OutputInterface::VERBOSITY_VERBOSE); 57 | $databaseUtility->removeDumpFiles(get('db_storage_path_local'), ['dumpcode' => $serverDumps[$i]]); 58 | } 59 | } 60 | } 61 | } else { 62 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:dumpclean ' 63 | . get('argument_host') . ' ' . (new ConsoleUtility())->getVerbosityAsParameter()); 64 | } 65 | })->desc('Cleans the database dump storage'); 66 | -------------------------------------------------------------------------------- /deployer/db/task/db_export.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 21 | $dumpCode = $optionUtility->getOption('dumpcode', true); 22 | $tags = $optionUtility->getOption('tags'); 23 | 24 | if (get('is_argument_host_the_same_as_local_host')) { 25 | foreach (get('db_databases_merged') as $databaseCode => $databaseConfig) { 26 | if ($optionUtility->getOption('dbCodeFilter')) { 27 | if (!in_array($databaseCode, $optionUtility->getOption('dbCodeFilter'), true)) { 28 | continue; 29 | } 30 | } 31 | 32 | $filenameParts = [ 33 | 'dateTime' => date('Y-m-d_H-i-s'), 34 | 'server' => 'server=' . $fileUtility->normalizeFilename(get('local_host')), 35 | 'dbcode' => 'dbcode=' . $fileUtility->normalizeFilename($databaseCode), 36 | 'dumpcode' => 'dumpcode=' . $fileUtility->normalizeFilename($dumpCode), 37 | ]; 38 | if (is_array($tags) && count($tags) > 0) { 39 | $filenameParts['tags'] = 'tags=' 40 | . $fileUtility->normalizeFilename(implode(OptionUtility::ARRAY_OPTIONS_IMPLODE_CHAR, $tags)); 41 | } 42 | $databaseStoragePathLocal = get('db_storage_path_local'); 43 | $tmpMyCnfFile = DatabaseUtility::getTemporaryMyCnfFile( 44 | $databaseConfig, 45 | $databaseStoragePathLocal 46 | ); 47 | $mysqlDumpArgs = [ 48 | 'local/bin/mysqldump' => get('local/bin/mysqldump'), 49 | escapeshellarg($tmpMyCnfFile), 50 | 'options' => '', 51 | 'dbname' => escapeshellarg($databaseConfig['dbname']), 52 | 'absolutePath' => '', 53 | 'ignore-tables' => '', 54 | ]; 55 | 56 | if (isset($databaseConfig['ignore_tables_out']) && is_array($databaseConfig['ignore_tables_out'])) { 57 | $ignoreTables = $arrayUtility->filterWithRegexp( 58 | $databaseConfig['ignore_tables_out'], 59 | $databaseUtility->getTables($databaseConfig) 60 | ); 61 | if (!empty($ignoreTables)) { 62 | if (get('db_export_mysqldump_show_ignore_tables_out_info_enable', true)) { 63 | $maxLineLength = get('db_export_mysqldump_show_ignore_tables_out_max_line_length', 120); 64 | 65 | $chunks = []; 66 | $currentLine = ''; 67 | $currentLength = 0; 68 | 69 | foreach ($ignoreTables as $table) { 70 | $tableWithSeparator = $table . ' | '; 71 | $tableLength = strlen($tableWithSeparator); 72 | 73 | if ($currentLength + $tableLength > $maxLineLength && $currentLength > 0) { 74 | $chunks[] = rtrim($currentLine, ' |'); 75 | $currentLine = ''; 76 | $currentLength = 0; 77 | } 78 | 79 | $currentLine .= $tableWithSeparator; 80 | $currentLength += $tableLength; 81 | } 82 | 83 | if ($currentLine !== '') { 84 | $chunks[] = rtrim($currentLine, ' |'); 85 | } 86 | 87 | $ignoredTablesText = implode("\n", $chunks); 88 | 89 | output()->writeln($consoleUtility->formattingTaskOutputHeader('Ignored tables:')); 90 | output()->write($consoleUtility->formattingTaskOutputContent($ignoredTablesText)); 91 | } 92 | $mysqlDumpArgs['ignore-tables'] = implode(' ', 93 | array_map(static function ($table) use ($databaseConfig) { 94 | return '--ignore-table=' . escapeshellarg($databaseConfig['dbname'] . '.' . $table); 95 | }, $ignoreTables)); 96 | } 97 | } 98 | 99 | try { 100 | // dump database structure 101 | $filenameParts['type'] = 'type=structure'; 102 | $mysqlDumpArgs['options'] = get('db_export_mysqldump_options_structure', ''); 103 | $mysqlDumpArgs['options'] .= DatabaseUtility::getSslCliOptions($databaseConfig); 104 | $mysqlDumpArgs['absolutePath'] = escapeshellarg($databaseStoragePathLocal . implode('#', 105 | $filenameParts) . '.sql'); 106 | 107 | runLocally(vsprintf( 108 | '%s --defaults-file=%s %s %s -r%s' 109 | . ($optionUtility->getOption('exportTaskAddIgnoreTablesToStructureDump') ? ' %s' : ''), 110 | $mysqlDumpArgs 111 | )); 112 | 113 | // dump database data 114 | $filenameParts['type'] = 'type=data'; 115 | $mysqlDumpArgs['options'] = get('db_export_mysqldump_options_data', ''); 116 | $mysqlDumpArgs['options'] .= DatabaseUtility::getSslCliOptions($databaseConfig); 117 | $mysqlDumpArgs['absolutePath'] = escapeshellarg($databaseStoragePathLocal . implode('#', 118 | $filenameParts) . '.sql'); 119 | runLocally(vsprintf( 120 | '%s --defaults-file=%s %s %s -r%s %s', 121 | $mysqlDumpArgs 122 | )); 123 | } catch (\Exception $exception) { 124 | throw new GracefulShutdownException( 125 | 'Error during import dump with dumpcode: ' . $dumpCode . '. ' . $exception->getMessage(), 126 | 1500722095323 127 | ); 128 | } finally { 129 | unlink($tmpMyCnfFile); 130 | } 131 | } 132 | 133 | } else { 134 | $params = [ 135 | get('argument_host'), 136 | $consoleUtility->getVerbosityAsParameter(), 137 | $optionUtility->getOptionsString(), 138 | ]; 139 | $output = run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:export ' 140 | . implode(' ', $params)); 141 | output()->write(preg_replace('/^task db:export\n?/', '', $output)); 142 | } 143 | })->desc('Dump database and store it in database dumps storage'); 144 | -------------------------------------------------------------------------------- /deployer/db/task/db_import.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 18 | $dumpCode = $optionUtility->getOption('dumpcode', true); 19 | if (get('is_argument_host_the_same_as_local_host')) { 20 | $databaseStoragePathLocal = get('db_storage_path_local'); 21 | foreach (get('db_databases_merged') as $databaseCode => $databaseConfig) { 22 | if ($optionUtility->getOption('dbCodeFilter')) { 23 | if (!in_array($databaseCode, $optionUtility->getOption('dbCodeFilter'), true)) { 24 | continue; 25 | } 26 | } 27 | 28 | $globStart = $databaseStoragePathLocal 29 | . '*dbcode=' . $fileUtility->normalizeFilename($databaseCode) 30 | . '*dumpcode=' . $dumpCode; 31 | 32 | $structureSqlFile = glob($globStart . '*type=structure.sql'); 33 | if (empty($structureSqlFile)) { 34 | throw new GracefulShutdownException( 35 | 'No structure file for --options=dumpcode:' . $dumpCode . '. Glob build: ' . 36 | $globStart . '*type=structure.sql', 37 | 1500718221204 38 | ); 39 | } 40 | if (count($structureSqlFile) > 1) { 41 | throw new GracefulShutdownException('There are more than two structure file for --options=dumpcode:' . $dumpCode . 42 | '. Glob build: ' . $globStart . '*type=structure.sql. ' . 43 | "\n" . ' Files founded: ' . "\n" . implode("\n", $structureSqlFile), 1500722088929); 44 | } 45 | $dataSqlFile = glob($globStart . '*type=data.sql'); 46 | if (empty($dataSqlFile)) { 47 | throw new GracefulShutdownException( 48 | 'No data file for --options=dumpcode:' . $dumpCode . '. Glob built: ' . 49 | $globStart . '*type=data.sql', 50 | 1500722093334 51 | ); 52 | } 53 | if (count($dataSqlFile) > 1) { 54 | throw new GracefulShutdownException( 55 | 'There are more than two data files for --options=dumpcode:' . $dumpCode . '. Glob built: ' . 56 | $globStart . '*type=data.sql. ' . 57 | "\n" . ' Files founded: ' . "\n" . implode("\n", $dataSqlFile), 58 | 1500722095323 59 | ); 60 | } 61 | $tmpMyCnfFile = DatabaseUtility::getTemporaryMyCnfFile( 62 | $databaseConfig, 63 | $databaseStoragePathLocal 64 | ); 65 | 66 | try { 67 | // Drop all tables. 68 | if (empty($optionUtility->getOption('importTaskDoNotDropAllTablesBeforeImport'))) { 69 | runLocally(sprintf( 70 | '%s --defaults-file=%s %s %s --add-drop-table --no-data | ' . 71 | 'grep -e \'^DROP \| FOREIGN_KEY_CHECKS\' | %s --defaults-file=%s %s -D%s', 72 | get('local/bin/mysqldump'), 73 | escapeshellarg($tmpMyCnfFile), 74 | DatabaseUtility::getSslCliOptions($databaseConfig), 75 | escapeshellarg($databaseConfig['dbname']), 76 | get('local/bin/mysql'), 77 | escapeshellarg($tmpMyCnfFile), 78 | DatabaseUtility::getSslCliOptions($databaseConfig), 79 | escapeshellarg($databaseConfig['dbname']) 80 | )); 81 | } 82 | // Import dump with database structure. 83 | runLocally(sprintf( 84 | '%s --defaults-file=%s %s %s -D%s -e%s', 85 | get('local/bin/mysql'), 86 | escapeshellarg($tmpMyCnfFile), 87 | get('db_import_mysql_options_structure', ''), 88 | DatabaseUtility::getSslCliOptions($databaseConfig), 89 | escapeshellarg($databaseConfig['dbname']), 90 | escapeshellarg('SOURCE ' . $structureSqlFile[0]) 91 | )); 92 | // Import dump with data. 93 | runLocally(sprintf( 94 | '%s --defaults-file=%s %s %s -D%s -e%s', 95 | get('local/bin/mysql'), 96 | escapeshellarg($tmpMyCnfFile), 97 | get('db_import_mysql_options_data', ''), 98 | DatabaseUtility::getSslCliOptions($databaseConfig), 99 | escapeshellarg($databaseConfig['dbname']), 100 | escapeshellarg('SOURCE ' . $dataSqlFile[0]) 101 | )); 102 | $postSqlInCollected = []; 103 | if (isset($databaseConfig['post_sql_in_markers']) && is_string($databaseConfig['post_sql_in_markers']) && $databaseConfig['post_sql_in_markers'] !== '') { 104 | $markersArray = []; 105 | if (!empty(get('public_urls', []))) { 106 | $publicUrlCollected = []; 107 | foreach (get('public_urls') as $publicUrl) { 108 | if (parse_url($publicUrl, PHP_URL_SCHEME) && parse_url($publicUrl, PHP_URL_HOST)) { 109 | $port = ''; 110 | if (parse_url($publicUrl, PHP_URL_PORT)) { 111 | $port = ':' . parse_url($publicUrl, PHP_URL_PORT); 112 | } 113 | $publicUrlCollected[] = parse_url($publicUrl, PHP_URL_HOST) . $port; 114 | } else { 115 | throw new GracefulShutdownException('The configuration setting "public_urls" should have full url like 116 | "https://www.example.com" but the value is only "' . $publicUrl . '"', 1491384103020); 117 | } 118 | } 119 | $markersArray['{{domainsSeparatedByComma}}'] = '"' . implode( 120 | '","', 121 | $publicUrlCollected 122 | ) . '"'; 123 | $markersArray['{{firstDomainWithScheme}}'] = get('public_urls')[0]; 124 | $markersArray['{{firstDomainWithSchemeAndEndingSlash}}'] = rtrim(get('public_urls')[0], 125 | '/') . '/'; 126 | } 127 | $postSqlInCollected[] = str_replace( 128 | array_keys($markersArray), 129 | $markersArray, 130 | $databaseConfig['post_sql_in_markers'] 131 | ); 132 | } 133 | if (isset($databaseConfig['post_sql_in']) && is_string($databaseConfig['post_sql_in']) && $databaseConfig['post_sql_in'] !== '') { 134 | $postSqlInCollected[] = $databaseConfig['post_sql_in']; 135 | } 136 | if (!empty($postSqlInCollected)) { 137 | $importSqlFile = $databaseStoragePathLocal . $dumpCode . '.sql'; 138 | file_put_contents($importSqlFile, implode(' ', $postSqlInCollected)); 139 | runLocally(sprintf( 140 | ' %s --defaults-file=%s %s %s -D%s -e%s', 141 | get('local/bin/mysql'), 142 | escapeshellarg($tmpMyCnfFile), 143 | get('db_import_mysql_options_post_sql_in', ''), 144 | DatabaseUtility::getSslCliOptions($databaseConfig), 145 | escapeshellarg($databaseConfig['dbname']), 146 | escapeshellarg('SOURCE ' . $importSqlFile) 147 | )); 148 | unlink($importSqlFile); 149 | } 150 | if (isset($databaseConfig['post_command']) && is_array($databaseConfig['post_command'])) { 151 | foreach ($databaseConfig['post_command'] as $postCommand) { 152 | runLocally($postCommand . ' ' . $optionUtility->getOptionsString()); 153 | } 154 | } 155 | 156 | if (get('db_import_big_table_info_enable', true)) { 157 | $databaseUtility = new DatabaseUtility(); 158 | $bigTableSizeThreshold = get('db_import_big_table_size_threshold', 50); 159 | $bigTables = $databaseUtility->getBigTables($databaseConfig, $bigTableSizeThreshold); 160 | 161 | if (!empty($bigTables)) { 162 | output()->writeln($consoleUtility->formattingTaskOutputHeader('Big tables:')); 163 | $bigTablesText = ''; 164 | $line = ''; 165 | $lineLength = 0; 166 | $maxLineLength = get('db_import_big_table_output_max_line_length', 120); 167 | 168 | foreach ($bigTables as $tableInfo) { 169 | $tableSizeText = $tableInfo['Table'] . ' (' . $tableInfo['Size (MB)'] . ' MB) | '; 170 | $tableSizeTextLength = strlen($tableSizeText); 171 | 172 | if ($lineLength + $tableSizeTextLength > $maxLineLength) { 173 | $bigTablesText .= rtrim($line, ' |') . "\n"; 174 | $line = ''; 175 | $lineLength = 0; 176 | } 177 | $line .= $tableSizeText; 178 | $lineLength += $tableSizeTextLength; 179 | } 180 | $bigTablesText .= rtrim($line, ' |'); 181 | 182 | output()->write($consoleUtility->formattingTaskOutputContent($bigTablesText)); 183 | } 184 | } 185 | 186 | } catch (\Exception $exception) { 187 | throw new GracefulShutdownException( 188 | 'Error during import dump with dumpcode: ' . $dumpCode . '. ' . $exception->getMessage(), 189 | 1500722095323 190 | ); 191 | } finally { 192 | unlink($tmpMyCnfFile); 193 | } 194 | } 195 | } else { 196 | $params = [ 197 | get('argument_host'), 198 | $consoleUtility->getVerbosityAsParameter(), 199 | $optionUtility->getOptionsString(), 200 | ]; 201 | 202 | $output = run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:import ' 203 | . implode(' ', $params)); 204 | output()->write(preg_replace('/^task db:import\n?/', '', $output)); 205 | } 206 | })->desc('Import dump with given dumpcode from database dumps storage to database'); 207 | -------------------------------------------------------------------------------- /deployer/db/task/db_process.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 13 | $dumpCode = $optionUtility->getOption('dumpcode', true); 14 | if (get('is_argument_host_the_same_as_local_host')) { 15 | $markersArray = []; 16 | $markersArray['{{databaseStorageAbsolutePath}}'] = get('db_storage_path_local'); 17 | $markersArray['{{dumpcode}}'] = $dumpCode; 18 | if (get('db_process_commands', false) !== false) { 19 | foreach (get('db_process_commands') as $dbProcessCommand) { 20 | runLocally(str_replace( 21 | array_keys($markersArray), 22 | $markersArray, 23 | $dbProcessCommand 24 | )); 25 | } 26 | } 27 | } else { 28 | $params = [ 29 | get('argument_host'), 30 | (new ConsoleUtility())->getVerbosityAsParameter(), 31 | $optionUtility->getOptionsString(), 32 | ]; 33 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:process ' . implode(' ', $params)); 34 | } 35 | })->desc('Run commands that process mysql dump file directly'); 36 | -------------------------------------------------------------------------------- /deployer/db/task/db_pull.php: -------------------------------------------------------------------------------- 1 | \n\n"); 28 | writeln(sprintf( 29 | "You going to pull database from instance: \"%s\" to top instance: \"%s\". ", 30 | $sourceName, 31 | get('local_host') 32 | )); 33 | writeln("This can be destructive.\n\n"); 34 | writeln(""); 35 | if (!askConfirmation('Do you really want to continue?', false)) { 36 | throw new GracefulShutdownException('Process aborted.'); 37 | } 38 | if (!askConfirmation('Are you sure?', false)) { 39 | throw new GracefulShutdownException('Process aborted.'); 40 | } 41 | } 42 | } 43 | $local = get('local_host'); 44 | $dl = host($local)->get('local/bin/php') . ' ' . get('local/bin/deployer'); 45 | $consoleUtility = new ConsoleUtility(); 46 | $verbosity = $consoleUtility->getVerbosityAsParameter(); 47 | $optionUtility = new OptionUtility(input()->getOption('options')); 48 | $optionUtility->setOption('tags', ['pull']); 49 | 50 | $fromStorage = $optionUtility->getOption('fromLocalStorage'); 51 | if ($fromStorage) { 52 | $databaseUtility = new DatabaseUtility(); 53 | $lastDumpFilename = $databaseUtility->getLastDumpFile( 54 | get('db_storage_path_local'), ['server' => get('argument_host')] 55 | ); 56 | if ($lastDumpFilename === null) { 57 | output()->writeln($consoleUtility->formattingTaskOutputHeader('No database dumps found for `' . get('argument_host') . '` in local database storage.')); 58 | return; 59 | } 60 | $fileUtility = new FileUtility(); 61 | $optionUtility->setOption('dumpcode', $fileUtility->getDumpFilenamePart($lastDumpFilename, 'dumpcode')); 62 | $optionUtility->removeOption('fromLocalStorage'); 63 | $options = $optionUtility->getOptionsString(); 64 | 65 | $dumpCode = $fileUtility->getDumpFilenamePart($lastDumpFilename, 'dumpcode'); 66 | $dateTime = $fileUtility->getDumpFilenamePart($lastDumpFilename, 'dateTime'); 67 | $server = $fileUtility->getDumpFilenamePart($lastDumpFilename, 'server'); 68 | 69 | if (get('db_pull_from_local_storage_info_enable', true)) { 70 | output()->writeln($consoleUtility->formattingTaskOutputHeader('Last dump found:')); 71 | output()->writeln($consoleUtility->formattingTaskOutputContent('- dumpcode: ' . $dumpCode)); 72 | output()->writeln($consoleUtility->formattingTaskOutputContent('- date: ' . $dateTime->format('Y-m-d H:i:s'))); 73 | output()->writeln($consoleUtility->formattingTaskOutputContent('- from host: ' . $server)); 74 | } 75 | runLocally($dl . ' db:decompress ' . $local . ' ' . $options . ' ' . $verbosity); 76 | } else { 77 | $optionUtility->setOption('dumpcode', $consoleUtility->getDumpCode()); 78 | $options = $optionUtility->getOptionsString(); 79 | 80 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:export ' . $sourceName . ' ' . $options . ' ' . $verbosity))); 81 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:download ' . $sourceName . ' ' . $options . ' ' . $verbosity))); 82 | runLocally($dl . ' db:rmdump ' . $sourceName . ' ' . $options . ' ' . $verbosity); 83 | runLocally($dl . ' db:process ' . $local . ' ' . $options . ' ' . $verbosity); 84 | 85 | // Make backup of local database before import. No backup if $from Storage=true as it expect to be fast repeatable last downloaded db import. 86 | $backupOptions = new OptionUtility(); 87 | $backupOptions->setOption('dumpcode', $consoleUtility->getDumpCode()); 88 | $backupOptions->setOption('tags', ['pull', 'import_backup']); 89 | runLocally($dl . ' db:backup ' . $local . ' ' . $backupOptions->getOptionsString() . ' ' . $verbosity); 90 | } 91 | 92 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:import ' . $local . ' ' . $options . ' ' . $verbosity))); 93 | runLocally($dl . ' db:compress ' . $local . ' ' . $options . ' ' . $verbosity); 94 | runLocally($dl . ' db:dumpclean ' . $local . ' ' . $verbosity); 95 | })->desc('Pull database from remote to local'); 96 | -------------------------------------------------------------------------------- /deployer/db/task/db_push.php: -------------------------------------------------------------------------------- 1 | \n\n"); 22 | writeln(sprintf( 23 | "You going to push database from instance: \"%s\" to top instance: \"%s\". ", 24 | get('local_host'), 25 | $targetName 26 | )); 27 | writeln("This can be destructive.\n\n"); 28 | writeln(""); 29 | if (!askConfirmation('Do you really want to continue?', false)) { 30 | throw new GracefulShutdownException('Process aborted.'); 31 | } 32 | if (!askConfirmation('Are you sure?', false)) { 33 | throw new GracefulShutdownException('Process aborted.'); 34 | } 35 | } 36 | } 37 | $local = get('local_host'); 38 | $dl = host($local)->get('local/bin/php') . ' ' . get('local/bin/deployer'); 39 | $consoleUtility = new ConsoleUtility(); 40 | $optionUtility = new OptionUtility(input()->getOption('options')); 41 | $optionUtility->setOption('dumpcode', $consoleUtility->getDumpCode()); 42 | $optionUtility->setOption('tags', ['push']); 43 | $verbosity = $consoleUtility->getVerbosityAsParameter(); 44 | $options = $optionUtility->getOptionsString(); 45 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:export ' . $local . ' ' . $options . ' ' . $verbosity))); 46 | output()->writeln($consoleUtility->formattingSubtaskTree(runLocally($dl . ' db:upload ' . $targetName . ' ' . $options . ' ' . $verbosity))); 47 | runLocally($dl . ' db:rmdump ' . $local . ' ' . $options . ' ' . $verbosity); 48 | runLocally($dl . ' db:process ' . $targetName . ' ' . $options . ' ' . $verbosity); 49 | 50 | // Make backup of target database before import 51 | $backupOptions = new OptionUtility(); 52 | $backupOptions->setOption('dumpcode', $consoleUtility->getDumpCode()); 53 | $backupOptions->setOption('tags', ['push', 'import_backup']); 54 | runLocally($dl . ' db:backup ' . $targetName . ' ' . $backupOptions->getOptionsString() . ' ' . $verbosity); 55 | 56 | $importOutput = runLocally($dl . ' db:import ' . $targetName . ' ' . $options . ' ' . $verbosity); 57 | output()->writeln($consoleUtility->formattingSubtaskTree(preg_replace('/^task db:export\n?/', '', $importOutput))); 58 | runLocally($dl . ' db:compress ' . $targetName . ' ' . $options . ' ' . $verbosity); 59 | runLocally($dl . ' db:dumpclean ' . $targetName . ' ' . $verbosity); 60 | 61 | })->desc('Push database from local to remote'); 62 | -------------------------------------------------------------------------------- /deployer/db/task/db_rmdump.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 14 | $dumpCode = $optionUtility->getOption('dumpcode', true); 15 | if (get('is_argument_host_the_same_as_local_host')) { 16 | $databaseUtility = new DatabaseUtility(); 17 | $databaseUtility->removeDumpFiles(get('db_storage_path_local'), ['dumpcode' => $dumpCode]); 18 | } else { 19 | $params = [ 20 | get('argument_host'), 21 | (new ConsoleUtility())->getVerbosityAsParameter(), 22 | input()->getOption('options') ? '--options=' . input()->getOption('options') : '', 23 | ]; 24 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:rmdump ' . implode(' ', $params)); 25 | } 26 | })->desc('Remove all dumps with given dumpcode (compressed and uncompressed)'); 27 | -------------------------------------------------------------------------------- /deployer/db/task/db_truncate.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 19 | 20 | if (get('is_argument_host_the_same_as_local_host')) { 21 | foreach (get('db_databases_merged') as $databaseCode => $databaseConfig) { 22 | if ($optionUtility->getOption('dbCodeFilter')) { 23 | if (!in_array($databaseCode, $optionUtility->getOption('dbCodeFilter'), true)) { 24 | continue; 25 | } 26 | } 27 | 28 | if (isset($databaseConfig['truncate_tables']) && is_array($databaseConfig['truncate_tables'])) { 29 | $truncateTables = $arrayUtility->filterWithRegexp( 30 | $databaseConfig['truncate_tables'], 31 | $databaseUtility->getTables($databaseConfig) 32 | ); 33 | 34 | if (!empty($truncateTables)) { 35 | $databaseStoragePathLocal = get('db_storage_path_local'); 36 | 37 | $tmpMyCnfFile = DatabaseUtility::getTemporaryMyCnfFile( 38 | $databaseConfig, 39 | $databaseStoragePathLocal 40 | ); 41 | 42 | try { 43 | $truncateSql = implode('; ', array_map(function ($table) { 44 | return 'TRUNCATE ' . $table; 45 | }, $truncateTables)) . ';'; 46 | 47 | runLocally(sprintf( 48 | '%s --defaults-file=%s -D%s %s -e %s', 49 | get('local/bin/mysql'), 50 | escapeshellarg($tmpMyCnfFile), 51 | escapeshellarg($databaseConfig['dbname']), 52 | DatabaseUtility::getSslCliOptions($databaseConfig), 53 | escapeshellarg($truncateSql) 54 | )); 55 | 56 | if (output()->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { 57 | info('Truncated tables: ' . implode(',', $truncateTables)); 58 | } 59 | 60 | } catch (\Exception $exception) { 61 | throw new GracefulShutdownException( 62 | 'Error during truncate. ' . $exception->getMessage(), 63 | 1500722095323 64 | ); 65 | } finally { 66 | unlink($tmpMyCnfFile); 67 | } 68 | } 69 | } 70 | } 71 | } else { 72 | $verbosity = (new ConsoleUtility())->getVerbosityAsParameter(); 73 | run('cd {{release_or_current_path}} && {{bin/php}} {{bin/deployer}} db:truncate ' . get('argument_host') . $verbosity); 74 | } 75 | })->desc('Truncate tables defined in "truncate_tables" variable'); 76 | -------------------------------------------------------------------------------- /deployer/db/task/db_upload.php: -------------------------------------------------------------------------------- 1 | getOption('options')); 16 | $dumpCode = $optionUtility->getOption('dumpcode', true); 17 | $localPath = get('db_storage_path_local'); 18 | 19 | runLocally(sprintf( 20 | 'rsync -rz %s --include=%s --exclude=* %s %s', 21 | $rsyncUtility->getSshOptions(get('argument_host')), 22 | escapeshellarg('*dumpcode=' . $dumpCode . '*'), 23 | escapeshellarg($localPath), 24 | escapeshellarg($rsyncUtility->getHostWithDbStoragePath(get('argument_host'))) 25 | )); 26 | 27 | if (get('db_upload_info_enable', true)) { 28 | $filePathPattern = $localPath . '/*dumpcode=' . $dumpCode . '*'; 29 | $files = glob($filePathPattern); 30 | if (!empty($files)) { 31 | $totalSizeBytes = 0; 32 | foreach ($files as $filePath) { 33 | $totalSizeBytes += filesize($filePath); 34 | } 35 | $totalSizeMB = number_format($totalSizeBytes / (1024 * 1024), 2); 36 | output()->write($consoleUtility->formattingTaskOutputHeader("Transferred files size: ")); 37 | output()->write($consoleUtility->formattingTaskOutputContent(sprintf("%s MB", $totalSizeMB), false)); 38 | } 39 | } 40 | 41 | })->desc('Upload the database dump for given dumpcode from local to remote database dumps storage'); 42 | -------------------------------------------------------------------------------- /src/Driver/EnvDriver.php: -------------------------------------------------------------------------------- 1 | load($envFilePath); 14 | foreach (['DATABASE_HOST', 'DATABASE_NAME', 'DATABASE_USER', 'DATABASE_PASSWORD'] as $requiredEnv) { 15 | if (false === $this->getenv($prefix . $requiredEnv)) { 16 | throw new RuntimeException('Missing ' . $prefix . $requiredEnv . ' in ' . $envFilePath . ' file.'); 17 | } 18 | } 19 | return [ 20 | // required 21 | 'host' => $this->getenv($prefix . 'DATABASE_HOST'), 22 | 'port' => $this->getenv($prefix . 'DATABASE_PORT') ?: 3306, 23 | 'dbname' => $this->getenv($prefix . 'DATABASE_NAME'), 24 | 'user' => $this->getenv($prefix . 'DATABASE_USER'), 25 | 'password' => $this->getenv($prefix . 'DATABASE_PASSWORD'), 26 | // flags 27 | 'flags' => $this->getenv($prefix . 'DATABASE_FLAGS'), 28 | // SSL 29 | 'ssl_key' => $this->getenv($prefix . 'DATABASE_SSL_KEY'), 30 | 'ssl_cert' => $this->getenv($prefix . 'DATABASE_SSL_CERT'), 31 | 'ssl_ca' => $this->getenv($prefix . 'DATABASE_SSL_CA'), 32 | 'ssl_capath' => $this->getenv($prefix . 'DATABASE_SSL_CAPATH'), 33 | 'ssl_cipher' => $this->getenv($prefix . 'DATABASE_SSL_CIPHER'), 34 | ]; 35 | } 36 | 37 | private function getenv($env) 38 | { 39 | return $_ENV[$env] ?? null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Utility/ArrayUtility.php: -------------------------------------------------------------------------------- 1 | $value) { 22 | if (!array_key_exists($key, $base) && !is_numeric($key)) { 23 | $base[$key] = $append[$key]; 24 | continue; 25 | } 26 | if (is_array($value) || is_array($base[$key])) { 27 | $base[$key] = self::arrayMergeRecursiveDistinct($base[$key], $append[$key]); 28 | } else { 29 | if (is_numeric($key)) { 30 | if (!in_array($value, $base)) { 31 | $base[] = $value; 32 | } 33 | } else { 34 | $base[$key] = $value; 35 | } 36 | } 37 | } 38 | } 39 | return $base; 40 | } 41 | 42 | 43 | /** 44 | * Filter $haystack array items with items from array $patterns. 45 | * Example usage: 46 | * filterWithRegexp(['cf_.*', 'bcd'], ['abc', 'cf_test1', 'bcd' ,'cf_test2', 'cde']) will return ['cf_test1', 'bcd', 'cf_test2'] 47 | */ 48 | public function filterWithRegexp(array $patterns, array $haystack): array 49 | { 50 | $foundItems = []; 51 | foreach ($patterns as $pattern) { 52 | $regexp = false; 53 | 54 | set_error_handler(function () {}, E_WARNING); 55 | $isValidPattern = preg_match($pattern, '') !== false; 56 | $isValidPatternDelimiters = preg_match('/^' . $pattern . '$/', '') !== false; 57 | restore_error_handler(); 58 | 59 | if (preg_match('/^[\/\#\+\%\~]/', $pattern) && $isValidPattern) { 60 | $regexp = $pattern; 61 | } elseif ($isValidPatternDelimiters) { 62 | $regexp = '/^' . $pattern . '$/i'; 63 | } 64 | if ($regexp) { 65 | $foundItems = array_merge($foundItems, preg_grep($regexp, $haystack)); 66 | } elseif (in_array($pattern, $haystack)) { 67 | $foundItems[] = $pattern; 68 | } 69 | } 70 | return $foundItems; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Utility/ConsoleUtility.php: -------------------------------------------------------------------------------- 1 | getVerbosity()) { 13 | case OutputInterface::VERBOSITY_DEBUG: 14 | $verbosity = ' -vvv'; 15 | break; 16 | case OutputInterface::VERBOSITY_VERY_VERBOSE: 17 | $verbosity = ' -vv'; 18 | break; 19 | case OutputInterface::VERBOSITY_VERBOSE: 20 | $verbosity = ' -v'; 21 | break; 22 | case OutputInterface::VERBOSITY_QUIET: 23 | $verbosity = ' -q'; 24 | break; 25 | case OutputInterface::VERBOSITY_NORMAL: 26 | default: 27 | $verbosity = ''; 28 | } 29 | return $verbosity; 30 | } 31 | 32 | public function formattingSubtaskTree(string $content): string 33 | { 34 | return ' ├──╸' . $content; 35 | } 36 | 37 | public function formattingTaskOutputHeader(string $output, bool $tab = true): string 38 | { 39 | $content = "\033[35;1m" . $output . "\033[0m"; 40 | return $tab ? $this->formattingTaskOutputTab($content) : $content; 41 | } 42 | 43 | public function formattingTaskOutputContent(string $output, bool $tab = true): string 44 | { 45 | $content = "\033[32;1m" . $output . "\033[0m"; 46 | return $tab ? $this->formattingTaskOutputTab($content) : $content; 47 | } 48 | 49 | public function formattingTaskOutputTab($output): string 50 | { 51 | $outputLines = explode("\n", $output); 52 | $formattedLines = array_map(function ($line) { 53 | $out = "\033[32;1m" . $line . "\033[0m"; 54 | return preg_replace('/^/m', ' │ ', $out); 55 | }, $outputLines); 56 | return implode("\n", $formattedLines); 57 | } 58 | 59 | public function getDumpCode(): string 60 | { 61 | return md5(microtime(true) . random_int(0, 10000)); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Utility/DatabaseUtility.php: -------------------------------------------------------------------------------- 1 | ssl_set( 12 | $dbConf['ssl_key'] ?? null, 13 | $dbConf['ssl_cert'] ?? null, 14 | $dbConf['ssl_ca'] ?? null, 15 | $dbConf['ssl_capath'] ?? null, 16 | $dbConf['ssl_cipher'] ?? null 17 | ); 18 | 19 | $mysqli->real_connect( 20 | $dbConf['host'], 21 | $dbConf['user'], 22 | $dbConf['password'], 23 | $dbConf['dbname'], 24 | $dbConf['port'], 25 | null, 26 | isset($dbConf['flags']) ? (int)$dbConf['flags'] : 0 27 | ); 28 | 29 | if ($mysqli->connect_error) { 30 | throw new \RuntimeException('Connection error: ' . $mysqli->connect_error); 31 | } 32 | 33 | return $mysqli; 34 | } 35 | 36 | public function getTables(array $dbConf): array 37 | { 38 | $mysqli = $this->connect($dbConf); 39 | $result = $mysqli->query('SHOW TABLES'); 40 | $allTables = []; 41 | while ($row = $result->fetch_row()) { 42 | $allTables[] = array_shift($row); 43 | } 44 | return $allTables; 45 | } 46 | 47 | public function getBigTables(array $dbConf, float $bigTableSizeThreshold): array 48 | { 49 | $mysqli = $this->connect($dbConf); 50 | $bigTablesQuery = 'SELECT table_name AS `Table`, 51 | round(((data_length + index_length) / 1024 / 1024), 2) `Size (MB)` 52 | FROM information_schema.TABLES 53 | WHERE table_schema = \'' . $mysqli->real_escape_string($dbConf['dbname']) . '\' 54 | AND ((data_length + index_length) / 1024 / 1024) > ' . $mysqli->real_escape_string($bigTableSizeThreshold) . ' 55 | ORDER BY (data_length + index_length) DESC 56 | LIMIT 100;'; 57 | $result = $mysqli->query($bigTablesQuery); 58 | if (!$result) { 59 | throw new \RuntimeException('Query error: ' . $mysqli->error); 60 | } 61 | $bigTables = []; 62 | while ($row = $result->fetch_assoc()) { 63 | $bigTables[] = $row; 64 | } 65 | return $bigTables; 66 | } 67 | 68 | public static function getSslCliOptions(array $dbConfig): string 69 | { 70 | $options = []; 71 | 72 | if (isset($dbConfig['flags']) && (int)$dbConfig['flags'] === MYSQLI_CLIENT_SSL) { 73 | $options[] = '--ssl'; 74 | } 75 | 76 | foreach (['ssl_key', 'ssl_cert', 'ssl_ca', 'ssl_capath', 'ssl_cipher'] as $option) { 77 | if (isset($dbConfig[$option])) { 78 | $options[] = '--' . str_replace('_', '-', $option) . '=' . escapeshellarg($dbConfig[$option]); 79 | } 80 | } 81 | 82 | return count($options) ? ' ' . implode(' ', $options) : ''; 83 | } 84 | 85 | public static function getTemporaryMyCnfFile( 86 | array $dbConfig, 87 | string $localInstanceDatabaseStoragePath 88 | ): string { 89 | $tmpMyCnfFile = $localInstanceDatabaseStoragePath . 'tmp_mysql_defaults_file_' . date('YmdHis') . '.cnf'; 90 | $content = "[client]\n"; 91 | $content .= "host=\"{$dbConfig['host']}\"\n"; 92 | $content .= "port=\"" . ((isset($dbConfig['port']) && $dbConfig['port']) ? $dbConfig['port'] : 3306) . "\"\n"; 93 | $content .= "user=\"{$dbConfig['user']}\"\n"; 94 | $content .= "password=\"{$dbConfig['password']}\"\n"; 95 | file_put_contents($tmpMyCnfFile, $content); 96 | 97 | return $tmpMyCnfFile; 98 | } 99 | 100 | public function getDumpFiles( 101 | string $localInstanceDatabaseStoragePath, 102 | array $filters = [], 103 | array $fileTypes = ['sql', 'gz'], 104 | array $sort = [] 105 | ): array { 106 | $dumpFiles = []; 107 | foreach ($fileTypes as $type) { 108 | $dumpFiles = [...$dumpFiles, ...glob($localInstanceDatabaseStoragePath . '*.' . $type)]; 109 | } 110 | if ($dumpFiles) { 111 | $fileUtility = new FileUtility(); 112 | 113 | foreach ($filters as $key => $value) { 114 | $dumpFiles = array_filter($dumpFiles, static function ($file) use ($fileUtility, $key, $value) { 115 | return $fileUtility->getDumpFilenamePart(basename($file), $key) === $value; 116 | }); 117 | } 118 | 119 | if (isset($filters['tags'])) { 120 | $tags = explode('+', $filters['tags']); 121 | $dumpFiles = array_filter($dumpFiles, static function ($file) use ($fileUtility, $tags) { 122 | $fileTags = explode('+', $fileUtility->getDumpFilenamePart(basename($file), 'tags')); 123 | return !array_diff($tags, $fileTags); 124 | }); 125 | } 126 | 127 | if (isset($sort['dateTime'])) { 128 | usort($dumpFiles, static function ($a, $b) use ($fileUtility, $sort) { 129 | $dateTimeA = $fileUtility->getDumpFilenamePart(basename($a), 'dateTime'); 130 | $dateTimeB = $fileUtility->getDumpFilenamePart(basename($b), 'dateTime'); 131 | return $sort['dateTime'] === 'asc' ? $dateTimeA <=> $dateTimeB : $dateTimeB <=> $dateTimeA; 132 | }); 133 | } 134 | } 135 | return $dumpFiles; 136 | } 137 | 138 | public function getDumpFile( 139 | string $localInstanceDatabaseStoragePath, 140 | array $filters = [], 141 | array $fileTypes = ['sql', 'gz'] 142 | ): ?string { 143 | $dumpFiles = $this->getDumpFiles($localInstanceDatabaseStoragePath, $filters, $fileTypes); 144 | return !empty($dumpFiles) ? reset($dumpFiles) : null; 145 | } 146 | 147 | public function getLastDumpFile( 148 | string $localInstanceDatabaseStoragePath, 149 | array $filters = [], 150 | array $fileTypes = ['sql', 'gz'] 151 | ): ?string { 152 | $dumpFiles = $this->getDumpFiles($localInstanceDatabaseStoragePath, $filters, $fileTypes, 153 | ['dateTime' => 'asc']); 154 | return !empty($dumpFiles) ? end($dumpFiles) : null; 155 | } 156 | 157 | public function removeDumpFiles( 158 | string $localInstanceDatabaseStoragePath, 159 | array $filters = [], 160 | array $fileTypes = ['sql', 'gz'], 161 | array $sort = [] 162 | ): void { 163 | $dumpFiles = $this->getDumpFiles($localInstanceDatabaseStoragePath, $filters, $fileTypes, $sort); 164 | foreach ($dumpFiles as $file) { 165 | unlink($file); 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/Utility/FileUtility.php: -------------------------------------------------------------------------------- 1 | parseOptionsString($optionsString); 33 | } 34 | } 35 | 36 | private function parseOptionsString(string $optionsString): void 37 | { 38 | $options = explode(',', $optionsString); 39 | 40 | foreach ($options as $option) { 41 | $optionParts = explode(':', $option); 42 | $optionKey = $optionParts[0]; 43 | $optionValue = count($optionParts) === 2 ? $optionParts[1] : true; 44 | 45 | $this->setOption($optionKey, $optionValue); 46 | } 47 | } 48 | 49 | public function getOption(string $optionName, bool $required = false) 50 | { 51 | $optionReturnValue = null; 52 | foreach ($this->options as $key => $value) { 53 | if ($optionName === $key) { 54 | if (!empty($value)) { 55 | $optionReturnValue = $value; 56 | } else { 57 | $optionReturnValue = null; 58 | } 59 | } 60 | } 61 | 62 | if ($required && $optionReturnValue === null) { 63 | throw new GracefulShutdownException('No `--options=' . $optionName . ':value` set.', 1458937128560); 64 | } 65 | 66 | return in_array($optionName, self::ARRAY_OPTIONS, true) ? (array)$optionReturnValue : $optionReturnValue; 67 | } 68 | 69 | public function setOption(string $optionName, $optionValue): void 70 | { 71 | if (!in_array($optionName, self::AVAILABLE_OPTIONS, true) && strpos($optionName, 'tx') !== 0) { 72 | throw new GracefulShutdownException( 73 | "Option $optionName is not available for '--options='. \nAvailable options are: " 74 | . implode(', ', self::AVAILABLE_OPTIONS) . "\nOr use 'tx' prefix for custom options (e.g. txMyOption)", 75 | 1458937128562 76 | ); 77 | } 78 | 79 | $pregMatchRequired = get('db_pregmatch_' . $optionName, ''); 80 | if ($pregMatchRequired !== '' && !empty($optionValue) && !preg_match($pregMatchRequired, $optionValue)) { 81 | throw new GracefulShutdownException('Value of option \'' . $optionName . '\' does not match the required pattern: ' . $pregMatchRequired, 82 | 1458937128561); 83 | } 84 | 85 | if (in_array($optionName, self::ARRAY_OPTIONS, true)) { 86 | if (is_array($optionValue)) { 87 | $this->options[$optionName] = $optionValue; 88 | } else { 89 | $this->options[$optionName] = explode(self::ARRAY_OPTIONS_IMPLODE_CHAR, $optionValue); 90 | } 91 | } else { 92 | $this->options[$optionName] = $optionValue; 93 | } 94 | } 95 | 96 | public function removeOption(string $optionName): void 97 | { 98 | if (isset($this->options[$optionName])) { 99 | unset($this->options[$optionName]); 100 | } 101 | } 102 | 103 | public function getOptionsString(): string 104 | { 105 | $optionsArray = []; 106 | foreach ($this->options as $key => $value) { 107 | if ($value === true) { 108 | $value = 'true'; 109 | } elseif ($value === false) { 110 | $value = 'false'; 111 | } 112 | if (in_array($key, self::ARRAY_OPTIONS, true) && is_array($value)) { 113 | $value = implode('+', $value); 114 | } 115 | $optionsArray[] = $key . ':' . $value; 116 | } 117 | return '--options=' . implode(',', $optionsArray); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Utility/RsyncUtility.php: -------------------------------------------------------------------------------- 1 | connectionOptionsString(); 12 | if ($connectionOptions !== '') { 13 | return '-e "ssh ' . $connectionOptions . '"'; 14 | } 15 | return ''; 16 | } 17 | 18 | public function getHostWithDbStoragePath(string $targetStageName): string 19 | { 20 | $host = Configuration::getHost($targetStageName); 21 | return 22 | ($host->getRemoteUser() ? $host->getRemoteUser() . '@' : '') . 23 | $host->getHostname() . 24 | ':' . rtrim(Configuration::getHost($targetStageName)->get('db_storage_path'), '/') . '/'; 25 | } 26 | } 27 | --------------------------------------------------------------------------------