diff --git a/README.md b/README.md
index 37194f3..afaf6e0 100644
--- a/README.md
+++ b/README.md
@@ -8,16 +8,23 @@
[![Build Status](https://travis-ci.org/digipolisgent/robo-digipolis-helpers.svg?branch=develop)](https://travis-ci.org/digipolisgent/robo-digipolis-helpers)
[![Maintainability](https://api.codeclimate.com/v1/badges/1c4c5693cb7945f5e5e9/maintainability)](https://codeclimate.com/github/digipolisgent/robo-digipolis-helpers/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/1c4c5693cb7945f5e5e9/test_coverage)](https://codeclimate.com/github/digipolisgent/robo-digipolis-helpers/test_coverage)
-[![PHP 7 ready](https://php7ready.timesplinter.ch/digipolisgent/robo-digipolis-helpers/develop/badge.svg)](https://travis-ci.org/digipolisgent/robo-digipolis-helpers)
-Used by digipolis, abstract robo file to help with the deploy flow.
+Used by digipolis, generic commands/skeleton do execute deploys and syncs between environments.
-By default, we assume a [capistrano-like directory structure](http://capistranorb.com/documentation/getting-started/structure/):
+## Getting started
+
+We make a couple of assumptions, most of which can be overwritten. See
+[default.properties.yml](src/default.properties.yml) for all default values, and
+[the properties.yml documentation](#propertiesyml) for all available
+configuration options.
+
+By default, we assume a [capistrano-like directory structure](http://capistranorb.com/documentation/getting-started/structure/)
+on your servers:
```
-├── current -> releases/20150120114500/
-├── releases
+├── ~/apps/[app]/current -> ~/apps/[app]/releases/20150120114500/
+├── ~/apps/[app]/releases
│ ├── 20150080072500
│ ├── 20150090083000
│ ├── 20150100093500
@@ -25,92 +32,25 @@ By default, we assume a [capistrano-like directory structure](http://capistranor
│ └── 20150120114500
```
-## Example implementation
-
-### RoboFile.php
-
-```php
-collectionBuilder();
- $collection->addTask($this->taskExec('phpcs --standard=PSR2 ./src'));
- return $collection;
- }
-
- /**
- * Detects whether this site is installed or not. This method is used to
- * determine whether we should run `updateTask` (if this returns `true`) or
- * `installTask` (if this returns `false`).
- */
- protected function isSiteInstalled($worker, AbstractAuth $auth, $remote)
- {
- $currentProjectRoot = $this->getCurrentProjectRoot($worker, $auth, $remote);
- $migrateStatus = '';
- return $this->taskSsh($worker, $auth)
- ->remoteDirectory($currentProjectRoot, true)
- ->exec('ls -al | grep index.php')
- ->run()
- ->wasSuccessful();
- }
-
- protected function updateTask($worker, AbstractAuth $auth, $remote)
- {
- $currentProjectRoot = $remote['rootdir'];
- return $this->taskSsh($server, $auth)
- ->remoteDirectory($currentProjectRoot, true)
- ->exec('./update.sh');
- }
-
- protected function installTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $extra = [],
- $force = false
- ) {
- $currentProjectRoot = $remote['rootdir'];
- return $this->taskSsh($server, $auth)
- ->remoteDirectory($currentProjectRoot, true)
- ->exec('./install.sh');
- }
-
- /**
- * Build a my site and push it to the server(s).
- *
- * @param array $arguments
- * Variable amount of arguments. The last argument is the path to the
- * the private key file (ssh), the penultimate is the ssh user. All
- * arguments before that are server IP's to deploy to.
- * @param array $opts
- * The options for this command.
- *
- * @option option1 Description of the first option.
- * @option option2 Description of the second option.
- *
- * @usage --option1=first --option2=2 192.168.1.2 sshuser /home/myuser/.ssh/id_rsa
- */
- public function myDeployCommand(
- array $arguments,
- $opts = ['option1' => 'one', 'option2' => 'two']
- ) {
- return $this->deployTask($arguments, $opts);
- }
-}
-```
-
-If you place this in `RoboFile.php` in your project root, you'll be able to run
-`vendor/bin/robo my:deploy-command --option1=1 --option2=2 192.168.1.2 sshuser /home/myuser/.ssh/id_rsa`
-to release your website. The script will automatically detect whether it should
-update your site or do a fresh install, based on your implementation of
-`isSiteInstalled`. Note that this command can only run after the `composer install`
-command completed successfully (without any errors).
+This package provides a couple of commands. You can use `vendor/bin/robo list`
+and `vendor/bin/robo help [command]` to find out what they do. Most importantly
+these commands follow a "skeleton", in which each step of the command fires an
+event, and the event listeners return an
+[EventHandlerWithPriority](src/EventHandler/EventHandlerWithPriority). The
+default event listeners provided by this package are in the
+[DigipolisHelpersDefaultHooksCommands](src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands)
+class. Each method of that class is an event listener, and returns an event
+handler. The default handlers provided by this package can be found in
+[src/EventHandler/DefaultHandler](src/EventHandler/DefaultHandler). If you want
+to overwrite or alter the behavior of a certain step in the command, all you
+have to do is
+[create an event listener by using the on-event hook](https://github.com/consolidation/annotated-command#on-event-hook)
+for the right event, and let it return your custom handler. Handlers are
+executed in order of priority (lower numbers executed first), the priority of
+default handlers is 999. If your handler calls `$event->stopPropagation()` in
+its `handle` method, handlers that come after it, won't get executed. For
+further information, see the
+[list of available events](#list-of-available-events);
### properties.yml
@@ -122,7 +62,7 @@ Below is an example of some sensible defaults:
```YAML
remote:
# The application directory where your capistrano folder structure resides.
- appdir: '/home/[user]'
+ appdir: '/home/[user]/apps/[app]'
# The releases directory where to deploy new releases to.
releasesdir: '${remote.appdir}/releases'
# The root directory of a new release.
@@ -215,7 +155,7 @@ timeouts:
restore_db_backup: 60
# Before a files backup is restored, the current files are removed. This is
# the timeout for removing those files.
- pre_restore_remove_files: 300
+ pre_restore: 300
# See ${remote.cleandir_limit}. This is the timeout for that operation.
clean_dir: 30
```
@@ -225,7 +165,451 @@ following notation: `${path.to.property}`. There are also other tokens
available:
```
-[user] The ssh user we used to connect to the server.
-[time] New releases are put in a folder with the current timestamp as folder
- name. This is that timestamp.
+[user] The ssh user we used to connect to the server.
+[private-key] The path to the private key that was used to connect to the
+ server.
+[time] New releases are put in a folder with the current timestamp as
+ folder name. This is that timestamp.
+[app] The name of the app that is being deployed.
```
+
+### List of available events
+
+Event arguments can be retrieved with `$event->getArgument($argumentName);`
+
+#### digipolis:backup-remote
+
+The handler for this event should return a task that creates a backup on a
+host, based on options that are passed.
+
+*Default handler*: [BackupRemoteHandler](src/EventHandler/DefaultHandler/BackupRemoteHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to create a backup.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not to create a backup of the files.
+ - data (bool): Whether or not to create a backup of the database.
+ - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - backup_files: Timeout in seconds for the files backup.
+ - backup_database: Timeout in second for the database backup.
+
+### digipolis:build-task
+
+The handler for this event should return a task that creates a release archive
+of the current codebase to upload to an environment.
+
+*Default handler*: [BuildTaskHandler](src/EventHandler/DefaultHandler/BuildTaskHandler.php)
+*Event arguments*:
+ - archiveName: The name of the archive that should be created.
+
+### digipolis:clean-dirs
+
+The handler for this event should return a task that cleans the releases
+directory by removing the older releases.
+
+*Default handler*: [CleanDirsHandler](src/EventHandler/DefaultHandler/CleanDirsHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to clean the releases
+ directory.
+
+### digipolis:clear-cache
+
+The handler for this event should return a task that clears the cache on the
+remote host.
+
+*Default handler*: [ClearCacheHandler](src/EventHandler/DefaultHandler/ClearCacheHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to clear the cache.
+
+### digipolis:clear-remote-opcache
+
+The handler for this event should return a task that clears the opcache on the
+remote host.
+
+*Default handler*: [ClearRemoteOpcacheHandler](src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to clear the opcache.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - clear_op_cache: Timeout in seconds for clearing the opcache.
+
+### digipolis:compress-old-release
+
+The handler for this event should return a task that compresses old releases on
+the host for the given app.
+
+*Default handler*: [CompressOldReleaseHandler](src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to compress the old
+ releases.
+ - releaseToCompress: The path to the release directory that should be
+ compressed.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - compress_old_release: Timeout in seconds for compressing the release.
+
+### digipolis:current-project-root
+
+The handler for this event should return the path to the current project root
+for the given app on the given host. This means the actual path, not a task that
+will return it when executed.
+
+*Default handler*: [CurrentProjectRootHandler](src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php)
+*Event arguments*:
+ - host: The host on which to get the project root.
+ - user: The SSH user to connect to the host.
+ - privateKeyFile: The path to the private key to use to connect to the host.
+ - remoteSettings: The remote settings for the given host and app as parsed
+ from `properties.yml`.
+
+### digipolis:download-backup
+
+The handler for this event should return a task that downloads a backup of an
+app from a host.
+
+*Default handler*: [DownloadBackupHandler](src/EventHandler/DefaultHandler/DownloadBackupHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to download a backup.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not a backup of the files was created.
+ - data (bool): Whether or not a backup of the database was created.
+
+### digipolis:install
+
+The handler for this event should return a task that executes the install script
+on the host.
+
+*Default handler*: [InstallHandler](src/EventHandler/DefaultHandler/InstallHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app we're going to install.
+ - options: Options passed from the command to the install task.
+ - force: Boolean indicating whether or not to force the install, even if there
+ already is an installation.
+
+### digipolis:is-site-installed
+
+The handler for this event should return a boolean indicating whether or not
+there already is an active installation of the app on the host. This means the
+actual boolean, not a task that will return it when executed. This helps us to
+determine whether the install or the update script should be ran when deploying
+the app.
+
+*Default handler*: [IsSiteInstalledHandler](src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app we're checking.
+
+### digipolis:get-local-settings
+
+The handler for this event should return the settings for the local installation
+of the app as parsed from `properties.yml`.
+
+*Default handler*: [LocalSettingsHandler](src/EventHandler/DefaultHandler/LocalSettingsHandler.php)
+*Event arguments*:
+ - app: The name of the app.
+ - timestamp: The current timestamp (sometimes used as token in paths).
+
+#### digipolis:mirror-dir
+
+The handler for this event should return a task that mirrors everything (files,
+symlink, subdirectories, ...) from one directory to another.
+
+*Default Handler*: [MirrorDirHandler](src/EventHandler/DefaultHandler/MirrorDirHandler.php)
+*Event arguments*:
+ - dir: The directory to mirror.
+ - destination: The destination path to mirror the directory to.
+
+### digipolis:post-symlink
+
+The handler for this event should return a task that will be executed after
+creating the symlinks (as parsed from `properties.yml`) on the remote host.
+
+*Default Handler*: [PostSymlinkHandler](src/EventHandler/DefaultHandler/PostSymlinkHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - post_symlink: Timeout in seconds for the post symlink tasks.
+
+### digipolis:pre-local-sync-files
+
+The handler for this event should return a task that should be executed before
+syncing files from a remote installation to your local installation.
+
+*Default Handler*: [PreLocalSyncFilesHandler](src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - localSettings: the settings for the local installation of the app as parsed
+ from `properties.yml`.
+
+### digipolis:pre-restore-backup-remote
+
+The handler for this event should return a task that should be executed before
+restoring a backup on a host.
+
+*Default Handler*: [PreRestoreBackupRemoteHandler](src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not a backup of the files was created.
+ - data (bool): Whether or not a backup of the database was created.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - pre_restore: Timeout in seconds for the pre restore task.
+
+### digipolis:pre-symlink
+
+The handler for this event should return a task that should be executed before
+the symlinks on the remote host are created.
+
+*Default Handler*: [PreSymlinkHandler](src/EventHandler/DefaultHandler/PreSymlinkHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - pre_symlink: Timeout in seconds for the pre symlink task.
+
+### digipolis:push-package
+
+The handler for this event should return a task that pushes a release archive to
+a host.
+
+*Default Handler*: [PushPackageHandler](src/EventHandler/DefaultHandler/PushPackageHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - archiveName: The name of the archive that should be pushed.
+
+### digipolis:realpath
+
+The handler for this event should return the `realpath` of the given path. This
+means the actual path, not a task that will return it when executed. The default
+handler supports replacing `~` (tilde) with the user's homedir.
+
+*Default handler*: [RealpathHandler](src/EventHandler/DefaultHandler/RealpathHandler.php)
+*Event arguments*:
+ - path: The path to get the real path for.
+
+### digipolis:get-remote-settings
+
+The handler for this event should return the settings for the remote
+installation of the app as parsed from `properties.yml`. This means the actual
+settings, not a task that will return it when executed.
+
+*Default handler*: [RemoteSettingsHandler](src/EventHandler/DefaultHandler/RemoteSettingsHandler.php)
+*Event arguments*:
+ - servers: An array of servers (can be one, or multiple for loadbalanced
+ setups) where the app resides.
+ - user: The SSH user to connect to the servers.
+ - privateKeyFile: The path to the private key to use to connect to the
+ servers.
+ - app: The name of the app.
+ - timestamp: The current timestamp (sometimes used as token in paths).
+
+### digipolis:remote-switch-previous
+
+The handler for this event should return a task that will switch the `current`
+symlink to the previous release (mostly used on rollback of a failed release).
+
+*Default Handler*: [RemoteSwitchPreviousHandler](src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+
+### digipolis:remote-symlink
+
+The handler for this event should return a task that will create the symlinks as
+defined in `properties.yml`.
+
+*Default Handler*: [RemoteSymlinkPreviousHandler](src/EventHandler/DefaultHandler/RemoteSymlinkPreviousHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+
+### digipolis:remove-backup-remote
+
+The handler for this event should return a task that removes a backup from the
+host.
+
+*Default Handler*: [RemoveBackupRemoteHandler](src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not a backup of the files was created.
+ - data (bool): Whether or not a backup of the database was created.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - remove_backup: Timeout in seconds for the pre symlink task.
+
+### digipolis:remove-failed-release
+
+The handler for this event should return a task that removes a failed release
+from the host.
+
+*Default Handler*: [RemoveFailedReleaseHandler](src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - releaseDir: The release directory to remove.
+
+### digipolis:remove-local-backup
+
+The handler for this event should return a task that removes a backup from your
+local machine.
+
+*Default Handler*: [RemoveLocalBackupHandler](src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not a backup of the files was created.
+ - data (bool): Whether or not a backup of the database was created.
+ - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+
+### digipolis:restore-backup-db-local
+
+The handler for this event should return a task that restores a database backup
+on your local machine.
+
+*Default Handler*: [RestoreBackupDbLocalHandler](src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - localSettings: the settings for the local installation of the app as parsed
+ from `properties.yml`.
+
+### digipolis:restore-backup-files-local
+
+The handler for this event should return a task that restores a files backup on
+your local machine.
+
+*Default Handler*: [RestoreBackupFilesLocalHandler](src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - localSettings: the settings for the local installation of the app as parsed
+ from `properties.yml`.
+
+### digipolis:restore-backup-remote
+
+The handler for this event should return a task that restores a backup on a
+host.
+
+*Default Handler*: [RestoreBackupRemoteHandler](src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not to create a backup of the files.
+ - data (bool): Whether or not to create a backup of the database.
+ - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - restore_files_backup: Timeout in seconds for the files backup.
+ - restore_db_backup: Timeout in second for the database backup.
+
+### digipolis:rsync-files-between-hosts
+
+The handler for this event should return a task that rsyncs files between two
+hosts.
+
+*Default Handler*: [RsyncFilesBetweenHostsHandler](src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php)
+*Event arguments*:
+ - sourceRemoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object
+ with data relevant to the source host and app.
+ - destinationRemoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php)
+ object with data relevant to the destination host and app.
+ - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+ - timeouts: SSH timeouts for relevant tasks. An array with keys:
+ - synctask_rsync: Timeout in seconds for the rsync.
+
+### digipolis:rsync-files-to-local
+
+The handler for this event should return a task that rsyncs files to your local
+machine.
+
+*Default Handler*: [RsyncFilesToLocalHandler](src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app.
+ - localSettings: the settings for the local installation of the app as parsed
+ from `properties.yml`.
+ - directory: The subdirectory under the `$remoteSettings['filesdir']` that
+ should be synced.
+ - - fileBackupConfig: Configuration for the file backup. An array with keys:
+ - exclude_from_backup: Files and/or directories to exclude from the backup.
+ - file_backup_subdirs: The subdirectories of the files directory that need
+ to be backed up.
+
+### digipolis:switch-previous
+
+The handler for this event should return a task that will switch the `current`
+symlink to the previous release (mostly used on rollback of a failed release).
+The difference with the (digipolis:remote-switch-previous)[#digipolis-remote-switch-previous]
+event is that this will be executed directly on the host, and thus doesn't need
+an ssh connection, while the (digipolis:remote-switch-previous)[#digipolis-remote-switch-previous]
+will be executed from your deployment server, or your local machine, and thus
+will need an ssh connection to the host.
+
+*Default Handler*: [SwitchPreviousHandler](src/EventHandler/DefaultHandler/SwitchPreviousHandler.php)
+*Event arguments*:
+ - releasesDir: The directory containing all your releases.
+ - currentSymlink: The path to your `current` symlink.
+
+### digipolis:timeout-setting
+
+The handler for this event should return the the timeout setting of the given
+type in seconds. This means the actual setting, not a task that will return it
+when executed.
+
+*Default handler*: [TimeoutSettingHandler](src/EventHandler/DefaultHandler/TimeoutSettingHandler.php)
+*Event arguments*:
+ - type: The type of timeout setting to get. See timeout event arguments for
+ the other events.
+
+### digipolis:update
+
+The handler for this event should return a task that executes the update script
+on the host.
+
+*Default handler*: [UpdateHandler](src/EventHandler/DefaultHandler/UpdateHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app we're going to update.
+ - options: Options passed from the command to the update task.
+ - force: Boolean indicating whether or not to force the install, even if there
+ already is an installation.
+
+### digipolis:upload-backup
+
+The handler for this event should return a task that uploads a backup of an
+app to a host.
+
+*Default handler*: [UploadBackupHandler](src/EventHandler/DefaultHandler/UploadBackupHandler.php)
+*Event arguments*:
+ - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data
+ relevant to the host and app of which we're going to download a backup.
+ - options: Options for the backup. An array with keys:
+ - files (bool): Whether or not a backup of the files was created.
+ - data (bool): Whether or not a backup of the database was created.
diff --git a/composer.json b/composer.json
index 77ef0c9..bda9f47 100644
--- a/composer.json
+++ b/composer.json
@@ -10,7 +10,7 @@
"digipolisgent/robo-digipolis-general": "^2.0",
"digipolisgent/command-builder": "^1.2.1",
"roave/better-reflection": "^5.0",
- "consolidation/annotated-command": "^4, <=4.5.6"
+ "symfony/event-dispatcher": "^6.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5.20"
diff --git a/src/DependencyInjection/AppTaskFactoryAwareInterface.php b/src/DependencyInjection/AppTaskFactoryAwareInterface.php
deleted file mode 100644
index 6f4de73..0000000
--- a/src/DependencyInjection/AppTaskFactoryAwareInterface.php
+++ /dev/null
@@ -1,10 +0,0 @@
-getContainer();
- $container->addShared('digipolis.time', time());
- $container->addShared(PropertiesHelper::class, [PropertiesHelper::class, 'create'])
- ->addArgument($container);
- $container->addShared(RemoteHelper::class, [RemoteHelper::class, 'create'])
- ->addArgument($container);
- $container->addShared(Backup::class, [Backup::class, 'create'])
- ->addArgument($container);
- $container->addShared(Build::class, [Build::class, 'create'])
- ->addArgument($container);
- $container->addShared(Cache::class, [Cache::class, 'create'])
- ->addArgument($container);
- $container->addShared(Deploy::class, [Deploy::class, 'create'])
- ->addArgument($container);
- $container->addShared(Sync::class, [Sync::class, 'create'])
- ->addArgument($container);
- }
-}
diff --git a/src/DependencyInjection/SyncTaskFactoryAwareInterface.php b/src/DependencyInjection/SyncTaskFactoryAwareInterface.php
deleted file mode 100644
index 248ef61..0000000
--- a/src/DependencyInjection/SyncTaskFactoryAwareInterface.php
+++ /dev/null
@@ -1,10 +0,0 @@
-appTaskFactory = $appTaskFactory;
- }
-}
diff --git a/src/DependencyInjection/Traits/BackupTaskFactoryAware.php b/src/DependencyInjection/Traits/BackupTaskFactoryAware.php
deleted file mode 100644
index c62f6df..0000000
--- a/src/DependencyInjection/Traits/BackupTaskFactoryAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-backupTaskFactory = $backupTaskFactory;
- }
-}
diff --git a/src/DependencyInjection/Traits/BuildTaskFactoryAware.php b/src/DependencyInjection/Traits/BuildTaskFactoryAware.php
deleted file mode 100644
index 85adcf5..0000000
--- a/src/DependencyInjection/Traits/BuildTaskFactoryAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-buildTaskFactory = $buildTaskFactory;
- }
-}
diff --git a/src/DependencyInjection/Traits/CacheTaskFactoryAware.php b/src/DependencyInjection/Traits/CacheTaskFactoryAware.php
deleted file mode 100644
index f8f0897..0000000
--- a/src/DependencyInjection/Traits/CacheTaskFactoryAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-cacheTaskFactory = $cacheTaskFactory;
- }
-}
diff --git a/src/DependencyInjection/Traits/DeployTaskFactoryAware.php b/src/DependencyInjection/Traits/DeployTaskFactoryAware.php
deleted file mode 100644
index 422375c..0000000
--- a/src/DependencyInjection/Traits/DeployTaskFactoryAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-deployTaskFactory = $deployTaskFactory;
- }
-}
diff --git a/src/DependencyInjection/Traits/PropertiesHelperAware.php b/src/DependencyInjection/Traits/PropertiesHelperAware.php
deleted file mode 100644
index 1e5c88f..0000000
--- a/src/DependencyInjection/Traits/PropertiesHelperAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-propertiesHelper = $propertiesHelper;
- }
-}
diff --git a/src/DependencyInjection/Traits/RemoteHelperAware.php b/src/DependencyInjection/Traits/RemoteHelperAware.php
deleted file mode 100644
index e18d71d..0000000
--- a/src/DependencyInjection/Traits/RemoteHelperAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-remoteHelper = $remoteHelper;
- }
-}
diff --git a/src/DependencyInjection/Traits/SyncTaskFactoryAware.php b/src/DependencyInjection/Traits/SyncTaskFactoryAware.php
deleted file mode 100644
index f0347ed..0000000
--- a/src/DependencyInjection/Traits/SyncTaskFactoryAware.php
+++ /dev/null
@@ -1,15 +0,0 @@
-syncTaskFactory = $syncTaskFactory;
- }
-}
diff --git a/src/EventHandler/AbstractBackupHandler.php b/src/EventHandler/AbstractBackupHandler.php
new file mode 100644
index 0000000..7b32f48
--- /dev/null
+++ b/src/EventHandler/AbstractBackupHandler.php
@@ -0,0 +1,34 @@
+getTime();
+ }
+ return $timestamp . '_' . date('Y_m_d_H_i_s', $timestamp) . $extension;
+ }
+}
diff --git a/src/EventHandler/AbstractTaskEventHandler.php b/src/EventHandler/AbstractTaskEventHandler.php
new file mode 100644
index 0000000..48a8952
--- /dev/null
+++ b/src/EventHandler/AbstractTaskEventHandler.php
@@ -0,0 +1,21 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+ $fileBackupConfig = $event->getArgument('fileBackupConfig');
+ $timeouts = $event->getArgument('timeouts');
+
+ if (!$options['files'] && !$options['data']) {
+ $options['files'] = true;
+ $options['data'] = true;
+ }
+
+ $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time'];
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $collection = $this->collectionBuilder();
+
+ if ($options['files']) {
+ $collection
+ ->taskRemoteFilesBackup($remoteConfig->getHost(), $auth, $backupDir, $remoteSettings['filesdir'])
+ ->backupFile($this->backupFileName('.tar.gz'))
+ ->excludeFromBackup($fileBackupConfig['exclude_from_backup'])
+ ->backupSubDirs($fileBackupConfig['file_backup_subdirs'])
+ ->timeout($timeouts['backup_files']);
+ }
+
+ if ($options['data']) {
+ $collection
+ ->taskRemoteDatabaseBackup($remoteConfig->getHost(), $auth, $backupDir, $remoteConfig->getCurrentProjectRoot())
+ ->backupFile($this->backupFileName('.sql'))
+ ->timeout($timeouts['backup_database']);
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/BuildTaskHandler.php b/src/EventHandler/DefaultHandler/BuildTaskHandler.php
new file mode 100644
index 0000000..baae4d5
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/BuildTaskHandler.php
@@ -0,0 +1,24 @@
+hasArgument('archiveName') ? $event->getArgument('archiveName') : null;
+ $archive = is_null($archiveName) ? TimeHelper::getInstance()->getTime() . '.tar.gz' : $archiveName;
+
+ return $this->taskPackageProject($archive);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/CleanDirsHandler.php b/src/EventHandler/DefaultHandler/CleanDirsHandler.php
new file mode 100644
index 0000000..8e828e1
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/CleanDirsHandler.php
@@ -0,0 +1,36 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+
+ $cleandirLimit = isset($remoteSettings['cleandir_limit']) ? max(1, $remoteSettings['cleandir_limit']) : '';
+ $collection = $this->collectionBuilder();
+ $collection->taskRemoteCleanDirs($remoteConfig->getHost(), $auth, $remoteSettings['rootdir'], $remoteSettings['releasesdir'], ($cleandirLimit ? ($cleandirLimit + 1) : false));
+
+ if ($remoteSettings['createbackup']) {
+ $collection->taskRemoteCleanDirs($remoteConfig->getHost(), $auth, $remoteSettings['rootdir'], $remoteSettings['backupsdir'], ($cleandirLimit ? ($cleandirLimit) : false));
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/ClearCacheHandler.php b/src/EventHandler/DefaultHandler/ClearCacheHandler.php
new file mode 100644
index 0000000..4892c9e
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/ClearCacheHandler.php
@@ -0,0 +1,19 @@
+collectionBuilder();
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php b/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php
new file mode 100644
index 0000000..6c4766f
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php
@@ -0,0 +1,36 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $timeouts = $event->getArgument('timeouts');
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+
+ $clearOpcache = CommandBuilder::create('vendor/bin/robo digipolis:clear-op-cache')->addArgument($remoteSettings['opcache']['env']);
+ if (isset($remoteSettings['opcache']['host'])) {
+ $clearOpcache->addOption('host', $remoteSettings['opcache']['host']);
+ }
+
+ return $this->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($remoteSettings['rootdir'], true)
+ ->timeout($timeouts['clear_op_cache'])
+ ->exec((string) $clearOpcache);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php b/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php
new file mode 100644
index 0000000..be8836a
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php
@@ -0,0 +1,62 @@
+getArgument(('remoteConfig'));
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $releaseToCompress = $event->getArgument('releaseToCompress');
+ $timeouts = $event->getArgument('timeouts');
+
+ // Strip the releases dir from the release to compress, so the tar
+ // contains relative paths.
+ $relativeReleaseToCompress = str_replace($remoteSettings['releasesdir'] . '/', '', $releaseToCompress);
+
+ return $this->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($remoteSettings['releasesdir'])
+ ->exec((string) CommandBuilder::create('tar')
+ ->addFlag('c')
+ ->addFlag('z')
+ ->addFlag('f', $relativeReleaseToCompress . '.tar.gz')
+ ->addArgument($relativeReleaseToCompress)
+ ->onSuccess(
+ CommandBuilder::create('chown')
+ ->addFlag('R')
+ ->addArgument($remoteConfig->getUser() . ':' . $remoteConfig->getUser())
+ ->addArgument($relativeReleaseToCompress)
+ ->onSuccess(CommandBuilder::create('chmod')
+ ->addFlag('R')
+ ->addArgument('a+rwx')
+ ->addArgument($relativeReleaseToCompress)
+ ->onSuccess(CommandBuilder::create('rm')
+ ->addFlag('rf')
+ ->addArgument($relativeReleaseToCompress)
+ )
+ )
+ )
+ ->onFailure(
+ CommandBuilder::create('rm')
+ ->addFlag('r')
+ ->addFlag('f')
+ ->addArgument($relativeReleaseToCompress . '.tar.gz')
+ )
+ )->timeout($timeouts['compress_old_release']);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php b/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php
new file mode 100644
index 0000000..ba29fdd
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php
@@ -0,0 +1,58 @@
+getArgument('host');
+ $user = $event->getArgument('user');
+ $privateKeyFile = $event->getArgument('privateKeyFile');
+ $remoteSettings = $event->getArgument('remoteSettings');
+ $key = $host . ':' . $user . ':' . $privateKeyFile . ':' . $remoteSettings['releasesdir'];
+
+ $auth = new KeyFile($user, $privateKeyFile);
+ if (!array_key_exists($key, $this->projectRoots)) {
+ $fullOutput = '';
+ $this->taskSsh($host, $auth)
+ ->remoteDirectory($remoteSettings['releasesdir'], true)
+ ->exec(
+ (string) CommandBuilder::create('ls')
+ ->addFlag('1')
+ ->pipeOutputTo(
+ CommandBuilder::create('sort')
+ ->addFlag('r')
+ ->pipeOutputTo(
+ CommandBuilder::create('head')
+ ->addFlag('1')
+ )
+ ),
+ function ($output) use (&$fullOutput) {
+ $fullOutput .= $output;
+ }
+ )
+ ->run();
+ $this->projectRoots[$key] = $remoteSettings['releasesdir'] . '/' . substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput)));
+ }
+
+ return $this->projectRoots[$key];
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/DownloadBackupHandler.php b/src/EventHandler/DefaultHandler/DownloadBackupHandler.php
new file mode 100644
index 0000000..f5ed367
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/DownloadBackupHandler.php
@@ -0,0 +1,48 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+
+ if (!$options['files'] && !$options['data']) {
+ $options['files'] = true;
+ $options['data'] = true;
+ }
+ $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time'];
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $collection = $this->collectionBuilder();
+ $collection
+ ->taskSFTP($remoteConfig->getHost(), $auth);
+
+ // Download files.
+ if ($options['files']) {
+ $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']);
+ $collection->get($backupDir . '/' . $filesBackupFile, $filesBackupFile);
+ }
+
+ // Download data.
+ if ($options['data']) {
+ $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']);
+ $collection->get($backupDir . '/' . $dbBackupFile, $dbBackupFile);
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/InstallHandler.php b/src/EventHandler/DefaultHandler/InstallHandler.php
new file mode 100644
index 0000000..0aeb710
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/InstallHandler.php
@@ -0,0 +1,19 @@
+collectionBuilder();
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php b/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php
new file mode 100644
index 0000000..c333aa3
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php
@@ -0,0 +1,39 @@
+siteInstalled)) {
+ return $this->siteInstalled;
+ }
+
+ /** @var RemoteConfig $remoteConfig */
+ $remoteConfig = $event->getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $currentWebRoot = $remoteSettings['currentdir'];
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $result = $this->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($currentWebRoot, true)
+ ->exec('ls -al | grep index.php')
+ ->run();
+ $this->siteInstalled = $result->wasSuccessful();
+
+ return $this->siteInstalled;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/LocalSettingsHandler.php b/src/EventHandler/DefaultHandler/LocalSettingsHandler.php
new file mode 100644
index 0000000..bb65f81
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/LocalSettingsHandler.php
@@ -0,0 +1,36 @@
+readProperties();
+ $app = $event->getArgument('app');
+ $timestamp = $event->getArgument('timestamp');
+ $defaults = [
+ 'app' => $app,
+ 'time' => is_null($timestamp) ? $this->time : $timestamp,
+ 'project_root' => $this->getConfig()->get('digipolis.root.project'),
+ 'web_root' => $this->getConfig()->get('digipolis.root.web'),
+ 'filesdir' => 'files',
+ ];
+
+ // Set up destination config.
+ $replacements = array(
+ '[project_root]' => $this->getConfig()->get('digipolis.root.project'),
+ '[web_root]' => $this->getConfig()->get('digipolis.root.web'),
+ '[app]' => $app,
+ '[time]' => is_null($timestamp) ? $this->time : $timestamp,
+ );
+
+ return ($this->tokenReplace($this->getConfig()->get('local'), $replacements) ?? []) + $defaults;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/MirrorDirHandler.php b/src/EventHandler/DefaultHandler/MirrorDirHandler.php
new file mode 100644
index 0000000..574649c
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/MirrorDirHandler.php
@@ -0,0 +1,45 @@
+getArgument('dir');
+ $destination = $event->getArgument('destination');
+ if (!is_dir($dir)) {
+ return $this->collectionBuilder();
+ }
+ $task = $this->taskFilesystemStack();
+ $task->mkdir($destination);
+
+ $directoryIterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
+ $recursiveIterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST);
+ foreach ($recursiveIterator as $item) {
+ $destinationFile = $destination . '/' . $recursiveIterator->getSubPathName();
+ if (file_exists($destinationFile)) {
+ continue;
+ }
+ if (is_link($item)) {
+ if ($item->getRealPath() !== false) {
+ $task->symlink($item->getLinkTarget(), $destinationFile);
+ }
+ continue;
+ }
+ if ($item->isDir()) {
+ $task->mkdir($destinationFile);
+ continue;
+ }
+ $task->copy($item, $destinationFile);
+ }
+ return $task;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/PostSymlinkHandler.php b/src/EventHandler/DefaultHandler/PostSymlinkHandler.php
new file mode 100644
index 0000000..b6d9612
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/PostSymlinkHandler.php
@@ -0,0 +1,52 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $timeouts = $event->getArgument('timeouts');
+
+ $collection = $this->collectionBuilder();
+ if (isset($remoteSettings['postsymlink_filechecks']) && $remoteSettings['postsymlink_filechecks']) {
+ $projectRoot = $remoteSettings['rootdir'];
+ $collection->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($projectRoot, true)
+ ->timeout($timeouts['post_symlink']);
+ foreach ($remoteSettings['postsymlink_filechecks'] as $file) {
+ // If this command fails, the collection will fail, which will
+ // trigger a rollback.
+ $builder = CommandBuilder::create('ls')
+ ->addArgument($file)
+ ->pipeOutputTo('grep')
+ ->addArgument($file)
+ ->onFailure(
+ CommandBuilder::create('echo')
+ ->addArgument('[ERROR] ' . $file . ' was not found.')
+ ->onFinished('exit')
+ ->addArgument('1')
+ );
+ $collection->exec((string) $builder);
+ }
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php b/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php
new file mode 100644
index 0000000..0a121cb
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php
@@ -0,0 +1,34 @@
+getArgument('localSettings');
+
+ return $this
+ ->taskExecStack()
+ ->exec(
+ (string) CommandBuilder::create('chown')
+ ->addFlag('R')
+ ->addRawArgument('$USER')
+ ->addArgument(dirname($localSettings['filesdir']))
+ )
+ ->exec(
+ (string) CommandBuilder::create('chmod')
+ ->addFlag('R')
+ ->addArgument('u+w')
+ ->addArgument(dirname($localSettings['filesdir']))
+ );
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php
new file mode 100644
index 0000000..5430b39
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php
@@ -0,0 +1,55 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $timeouts = $event->getArgument('timeouts');
+ $fileBackupConfig = $event->getArgument('fileBackupConfig');
+
+ if (!$options['files'] && !$options['data']) {
+ $options['files'] = true;
+ $options['data'] = true;
+ }
+ if ($options['files']) {
+ $removeFiles = CommandBuilder::create('rm')->addFlag('rf');
+ if (!$fileBackupConfig['file_backup_subdirs']) {
+ $removeFiles->addArgument('./*');
+ $removeFiles->addArgument('./.??*');
+ }
+ foreach ($fileBackupConfig['file_backup_subdirs'] as $subdir) {
+ $removeFiles->addArgument($subdir . '/*');
+ $removeFiles->addArgument($subdir . '/.??*');
+ }
+
+ return $this->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($remoteSettings['filesdir'], true)
+ // Files dir can be pretty big on large sites.
+ ->timeout($timeouts['pre_restore'])
+ ->exec((string) $removeFiles);
+ }
+
+ return $this->collectionBuilder();
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/PreSymlinkHandler.php b/src/EventHandler/DefaultHandler/PreSymlinkHandler.php
new file mode 100644
index 0000000..c3bc89b
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/PreSymlinkHandler.php
@@ -0,0 +1,79 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $timeouts = $event->getArgument('timeouts');
+
+ $collection = $this->collectionBuilder();
+ foreach ($remoteSettings['symlinks'] as $symlink) {
+ $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($remoteConfig, $symlink, $timeouts['pre_symlink']);
+ if ($preIndividualSymlinkTask) {
+ $collection->addTask($preIndividualSymlinkTask);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Tasks to execute before creating an individual symlink.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param string $symlink
+ * The symlink in format "target:link".
+ * @param int $timeout
+ * The SSH timeout in seconds.
+ *
+ * @return bool|\Robo\Contract\TaskInterface
+ * The presymlink task, false if no pre symlink task needs to run.
+ */
+ public function preIndividualSymlinkTask(RemoteConfig $remoteConfig, $symlink, $timeout)
+ {
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $projectRoot = $remoteSettings['rootdir'];
+ $task = $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->remoteDirectory($projectRoot, true)
+ ->timeout($timeout);
+ list($target, $link) = explode(':', $symlink);
+ if ($link === $remoteSettings['currentdir']) {
+ return false;
+ }
+ // If the link we're going to create is an existing directory,
+ // mirror that directory on the symlink target and then delete it
+ // before creating the symlink
+ $task->exec(
+ (string) CommandBuilder::create('vendor/bin/robo digipolis:mirror-dir')
+ ->addArgument($link)
+ ->addArgument($target)
+ );
+ $task->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('rf')
+ ->addArgument($link)
+ );
+
+ return $task;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/PushPackageHandler.php b/src/EventHandler/DefaultHandler/PushPackageHandler.php
new file mode 100644
index 0000000..ef17afe
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/PushPackageHandler.php
@@ -0,0 +1,44 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ $archive = $event->hasArgument('archiveName') && $event->getArgument('archiveName')
+ ? $event->getArgument('archiveName')
+ : $remoteSettings['time'] . '.tar.gz';
+ $releaseDir = $remoteSettings['releasesdir'] . '/' . $remoteSettings['time'];
+
+ $collection = $this->collectionBuilder();
+ $collection->taskPushPackage($remoteConfig->getHost(), $auth)
+ ->destinationFolder($releaseDir)
+ ->package($archive);
+
+ $collection->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($releaseDir, true)
+ ->exec((string) CommandBuilder::create('chmod')
+ ->addArgument('u+rx')
+ ->addArgument('vendor/bin/robo')
+ );
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RealpathHandler.php b/src/EventHandler/DefaultHandler/RealpathHandler.php
new file mode 100644
index 0000000..385c38b
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RealpathHandler.php
@@ -0,0 +1,18 @@
+getArgument('path'));
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php b/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php
new file mode 100644
index 0000000..531f585
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php
@@ -0,0 +1,54 @@
+readProperties();
+ $user = $event->getArgument('user');
+ $servers = $event->getArgument('servers');
+ $privateKeyFile = $event->getArgument('privateKeyFile');
+ $app = $event->getArgument('app');
+ $timestamp = $event->getArgument('timestamp');
+ $defaults = [
+ 'user' => $user,
+ 'private-key' => $privateKeyFile,
+ 'app' => $app,
+ 'createbackup' => true,
+ 'time' => $timestamp,
+ 'filesdir' => 'files',
+ ];
+
+ // Set up destination config.
+ $replacements = array(
+ '[user]' => $user,
+ '[private-key]' => $privateKeyFile,
+ '[app]' => $app,
+ '[time]' => $timestamp,
+ );
+ if (is_array($servers)) {
+ foreach ($servers as $key => $server) {
+ $replacements['[server-' . $key . ']'] = $server;
+ $defaults['server-' . $key] = $server;
+ }
+ }
+
+ $settings = $this->processEnvironmentOverrides(
+ ($this->tokenReplace($this->getConfig()->get('remote'), $replacements) ?? []) + $defaults
+ );
+
+ // Reverse the symlinks so the `current` symlink is the last one to be
+ // created.
+ $settings['symlinks'] = array_reverse($settings['symlinks'] ?? [], true);
+
+ return $settings;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php b/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php
new file mode 100644
index 0000000..f0764dc
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php
@@ -0,0 +1,33 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+
+ return $this->taskRemoteSwitchPrevious(
+ $remoteConfig->getHost(),
+ $auth,
+ $remoteConfig->getCurrentProjectRoot(),
+ $remoteSettings['releasesdir'],
+ $remoteSettings['currentdir']
+ );
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php b/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php
new file mode 100644
index 0000000..9a7d40d
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php
@@ -0,0 +1,45 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $timeouts = $event->getArgument('timeouts');
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+
+ $collection = $this->collectionBuilder();
+ foreach ($remoteSettings['symlinks'] as $link) {
+ $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($remoteConfig, $link, $timeouts['symlink']);
+ if ($preIndividualSymlinkTask) {
+ $collection->addTask($preIndividualSymlinkTask);
+ }
+ list($target, $linkname) = explode(':', $link);
+ $collection->taskSsh($remoteConfig->getHost(), $auth)
+ ->exec(
+ (string) CommandBuilder::create('ln')
+ ->addFlag('s')
+ ->addFlag('T')
+ ->addFlag('f')
+ ->addArgument($target)
+ ->addArgument($linkname)
+ );
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php
new file mode 100644
index 0000000..f6fc79a
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php
@@ -0,0 +1,40 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $timeouts = $event->getArgument('timeouts');
+ $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time'];
+
+ $collection = $this->collectionBuilder();
+ $collection->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->timeout($timeouts['remove_backup'])
+ ->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('rf')
+ ->addArgument($backupDir)
+ );
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php b/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php
new file mode 100644
index 0000000..35dacd5
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php
@@ -0,0 +1,32 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $auth = new KeyFile($remoteConfig->getHost(), $remoteConfig->getPrivateKeyFile());
+ $releaseDir = $event->hasArgument('releaseDir')
+ ? $event->getArgument('releaseDir')
+ : $remoteSettings['releasesdir'] . '/' . $remoteSettings['time'];
+
+ return $this->taskRemoteRemoveRelease($remoteConfig->getHost(), $auth, null, $releaseDir);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php b/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php
new file mode 100644
index 0000000..ea649d4
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php
@@ -0,0 +1,32 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+ $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']);
+ $removeLocalBackup = CommandBuilder::create('rm')
+ ->addFlag('f')
+ ->addArgument($dbBackupFile);
+ if ($options['files']) {
+ $removeLocalBackup->addArgument($this->backupFileName('.tar.gz', $remoteSettings['time']));
+ }
+
+ return $this->taskExecStack()->exec((string) $removeLocalBackup);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php
new file mode 100644
index 0000000..ba89638
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php
@@ -0,0 +1,41 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']);
+ $dbRestore = CommandBuilder::create('vendor/bin/robo digipolis:database-restore')->addOption('source', $dbBackupFile);
+ $cwd = getcwd();
+
+ return $this->taskExecStack()
+ ->exec(
+ (string) CommandBuilder::create('cd')
+ ->addArgument($this->getConfig()->get('digipolis.root.project'))
+ ->onSuccess($dbRestore)
+ )
+ ->exec(
+ (string) CommandBuilder::create('cd')
+ ->addArgument($cwd)
+ ->onSuccess(
+ CommandBuilder::create('rm')
+ ->addFlag('rf')
+ ->addArgument($dbBackupFile)
+ )
+ );
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php
new file mode 100644
index 0000000..cd34ffc
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php
@@ -0,0 +1,43 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $localSettings = $event->getArgument('localSettings');
+ $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']);
+
+ return $this->taskExecStack()
+ ->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('rf')
+ ->addArgument($localSettings['filesdir'] . '/*')
+ ->addArgument($localSettings['filesdir'] . '/.??*')
+ )
+ ->exec(
+ (string) CommandBuilder::create('tar')
+ ->addFlag('xkz')
+ ->addFlag('f', $filesBackupFile)
+ ->addFlag('C', $localSettings['filesdir'])
+ )
+ ->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('f')
+ ->addArgument($filesBackupFile)
+ );
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php
new file mode 100644
index 0000000..b361d65
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php
@@ -0,0 +1,66 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+ $timeouts = $event->getArgument('timeouts');
+
+ if (!$options['files'] && !$options['data']) {
+ $options['files'] = true;
+ $options['data'] = true;
+ }
+
+ $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time'];
+
+ $collection = $this->collectionBuilder();
+
+ if ($options['files']) {
+ $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']);
+ $collection
+ ->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->remoteDirectory($remoteSettings['filesdir'], true)
+ ->timeout($timeouts['restore_files_backup'])
+ ->exec(
+ (string) CommandBuilder::create('tar')
+ ->addFlag('xkz')
+ ->addFlag('f', $backupDir . '/' . $filesBackupFile)
+ );
+ }
+
+ // Restore the db backup.
+ if ($options['data']) {
+ $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']);
+ $collection
+ ->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->remoteDirectory($remoteConfig->getCurrentProjectRoot(), true)
+ ->timeout($timeouts['restore_db_backup'])
+ ->exec(
+ (string) CommandBuilder::create('vendor/bin/robo digipolis:database-restore')
+ ->addOption('source', $backupDir . '/' . $dbBackupFile)
+ );
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php b/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php
new file mode 100644
index 0000000..c23bbac
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php
@@ -0,0 +1,278 @@
+getArgument('sourceRemoteConfig');
+ $destinationRemoteConfig = $event->getArgument('destinationRemoteConfig');
+ $fileBackupConfig = $event->getArgument('fileBackupConfig');
+ $timeouts = $event->getArgument('timeouts');
+
+ $tmpPrivateKeyFile = '~/.ssh/' . uniqid('robo_', true) . '.id_rsa';
+ $collection = $this->collectionBuilder();
+ // Generate a temporary key.
+ $collection->addTask(
+ $this->generateKeyPair($tmpPrivateKeyFile)
+ );
+
+ $collection->completion(
+ $this->removeKeyPair($tmpPrivateKeyFile)
+ );
+
+ // Install it on the destination host.
+ $collection->addTask(
+ $this->installPublicKeyOnDestination(
+ $tmpPrivateKeyFile,
+ $destinationRemoteConfig
+ )
+ );
+
+ // Remove it from the destination host when we're done.
+ $collection->completion(
+ $this->removePublicKeyFromDestination(
+ $tmpPrivateKeyFile,
+ $destinationRemoteConfig
+ )
+ );
+
+ // Install the private key on the source host.
+ $collection->addTask(
+ $this->installPrivateKeyOnSource(
+ $tmpPrivateKeyFile,
+ $sourceRemoteConfig
+ )
+ );
+
+ // Remove the private key from the source host.
+ $collection->completion(
+ $this->removePrivateKeyFromSource(
+ $tmpPrivateKeyFile,
+ $sourceRemoteConfig
+ )
+ );
+
+ $dirs = ($fileBackupConfig['file_backup_subdirs'] ? $fileBackupConfig['file_backup_subdirs'] : ['']);
+
+ foreach ($dirs as $dir) {
+ $dir .= ($dir !== '' ? '/' : '');
+ $collection->addTask(
+ $this->rsyncDirectory(
+ $dir,
+ $tmpPrivateKeyFile,
+ $sourceRemoteConfig,
+ $destinationRemoteConfig,
+ $fileBackupConfig,
+ $timeouts['synctask_rsync']
+ )
+ );
+ }
+
+ return $collection;
+ }
+
+
+ /**
+ * Generate an SSH key pair.
+ *
+ * @param string $privateKeyFile
+ * Path to store the private key file.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function generateKeyPair($privateKeyFile)
+ {
+ return $this->taskExec(
+ (string) CommandBuilder::create('ssh-keygen')
+ ->addFlag('q')
+ ->addFlag('t', 'rsa')
+ ->addFlag('b', 4096)
+ ->addRawFlag('N', '""')
+ ->addRawFlag('f', $privateKeyFile)
+ ->addFlag('C', 'robo:' . md5($privateKeyFile))
+ );
+ }
+
+ /**
+ * Remove an SSH key pair.
+ *
+ * @param string $privateKeyFile
+ * Path to store the private key file.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removeKeyPair($privateKeyFile)
+ {
+ return $this->taskExecStack()
+ ->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('f')
+ ->addRawArgument($privateKeyFile)
+ ->addRawArgument($privateKeyFile . '.pub')
+ );
+ }
+
+ /**
+ * Install a public SSH key on a host.
+ *
+ * @param string $privateKeyFile
+ * Path to the private key file of the key pair to install.
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function installPublicKeyOnDestination($privateKeyFile, RemoteConfig $remoteConfig)
+ {
+ return $this->taskExec(
+ (string) CommandBuilder::create('cat')
+ ->addRawArgument($privateKeyFile . '.pub')
+ ->pipeOutputTo(
+ CommandBuilder::create('ssh')
+ ->addArgument($remoteConfig->getUser() . '@' . $remoteConfig->getHost())
+ ->addFlag('o', 'StrictHostKeyChecking=no')
+ ->addRawFlag('i', $remoteConfig->getPrivateKeyFile())
+ )
+ ->addArgument(
+ CommandBuilder::create('mkdir')
+ ->addFlag('p')
+ ->addRawArgument('~/.ssh')
+ ->onSuccess(
+ CommandBuilder::create('cat')
+ ->chain('~/.ssh/authorized_keys', '>>')
+ )
+ )
+ );
+ }
+
+ /**
+ * Remove a public key from a host.
+ *
+ * @param string $privateKeyFile
+ * Path to the private key file of the key pair to remove.
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removePublicKeyFromDestination($privateKeyFile, RemoteConfig $remoteConfig)
+ {
+ return $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->exec(
+ (string) CommandBuilder::create('sed')
+ ->addFlag('i', '/robo:' . md5($privateKeyFile) . '/d')
+ ->addRawArgument('~/.ssh/authorized_keys')
+ );
+ }
+
+ /**
+ * Install a private key on a host.
+ *
+ * @param string $privateKeyFile
+ * Private key to install.
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function installPrivateKeyOnSource($privateKeyFile, RemoteConfig $remoteConfig)
+ {
+ return $this->taskRsync()
+ ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $remoteConfig->getPrivateKeyFile() . '`"')
+ ->fromPath($privateKeyFile)
+ ->toHost($remoteConfig->getHost())
+ ->toUser($remoteConfig->getUser())
+ ->toPath('~/.ssh')
+ ->archive()
+ ->compress()
+ ->checksum()
+ ->wholeFile();
+ }
+
+ /**
+ * Remove a private key from a host.
+ *
+ * @param string $privateKeyFile
+ * Path to the private key file of the key pair to remove.
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removePrivateKeyFromSource($privateKeyFile, RemoteConfig $remoteConfig)
+ {
+ return $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()))
+ ->exec(
+ (string) CommandBuilder::create('rm')
+ ->addFlag('f')
+ ->addRawArgument($privateKeyFile)
+ );
+ }
+
+ /**
+ * Rsync a directory between hosts.
+ *
+ * @param string $directory
+ * The directory to sync.
+ * @param string $privateKeyFile
+ * The path the the private key of the keypair installed on src and dest.
+ * @param RemoteConfig $sourceRemoteConfig
+ * RemoteConfig object populated with data relevant to the source.
+ * @param RemoteConfig $destinationRemoteConfig
+ * RemoteConfig object populated with data relevant to the destination.
+ * @param array $fileBackupConfig
+ * File backup config.
+ * @param int $timeout
+ * Timeout setting for the sync.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function rsyncDirectory(
+ $directory,
+ $privateKeyFile,
+ RemoteConfig $sourceRemoteConfig,
+ RemoteConfig $destinationRemoteConfig,
+ $fileBackupConfig,
+ $timeout
+ ) {
+ $sourceRemoteSettings = $sourceRemoteConfig->getRemoteSettings();
+ $destinationRemoteSettings = $destinationRemoteConfig->getRemoteSettings();
+ $rsync = $this->taskRsync()
+ ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `cd -P ' . $sourceRemoteConfig->getCurrentProjectRoot() . ' && vendor/bin/robo digipolis:realpath ' . $privateKeyFile . '`"')
+ ->fromPath($sourceRemoteSettings['filesdir'] . '/' . $directory)
+ ->toHost($destinationRemoteConfig->getHost())
+ ->toUser($destinationRemoteConfig->getUser())
+ ->toPath($destinationRemoteSettings['filesdir'] . '/' . $directory)
+ ->archive()
+ ->delete()
+ ->rawArg('--copy-links --keep-dirlinks')
+ ->compress()
+ ->checksum()
+ ->wholeFile();
+ foreach ($fileBackupConfig['exclude_from_backup'] as $exclude) {
+ $rsync->exclude($exclude);
+ }
+
+ return $this->taskSsh($sourceRemoteConfig->getHost(), new KeyFile($sourceRemoteConfig->getUser(), $sourceRemoteConfig->getPrivateKeyFile()))
+ ->timeout($timeout)
+ ->exec($rsync);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php b/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php
new file mode 100644
index 0000000..2f4df15
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php
@@ -0,0 +1,43 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $localSettings = $event->getArgument('localSettings');
+ $directory = $event->getArgument('directory');
+ $fileBackupConfig = $event->getArgument('fileBackupConfig');
+
+ $rsync = $this->taskRsync()
+ ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $remoteConfig->getPrivateKeyFile() . '`"')
+ ->fromHost($remoteConfig->getHost())
+ ->fromUser($remoteConfig->getUser())
+ ->fromPath($remoteSettings['filesdir'] . '/' . $directory)
+ ->toPath($localSettings['filesdir'] . '/' . $directory)
+ ->archive()
+ ->delete()
+ ->rawArg('--copy-links --keep-dirlinks')
+ ->compress()
+ ->checksum()
+ ->wholeFile();
+
+ foreach ($fileBackupConfig['exclude_from_backup'] as $exclude) {
+ $rsync->exclude($exclude);
+ }
+
+ return $rsync;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/SettingsHandler.php b/src/EventHandler/DefaultHandler/SettingsHandler.php
new file mode 100644
index 0000000..c6b26bc
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/SettingsHandler.php
@@ -0,0 +1,110 @@
+ 'HOSTNAME',
+ 'environment_matcher' => '\\DigipolisGent\\Robo\\Helpers\\Util\\EnvironmentMatcher::regexMatch',
+ ];
+
+ /**
+ * Process environment-specific overrides.
+ *
+ * @param array $settings
+ * @return array
+ */
+ protected function processEnvironmentOverrides($settings)
+ {
+ $settings += static::$defaultEnvironmentOverrideSettings;
+ if (!isset($settings['environment_overrides']) || !$settings['environment_overrides']) {
+ return $settings;
+ }
+
+ $server = $this->getFirstServer($settings);
+ if (!$server) {
+ return $settings;
+ }
+
+ // Parse the env var on the server.
+ $auth = new KeyFile($settings['user'], $settings['private-key']);
+ $fullOutput = '';
+ $this->taskSsh($server, $auth)
+ ->exec(
+ (string) CommandBuilder::create('echo')
+ ->addRawArgument('$' . $settings['environment_env_var']),
+ function ($output) use (&$fullOutput) {
+ $fullOutput .= $output;
+ }
+ )
+ ->run();
+ $envVarValue = substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput)));
+ foreach ($settings['environment_overrides'] as $environmentMatch => $overrides) {
+ if (call_user_func($settings['environment_matcher'], $environmentMatch, $envVarValue)) {
+ $settings = ArrayMerger::doMerge($settings, $overrides);
+ }
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Get the first server entry from the remote settings.
+ *
+ * @param array $settings
+ *
+ * @return string|bool
+ * First server if found, false otherwise.
+ *
+ * @see self::processEnvironmentOverrides
+ */
+ protected function getFirstServer($settings)
+ {
+ foreach ($settings as $key => $value) {
+ if (preg_match('/^server/', $key) === 1) {
+ return $value;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper functions to replace tokens in an array.
+ *
+ * @param string|array $input
+ * The array or string containing the tokens to replace.
+ * @param array $replacements
+ * The token replacements.
+ *
+ * @return string|array
+ * The input with the tokens replaced with their values.
+ */
+ protected function tokenReplace($input, $replacements)
+ {
+ if (is_string($input)) {
+ return strtr($input, $replacements);
+ }
+ if (is_scalar($input) || empty($input)) {
+ return $input;
+ }
+ foreach ($input as &$i) {
+ $i = $this->tokenReplace($i, $replacements);
+ }
+
+ return $input;
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php b/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php
new file mode 100644
index 0000000..c23ae60
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php
@@ -0,0 +1,23 @@
+getArgument('releasesDir');
+ $currentSymlink = $event->getArgument('currentSymlink');
+
+ return $this->taskSwitchPrevious($releasesDir, $currentSymlink);
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php b/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php
new file mode 100644
index 0000000..a4338f0
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php
@@ -0,0 +1,74 @@
+getArgument('type');
+ $timeoutSettings = $this->getTimeoutSettings();
+ return isset($timeoutSettings[$type]) ? $timeoutSettings[$type] : static::DEFAULT_TIMEOUT;
+ }
+
+ /**
+ * Timeouts can be overwritten in properties.yml under the `timeout` key.
+ *
+ * @param string $setting
+ *
+ * @return int
+ */
+ public function getTimeoutSetting($setting)
+ {
+ $timeoutSettings = $this->getTimeoutSettings();
+ return isset($timeoutSettings[$setting]) ? $timeoutSettings[$setting] : static::DEFAULT_TIMEOUT;
+ }
+
+ /**
+ * Get all timeout settings.
+ *
+ * @return array
+ */
+ protected function getTimeoutSettings()
+ {
+ $this->readProperties();
+ return $this->getConfig()->get('timeouts', []) + $this->getDefaultTimeoutSettings();
+ }
+
+ /**
+ * Get the default timeout settings.
+ *
+ * @return array
+ */
+ protected function getDefaultTimeoutSettings()
+ {
+ // Refactor this to default.properties.yml
+ return [
+ 'presymlink_mirror_dir' => 60,
+ 'synctask_rsync' => 1800,
+ 'backup_files' => 300,
+ 'backup_database' => 300,
+ 'remove_backup' => 300,
+ 'restore_files_backup' => 300,
+ 'restore_db_backup' => 60,
+ 'pre_restore' => 300,
+ 'clean_dir' => 30,
+ 'clear_op_cache' => 30,
+ 'compress_old_release' => 300,
+ ];
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/UpdateHandler.php b/src/EventHandler/DefaultHandler/UpdateHandler.php
new file mode 100644
index 0000000..cf059c0
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/UpdateHandler.php
@@ -0,0 +1,19 @@
+collectionBuilder();
+ }
+}
diff --git a/src/EventHandler/DefaultHandler/UploadBackupHandler.php b/src/EventHandler/DefaultHandler/UploadBackupHandler.php
new file mode 100644
index 0000000..f8e9fe0
--- /dev/null
+++ b/src/EventHandler/DefaultHandler/UploadBackupHandler.php
@@ -0,0 +1,48 @@
+getArgument('remoteConfig');
+ $remoteSettings = $remoteConfig->getRemoteSettings();
+ $options = $event->getArgument('options');
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+
+ if (!$options['files'] && !$options['data']) {
+ $options['files'] = true;
+ $options['data'] = true;
+ }
+ $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time'];
+ $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']);
+ $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']);
+
+ $collection = $this->collectionBuilder();
+ $collection
+ ->taskSsh($remoteConfig->getHost(), $auth)
+ ->exec((string) CommandBuilder::create('mkdir')->addFlag('p')->addArgument($backupDir))
+ ->taskSFTP($remoteConfig->getHost(), $auth);
+ if ($options['files']) {
+ $collection->put($backupDir . '/' . $filesBackupFile, $filesBackupFile);
+ }
+ if ($options['data']) {
+ $collection->put($backupDir . '/' . $dbBackupFile, $dbBackupFile);
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/EventHandler/EventHandlerWithPriority.php b/src/EventHandler/EventHandlerWithPriority.php
new file mode 100644
index 0000000..c94dc35
--- /dev/null
+++ b/src/EventHandler/EventHandlerWithPriority.php
@@ -0,0 +1,27 @@
+addShared(AbstractApp::class, [$this->getAppTaskFactoryClass(), 'create'])->addArgument($container);
- $container->addServiceProvider(new ServiceProvider());
-
- // Inject all our dependencies.
- $this->setRemoteHelper($container->get(RemoteHelper::class));
- $this->setBackupTaskFactory($container->get(Backup::class));
-
- return $this;
- }
-
- abstract public function getAppTaskFactoryClass();
-
- /**
- * @return FilesystemStack
- */
- protected function taskFilesystemStack()
- {
- return $this->task(FilesystemStack::class);
- }
-
- /**
- * Mirror a directory.
- *
- * @param string $dir
- * Path of the directory to mirror.
- * @param string $destination
- * Path of the directory where $dir should be mirrored.
- *
- * @return \Robo\Contract\TaskInterface
- * The mirror dir task.
- *
- * @command digipolis:mirror-dir
- */
- public function digipolisMirrorDir(ConsoleIO $io, $dir, $destination)
- {
- if (!is_dir($dir)) {
- return;
- }
- $task = $this->taskFilesystemStack();
- $task->mkdir($destination);
-
- $directoryIterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
- $recursiveIterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST);
- foreach ($recursiveIterator as $item) {
- $destinationFile = $destination . '/' . $recursiveIterator->getSubPathName();
- if (file_exists($destinationFile)) {
- continue;
- }
- if (is_link($item)) {
- if ($item->getRealPath() !== false) {
- $task->symlink($item->getLinkTarget(), $destinationFile);
- }
- continue;
- }
- if ($item->isDir()) {
- $task->mkdir($destinationFile);
- continue;
- }
- $task->copy($item, $destinationFile);
- }
- return $task;
- }
-
- /**
- * Polyfill for realpath.
- *
- * @param string $path
- *
- * @return string
- *
- * @command digipolis:realpath
- */
- public function digipolisRealpath($path)
- {
- return Path::realpath($path);
- }
-
- /**
- * Switch the current release symlink to the previous release.
- *
- * @param string $releasesDir
- * Path to the folder containing all releases.
- * @param string $currentSymlink
- * Path to the current release symlink.
- *
- * @command digipolis:switch-previous
- */
- public function digipolisSwitchPrevious($releasesDir, $currentSymlink)
- {
- return $this->taskSwitchPrevious($releasesDir, $currentSymlink);
- }
-
- /**
- * Sync the database and files to your local environment.
- *
- * @param string $host
- * IP address of the source server.
- * @param string $user
- * SSH user to connect to the source server.
- * @param string $keyFile
- * Private key file to use to connect to the source server.
- * @param array $opts
- * Command options
- *
- * @option app The name of the app we're syncing.
- * @option files Sync only files.
- * @option data Sync only the database.
- * @option rsync Sync the files via rsync.
- *
- * @return \Robo\Contract\TaskInterface
- * The sync task.
- */
- public function digipolisSyncLocal(
- $host,
- $user,
- $keyFile,
- $opts = [
- 'app' => 'default',
- 'files' => false,
- 'data' => false,
- 'rsync' => true,
- ]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
-
- $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync'];
-
- $remote = $this->remoteHelper->getRemoteSettings($host, $user, $keyFile, $opts['app']);
- $local = $this->remoteHelper->getLocalSettings($opts['app']);
- $auth = new KeyFile($user, $keyFile);
- $collection = $this->collectionBuilder();
-
- if ($opts['files']) {
- $collection
- ->taskExecStack()
- ->exec(
- (string) CommandBuilder::create('chown')
- ->addFlag('R')
- ->addRawArgument('$USER')
- ->addArgument(dirname($local['filesdir']))
- )
- ->exec(
- (string) CommandBuilder::create('chmod')
- ->addFlag('R')
- ->addArgument('u+w')
- ->addArgument(dirname($local['filesdir']))
- );
-
- if ($opts['rsync']) {
- $opts['files'] = false;
-
- $backupConfig = $this->getBackupConfig();
- $dirs = ($backupConfig['file_backup_subdirs'] ? $backupConfig['file_backup_subdirs'] : ['']);
-
- foreach ($dirs as $dir) {
- $dir .= ($dir !== '' ? '/' : '');
-
- $rsync = $this->taskRsync()
- ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $keyFile . '`"')
- ->fromHost($host)
- ->fromUser($user)
- ->fromPath($remote['filesdir'] . '/' . $dir)
- ->toPath($local['filesdir'] . '/' . $dir)
- ->archive()
- ->delete()
- ->rawArg('--copy-links --keep-dirlinks')
- ->compress()
- ->checksum()
- ->wholeFile();
-
- $backupConfig = $this->getBackupConfig();
- foreach ($backupConfig['exclude_from_backup'] as $exclude) {
- $rsync->exclude($exclude);
- }
-
- $collection->addTask($rsync);
- }
- }
- }
-
- if ($opts['data'] || $opts['files']) {
- // Create a backup.
- $collection->addTask(
- $this->backupTaskFactory->backupTask(
- $host,
- $auth,
- $remote,
- $opts
- )
- );
- // Download the backup.
- $collection->addTask(
- $this->backupTaskFactory->downloadBackupTask(
- $host,
- $auth,
- $remote,
- $opts
- )
- );
- }
-
- if ($opts['files']) {
- // Restore the files backup.
- $filesBackupFile = $this->backupTaskFactory->backupFileName('.tar.gz', $remote['time']);
- $collection
- ->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('rf')
- ->addArgument($local['filesdir'] . '/*')
- ->addArgument($local['filesdir'] . '/.??*')
- )
- ->exec(
- (string) CommandBuilder::create('tar')
- ->addFlag('xkz')
- ->addFlag('f', $filesBackupFile)
- ->addFlag('C', $local['filesdir'])
- )
- ->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('f')
- ->addArgument($filesBackupFile)
- );
- }
-
- if ($opts['data']) {
- // Restore the db backup.
- $dbBackupFile = $this->backupTaskFactory->backupFileName('.sql.gz', $remote['time']);
- $dbRestore = CommandBuilder::create('vendor/bin/robo digipolis:database-restore')->addOption('source', $dbBackupFile);
- $cwd = getcwd();
-
- $collection->taskExecStack();
- $collection->exec(
- (string) CommandBuilder::create('cd')
- ->addArgument($this->getConfig()->get('digipolis.root.project'))
- ->onSuccess($dbRestore)
- );
- $collection->exec(
- (string) CommandBuilder::create('cd')
- ->addArgument($cwd)
- ->onSuccess(
- CommandBuilder::create('rm')
- ->addFlag('rf')
- ->addArgument($dbBackupFile)
- )
- );
- }
-
- return $collection;
- }
-}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php b/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php
new file mode 100644
index 0000000..f632e99
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php
@@ -0,0 +1,361 @@
+ false,
+ 'worker' => null,
+ 'app' => 'default',
+ ]
+ ) {
+ return $this->deploy($arguments, $opts);
+ }
+}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php
new file mode 100644
index 0000000..7343f58
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php
@@ -0,0 +1,35 @@
+handleTaskEvent(
+ 'digipolis:mirror-dir',
+ ['dir' => $dir, 'destination' => $destination]
+ );
+ }
+}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php
new file mode 100644
index 0000000..9807329
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php
@@ -0,0 +1,33 @@
+handleEvent(
+ 'digipolis:realpath',
+ ['path' => $path]
+ );
+
+ return reset($results);
+ }
+}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php
new file mode 100644
index 0000000..c8be5f6
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php
@@ -0,0 +1,32 @@
+handleTaskEvent(
+ 'digipolis:switch-previous',
+ ['releasesDir' => $releasesDir, 'currentSymlink' => $currentSymlink]
+ );
+ }
+}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php
new file mode 100644
index 0000000..c138b3d
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php
@@ -0,0 +1,67 @@
+ false, 'data' => false, 'rsync' => true]
+ ) {
+ return $this->sync(
+ $sourceUser,
+ $sourceHost,
+ $sourcePrivateKeyFile,
+ $destinationUser,
+ $destinationHost,
+ $destinationPrivateKeyFile,
+ $sourceApp,
+ $destinationApp,
+ $opts
+ );
+ }
+}
diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php
new file mode 100644
index 0000000..dcc6645
--- /dev/null
+++ b/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php
@@ -0,0 +1,129 @@
+ 'default',
+ 'files' => false,
+ 'data' => false,
+ 'rsync' => true,
+ ]
+ ) {
+ if (!$opts['files'] && !$opts['data']) {
+ $opts['files'] = true;
+ $opts['data'] = true;
+ }
+
+ $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync'];
+
+ $remoteSettings = $this->getRemoteSettings($host, $user, $privateKeyFile, $opts['app']);
+ $currentProjectRoot = $this->getCurrentProjectRoot($host, $user, $privateKeyFile, $remoteSettings);
+ $remoteConfig = new RemoteConfig($host, $user, $privateKeyFile, $remoteSettings, $currentProjectRoot);
+ $localSettings = $this->getLocalSettings($opts['app']);
+ $collection = $this->collectionBuilder();
+
+ if ($opts['files']) {
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:pre-local-sync-files',
+ [
+ 'localSettings' => $localSettings,
+ 'remoteConfig' => $remoteConfig,
+ ]
+ ));
+
+ $fileBackupConfig = $this->getFileBackupConfig();
+ if ($opts['rsync']) {
+ $opts['files'] = false;
+ $dirs = ($fileBackupConfig['file_backup_subdirs'] ? $fileBackupConfig['file_backup_subdirs'] : ['']);
+
+ foreach ($dirs as $dir) {
+ $dir .= ($dir !== '' ? '/' : '');
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:rsync-files-to-local',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'localSettings' => $localSettings,
+ 'directory' => $dir,
+ 'fileBackupConfig' => $fileBackupConfig,
+ ]
+ ));
+ }
+ }
+ }
+
+ if ($opts['data'] || $opts['files']) {
+ // Create the backup on the server.
+ $collection->addTask($this->backupRemoteTask($remoteConfig, $opts));
+
+ // Download the backup.
+ $collection->addTask($this->downloadBackupTask($remoteConfig, $opts));
+ }
+
+ if ($opts['files']) {
+ // Restore the files backup.
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:restore-backup-files-local',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'localSettings' => $localSettings,
+ ]
+ ));
+
+ }
+
+ if ($opts['data']) {
+ // Restore the db backup.
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:restore-backup-db-local',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'localSettings' => $localSettings,
+ ]
+ ));
+ }
+
+ return $collection;
+ }
+}
diff --git a/src/Robo/Plugin/Tasks/Remote.php b/src/Robo/Plugin/Tasks/Remote.php
index 9e0ab33..757b69a 100644
--- a/src/Robo/Plugin/Tasks/Remote.php
+++ b/src/Robo/Plugin/Tasks/Remote.php
@@ -10,6 +10,7 @@
abstract class Remote extends BaseTask implements BuilderAwareInterface
{
use \Robo\Common\BuilderAwareTrait;
+
/**
* The SSH host.
*
diff --git a/src/RoboFile.php b/src/RoboFile.php
new file mode 100644
index 0000000..cb4b743
--- /dev/null
+++ b/src/RoboFile.php
@@ -0,0 +1,5 @@
+handleTaskEvent(
+ 'digipolis:backup-remote',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'fileBackupConfig' => $this->getFileBackupConfig(),
+ 'options' => $backupOpts,
+ 'timeouts' => [
+ 'backup_files' => $this->getTimeoutSetting('backup_files'),
+ 'backup_database' => $this->getTimeoutSetting('backup_database'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will execute tasks before restoring a backup.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $backupOpts
+ * Extra options for restoring the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function preRestoreBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:pre-restore-backup-remote',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'fileBackupConfig' => $this->getFileBackupConfig(),
+ 'options' => $backupOpts,
+ 'timeouts' => [
+ 'pre_restore' => $this->getTimeoutSetting('pre_restore'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will restore a backup.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $backupOpts
+ * Extra options for restoring the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function restoreBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:restore-backup-remote',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'fileBackupConfig' => $this->getFileBackupConfig(),
+ 'options' => $backupOpts,
+ 'timeouts' => [
+ 'restore_files_backup' => $this->getTimeoutSetting('restore_files_backup'),
+ 'restore_db_backup' => $this->getTimeoutSetting('restore_db_backup'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will download a backup from a host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $backupOpts
+ * Extra options that were used for creating the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function downloadBackupTask(RemoteConfig $remoteConfig, $backupOpts)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:download-backup',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'options' => $backupOpts,
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will upload a backup to a host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $backupOpts
+ * Extra options that were used for creating the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function uploadBackupTask(RemoteConfig $remoteConfig, $backupOpts)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:upload-backup',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'options' => $backupOpts,
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will remove a backup from a host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $backupOpts
+ * Extra options that were used for creating the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removeBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:remove-backup-remote',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'options' => $backupOpts,
+ 'timeouts' => [
+ 'remove_backup' => $this->getTimeoutSetting('remove_backup'),
+ ],
+ ]
+ );
+ }
+}
diff --git a/src/Traits/DigipolisHelpersCommandUtilities.php b/src/Traits/DigipolisHelpersCommandUtilities.php
new file mode 100644
index 0000000..8acace1
--- /dev/null
+++ b/src/Traits/DigipolisHelpersCommandUtilities.php
@@ -0,0 +1,224 @@
+getTime() : $timestamp;
+ $servers = (array) $servers;
+ $serversCopy = $servers;
+ sort($serversCopy);
+ $serversKey = implode('_', $serversCopy);
+ $cacheKeyParts = [$serversKey, $user, $privateKeyFile, $app, $timestamp];
+ $cacheKey = implode(':', $cacheKeyParts);
+ if (!isset($this->remoteSettingsCache[$cacheKey])) {
+ $results = $this->handleEvent(
+ 'digipolis:get-remote-settings',
+ [
+ 'servers' => $servers,
+ 'user' => $user,
+ 'privateKeyFile' => $privateKeyFile,
+ 'app' => $app,
+ 'timestamp' => $timestamp,
+ ]
+ );
+ $settings = array_shift($results);
+ while ($results) {
+ $settings = ArrayMerger::doMerge($settings, array_shift($results));
+ }
+ $this->remoteSettingsCache[$cacheKey] = $settings;
+ }
+
+ return $this->remoteSettingsCache[$cacheKey];
+ }
+
+ /**
+ * Get the settings from the 'local' config key, with the tokens replaced.
+ *
+ * @param string $app
+ * The name of the app these settings apply to.
+ * @param string|null $timestamp
+ * The timestamp to use. Defaults to the request time.
+ *
+ * @return array
+ * The settings for the local environment and app.
+ */
+ protected function getLocalSettings($app, $timestamp = null)
+ {
+ $timestamp = is_null($timestamp) ? TimeHelper::getInstance()->getTime() : $timestamp;
+ $cacheKey = $app . ':' . $timestamp;
+ if (!isset($this->localSettingsCache[$cacheKey])) {
+ $results = $this->handleEvent(
+ 'digipolis:get-local-settings',
+ [
+ 'app' => $app,
+ 'timestamp' => $timestamp,
+ ]
+ );
+ $settings = array_shift($results);
+ while ($results) {
+ $settings = ArrayMerger::doMerge($settings, array_shift($results));
+ }
+ $this->localSettingsCache[$cacheKey] = $settings;
+ }
+
+ return $this->localSettingsCache[$cacheKey];
+ }
+
+ /**
+ * Gets the config for file backups.
+ *
+ * @return array
+ * The backup config with keys file_backup_subdirs and exclude_from_backup
+ */
+ protected function getFileBackupConfig()
+ {
+ $configs = $this->handleEvent('digipolis:file-backup-config', []);
+ $config = [
+ 'file_backup_subdirs' => [],
+ 'exclude_from_backup' => [],
+ ];
+ while ($configs) {
+ $config = ArrayMerger::doMerge($config, array_shift($configs));
+ }
+
+ return $config;
+ }
+
+ /**
+ * Get an ssh timeout setting.
+ *
+ * @param string $type
+ * The type to get the setting for.
+ *
+ * @return int
+ * The timeout in seconds.
+ */
+ protected function getTimeoutSetting($type)
+ {
+ $settings = $this->handleEvent('digipolis:timeout-setting', ['type' => $type]);
+ return max($settings);
+ }
+
+ /**
+ * Get the project root of the current release on the host.
+ *
+ * @param string $host
+ * The host ip.
+ * @param string $user
+ * The ssh user.
+ * @param string $privateKeyFile
+ * The path to the private ssh key.
+ * @param array $remoteSettings
+ * The remote settings as returned by static::getRemoteSettings().
+ *
+ * @return string
+ * The path to the project root on the server.
+ */
+ protected function getCurrentProjectRoot($host, $user, $privateKeyFile, $remoteSettings)
+ {
+ $results = $this->handleEvent(
+ 'digipolis:current-project-root',
+ [
+ 'host' => $host,
+ 'user' => $user,
+ 'privateKeyFile' => $privateKeyFile,
+ 'remoteSettings' => $remoteSettings,
+ ]
+ );
+
+ return reset($results);
+ }
+
+ /**
+ * Check if a site is already installed
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return bool
+ * Whether or not the site is installed.
+ */
+ protected function isSiteInstalled(RemoteConfig $remoteConfig)
+ {
+ $results = $this->handleEvent(
+ 'digipolis:is-site-installed',
+ [
+ 'remoteConfig' => $remoteConfig,
+ ]
+ );
+
+ return reset($results);
+ }
+
+ /**
+ * Check if the current release has robo available.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return bool
+ */
+ protected function currentReleaseHasRobo(RemoteConfig $remoteConfig)
+ {
+ $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile());
+ return $this->taskSsh($remoteConfig->getHost(), $auth)
+ ->remoteDirectory($remoteConfig->getCurrentProjectRoot(), true)
+ ->exec(
+ (string) CommandBuilder::create('ls')
+ ->addArgument('vendor/bin/robo')
+ ->pipeOutputTo(
+ CommandBuilder::create('grep')
+ ->addArgument('robo')
+ )
+ )
+ ->run()
+ ->wasSuccessful();
+ }
+
+ /**
+ * Get the task that will clear the cache on the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function clearCacheTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:clear-cache',
+ [
+ 'remoteConfig' => $remoteConfig,
+ ]
+ );
+ }
+}
diff --git a/src/Traits/DigipolisHelpersDeployCommandUtilities.php b/src/Traits/DigipolisHelpersDeployCommandUtilities.php
new file mode 100644
index 0000000..7bfa606
--- /dev/null
+++ b/src/Traits/DigipolisHelpersDeployCommandUtilities.php
@@ -0,0 +1,371 @@
+ false,
+ 'worker' => null,
+ 'app' => 'default',
+ ]
+ ) {
+ // Define variables.
+ $opts += ['force-install' => false];
+ $privateKeyFile = array_pop($arguments);
+ $user = array_pop($arguments);
+ $servers = $arguments;
+ $worker = is_null($opts['worker']) ? reset($servers) : $opts['worker'];
+ $remoteSettings = $this->getRemoteSettings($servers, $user, $privateKeyFile, $opts['app']);
+ $workerCurrentProjectRoot = $this->getCurrentProjectRoot($worker, $user, $privateKeyFile, $remoteSettings);
+ $releaseDir = $remoteSettings['releasesdir'] . '/' . $remoteSettings['time'];
+ $archive = $remoteSettings['time'] . '.tar.gz';
+ $backupOpts = ['files' => false, 'data' => true];
+ $workerRemoteConfig = new RemoteConfig($worker, $user, $privateKeyFile, $remoteSettings, $workerCurrentProjectRoot);
+
+ $collection = $this->collectionBuilder();
+
+ // Build the archive to deploy.
+ $collection->addTask($this->buildTask($archive));
+
+ // Create a backup and a rollback task if a site is already installed.
+ if (
+ $remoteSettings['createbackup']
+ && $this->isSiteInstalled($workerRemoteConfig)
+ && $this->currentReleaseHasRobo($workerRemoteConfig)
+ ) {
+ // Create a backup.
+ $collection->addTask($this->backupRemoteTask($workerRemoteConfig, $backupOpts));
+
+ // Create a rollback for this backup for when the deploy fails.
+ $collection->rollback($this->preRestoreBackupRemoteTask($workerRemoteConfig, $backupOpts));
+ $collection->rollback($this->restoreBackupRemoteTask($workerRemoteConfig, $backupOpts));
+ }
+
+ // Push the package to the servers and create the required symlinks.
+ foreach ($servers as $server) {
+ $serverProjectRoot = $this->getCurrentProjectRoot($server, $user, $privateKeyFile, $remoteSettings);
+ $serverRemoteConfig = new RemoteConfig($server, $user, $privateKeyFile, $remoteSettings, $serverProjectRoot);
+ // Remove this release on rollback.
+ $collection->rollback($this->removeFailedReleaseTask($serverRemoteConfig, $releaseDir));
+
+ // Clear opcache (if present) on rollback.
+ if (isset($remoteSettings['opcache']) && (!array_key_exists('clear', $remoteSettings['opcache']) || $remoteSettings['opcache']['clear'])) {
+ $collection->rollback($this->clearRemoteOpcacheTask($serverRemoteConfig));
+ }
+
+ // Push the package.
+ $collection->addTask($this->pushPackageTask($serverRemoteConfig, $archive));
+
+ // Add any tasks to execute before creating the symlinks.
+ $collection->addTask($this->preSymlinkTask($serverRemoteConfig));
+
+ // Switch the current symlink to the previous release on rollback.
+ $collection->rollback($this->remoteSwitchPreviousTask($serverRemoteConfig));
+
+ // Create the symlinks.
+ $collection->addTask($this->remoteSymlinksTask($serverRemoteConfig));
+
+ // Add any tasks to execute after creating the symlinks.
+ $collection->addTask($this->postSymlinkTask($serverRemoteConfig));
+ }
+
+ // Initialize the site (update or install).
+ $collection->addTask($this->initRemoteTask($workerRemoteConfig, $opts, $opts['force-install']));
+
+ // Clear cache after update or install.
+ $collection->addTask($this->clearCacheTask($workerRemoteConfig));
+
+ foreach ($servers as $server) {
+ $serverProjectRoot = $this->getCurrentProjectRoot($server, $user, $privateKeyFile, $remoteSettings);
+ $serverRemoteConfig = new RemoteConfig($server, $user, $privateKeyFile, $remoteSettings, $serverProjectRoot);
+ // Clear OPcache if present.
+ if (isset($remoteSettings['opcache']) && (!array_key_exists('clear', $remoteSettings['opcache']) || $remoteSettings['opcache']['clear'])) {
+ $collection->addTask($this->clearRemoteOpcacheTask($serverRemoteConfig));
+ }
+ // Compress old releases if configured.
+ if (isset($remoteSettings['compress_old_releases']) && $remoteSettings['compress_old_releases']) {
+ $collection->addTask($this->compressOldReleaseTask($serverRemoteConfig));
+ }
+ // Clean release and backup dirs on the servers.
+ $collection->completion($this->cleanDirsTask($serverRemoteConfig));
+ }
+
+ // Clear the site's cache on rollback too.
+ $collection->completion($this->clearCacheTask($workerRemoteConfig));
+
+ return $collection;
+ }
+
+ /**
+ * Get the task that will create a release archive.
+ *
+ * @param string $archiveName
+ * The name of the archive that will be created.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function buildTask($archiveName)
+ {
+ return $this->handleTaskEvent('digipolis:build-task', ['archiveName' => $archiveName]);
+ }
+
+ /**
+ * Get the task that will remove a failed release from the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param string $releaseDir
+ * The release directory to remove.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removeFailedReleaseTask(RemoteConfig $remoteConfig, $releaseDir)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:remove-failed-release',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'releaseDir' => $releaseDir,
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will clear opcache on a host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function clearRemoteOpcacheTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:clear-remote-opcache',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'timeouts' => [
+ 'clear_op_cache' => $this->getTimeoutSetting('clear_op_cache'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will push a release archive to a host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param string $archiveName
+ * The path to the archive to push.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function pushPackageTask(RemoteConfig $remoteConfig, $archiveName)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:push-package',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'archiveName' => $archiveName,
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will execute presymlink tasks.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function preSymlinkTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:pre-symlink',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'timeouts' => [
+ 'pre_symlink' => $this->getTimeoutSetting('pre_symlink'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will switch to the previous release on the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function remoteSwitchPreviousTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:remote-switch-previous',
+ [
+ 'remoteConfig' => $remoteConfig,
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will create the configured symlinks on the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return string
+ */
+ protected function remoteSymlinksTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:remote-symlink',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'timeouts' => [
+ 'symlink' => $this->getTimeoutSetting('symlink'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will execute postsymlink tasks on the host
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function postSymlinkTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:post-symlink',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'timeouts' => [
+ 'post_symlink' => $this->getTimeoutSetting('post_symlink'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will install or update a site on the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $options
+ * Extra parameters to pass to site install.
+ * @param bool $force
+ * Whether or not to force the install even when the site is present.
+ *
+ * @return \Robo\Contract\TaskInterface
+ * The init remote task.
+ */
+ protected function initRemoteTask(RemoteConfig $remoteConfig, $options = [], $force = false)
+ {
+ $collection = $this->collectionBuilder();
+ if (!$this->isSiteInstalled($remoteConfig) || $force) {
+ $this->say($force ? 'Forcing site install.' : 'Site status failed.');
+ $this->say('Triggering install script.');
+
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:install',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'options'=> $options,
+ 'force' => $force,
+ ]
+ ));
+
+ return $collection;
+ }
+ $collection->addTask($this->handleTaskEvent(
+ 'digipolis:update',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'options'=> $options,
+ 'force' => $force,
+ ]
+ ));
+
+ return $collection;
+ }
+
+ /**
+ * Get the task that will compress an old release on the host.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return type
+ */
+ protected function compressOldReleaseTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:compress-old-release',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'releaseToCompress' => $remoteConfig->getCurrentProjectRoot(),
+ 'timeouts' => [
+ 'compress_old_release' => $this->getTimeoutSetting('compress_old_release'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the task that will clean the directories (remove old releases).
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function cleanDirsTask(RemoteConfig $remoteConfig)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:clean-dirs',
+ [
+ 'remoteConfig' => $remoteConfig,
+ ]
+ );
+ }
+}
diff --git a/src/Traits/DigipolisHelpersSyncCommandUtilities.php b/src/Traits/DigipolisHelpersSyncCommandUtilities.php
new file mode 100644
index 0000000..9f8cf85
--- /dev/null
+++ b/src/Traits/DigipolisHelpersSyncCommandUtilities.php
@@ -0,0 +1,172 @@
+ false, 'data' => false, 'rsync' => true]
+ ) {
+ if (!$opts['files'] && !$opts['data']) {
+ $opts['files'] = true;
+ $opts['data'] = true;
+ }
+
+ $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync'];
+
+ $sourceRemoteSettings = $this->getRemoteSettings(
+ $sourceHost,
+ $sourceUser,
+ $sourcePrivateKeyFile,
+ $sourceApp
+ );
+ $sourceProjectRoot = $this->getCurrentProjectRoot($sourceHost, $sourceUser, $sourcePrivateKeyFile, $sourceRemoteSettings);
+ $sourceRemoteConfig = new RemoteConfig($sourceHost, $sourceUser, $sourcePrivateKeyFile, $sourceRemoteSettings, $sourceProjectRoot);
+
+ $destinationRemoteSettings = $this->getRemoteSettings(
+ $destinationHost,
+ $destinationUser,
+ $destinationPrivateKeyFile,
+ $destinationApp
+ );
+ $destinationProjectRoot = $this->getCurrentProjectRoot($destinationHost, $destinationUser, $destinationPrivateKeyFile, $destinationRemoteSettings);
+ $destinationRemoteConfig = new RemoteConfig($destinationHost, $destinationUser, $destinationPrivateKeyFile, $destinationRemoteSettings, $destinationProjectRoot);
+
+ $collection = $this->collectionBuilder();
+
+ if ($opts['files'] && $opts['rsync']) {
+ // Files are rsync'ed, no need to sync them through backups later.
+ $opts['files'] = false;
+ $collection->addTask(
+ $this->rsyncFilesBetweenHostsTask(
+ $sourceRemoteConfig,
+ $destinationRemoteConfig,
+ )
+ );
+ }
+
+ if ($opts['data'] || $opts['files']) {
+ // Create a backup on the source host.
+ $collection->addTask(
+ $this->backupRemoteTask($sourceRemoteConfig, $opts)
+ );
+ // Download the backup from the source host to the local machine.
+ $collection->addTask(
+ $this->downloadBackupTask($sourceRemoteConfig, $opts)
+ );
+ // Remove the backup from the source host.
+ $collection->addTask(
+ $this->removeBackupRemoteTask($sourceRemoteConfig, $opts)
+ );
+ // Upload the backup to the destination host.
+ $collection->addTask(
+ $this->uploadBackupTask($destinationRemoteConfig, $opts)
+ );
+ // Restore the backup on the destination host.
+ $collection->addTask(
+ $this->restoreBackupRemoteTask($destinationRemoteConfig, $opts)
+ );
+ // Remove the backup from the destination host.
+ $collection->completion(
+ $this->removeBackupRemoteTask($destinationRemoteConfig, $opts)
+ );
+
+ // Finally remove the local backups.
+ $collection->completion($this->removeLocalBackupTask($sourceRemoteConfig, $opts));
+ }
+
+ $collection->completion($this->clearCacheTask($destinationRemoteConfig));
+
+ return $collection;
+ }
+
+ /**
+ * Get the task that rsyncs files between hosts.
+ *
+ * @param RemoteConfig $sourceRemoteConfig
+ * RemoteConfig object populated with data relevant to the source.
+ * @param RemoteConfig $destinationRemoteConfig
+ * RemoteConfig object populated with data relevant to the destination.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function rsyncFilesBetweenHostsTask(
+ RemoteConfig $sourceRemoteConfig,
+ RemoteConfig $destinationRemoteConfig
+ ) {
+ return $this->handleTaskEvent(
+ 'digipolis:rsync-files-between-hosts',
+ [
+ 'sourceRemoteConfig' => $sourceRemoteConfig,
+ 'destinationRemoteConfig' => $destinationRemoteConfig,
+ 'fileBackupConfig' => $this->getFileBackupConfig(),
+ 'timeouts' => [
+ 'synctask_rsync' => $this->getTimeoutSetting('synctask_rsync')
+ ]
+ ]
+ );
+ }
+
+ /**
+ * Remove the local backup of a remote application.
+ *
+ * @param RemoteConfig $remoteConfig
+ * RemoteConfig object populated with data relevant to the host.
+ * @param array $options
+ * Options that were used to create the backup.
+ *
+ * @return \Robo\Contract\TaskInterface
+ */
+ protected function removeLocalBackupTask(RemoteConfig $remoteConfig, $options)
+ {
+ return $this->handleTaskEvent(
+ 'digipolis:remove-local-backup',
+ [
+ 'remoteConfig' => $remoteConfig,
+ 'fileBackupConfig' => $this->getFileBackupConfig(),
+ 'options' => $options,
+ ]
+ );
+ }
+}
diff --git a/src/Traits/EventDispatcher.php b/src/Traits/EventDispatcher.php
new file mode 100644
index 0000000..da47029
--- /dev/null
+++ b/src/Traits/EventDispatcher.php
@@ -0,0 +1,113 @@
+getEventHandlers($eventName);
+
+ $event = new GenericEvent();
+ $event->setArguments($arguments);
+ $collection = $this->collectionBuilder();
+ foreach ($handlers as $handler) {
+ $collection->addTask($handler->handle($event));
+ if ($event->isPropagationStopped()) {
+ break;
+ }
+ }
+ return $collection;
+ }
+
+ /**
+ * Handle an event.
+ *
+ * @param string $eventName
+ * The name of the event to handle.
+ *
+ * @param array $arguments
+ * Associative array of arguments.
+ *
+ * @return array
+ * The results of the event handlers.
+ */
+ protected function handleEvent(string $eventName, array $arguments): array
+ {
+ $handlers = $this->getEventHandlers($eventName);
+
+ $event = new GenericEvent();
+ $event->setArguments($arguments);
+ $result = [];
+ foreach ($handlers as $handler) {
+ $result[] = $handler->handle($event);
+ if ($event->isPropagationStopped()) {
+ break;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Returns a sorted (by priority) list of event handlers.
+ *
+ * @param string $eventName
+ * The name of the event to get the handlers for.
+ *
+ * @return EventHandlerWithPriority[]
+ * The sorted list of handlers for the given event.
+ */
+ protected function getEventHandlers(string $eventName): array
+ {
+ $handlerFactories = $this->getCustomEventHandlers($eventName);
+ $handlers = [];
+
+ foreach ($handlerFactories as $handlerFactory) {
+ /** @var EventHandlerWithPriority $handler */
+ $handler = $handlerFactory();
+ // If the handler implements the AddToContainerInterface, add it to
+ // the container, so all its dependencies are injected, based on the
+ // other interfaces it implements.
+ if ($handler instanceof AddToContainerInterface && $this instanceof ContainerAwareInterface) {
+ $class = get_class($handler);
+ if (!$this->getContainer()->has($class)) {
+ $this->getContainer()->addShared($class, $handler);
+ }
+ // Inflectors only run when getting the service.
+ $handler = $this->getContainer()->get($class);
+ }
+
+ // Inject the builder if possible and needed.
+ if ($handler instanceof BuilderAwareInterface && $this instanceof BuilderAwareInterface && $this instanceof ContainerAwareInterface) {
+ $handler->setBuilder(CollectionBuilder::create($this->getContainer(), $handler));
+ }
+ $handlers[] = $handler;
+ }
+
+ usort($handlers, function (EventHandlerWithPriority $handlerA, EventHandlerWithPriority $handlerB) {
+ return $handlerA->getPriority() - $handlerB->getPriority();
+ });
+
+ return $handlers;
+ }
+}
diff --git a/src/Traits/RemoteFilesBackupTrait.php b/src/Traits/RemoteFilesBackupTrait.php
index e781bd9..89fcadc 100644
--- a/src/Traits/RemoteFilesBackupTrait.php
+++ b/src/Traits/RemoteFilesBackupTrait.php
@@ -2,7 +2,7 @@
namespace DigipolisGent\Robo\Helpers\Traits;
-use DigipolisGent\Robo\Helpers\RemoteFilesBackup;
+use DigipolisGent\Robo\Helpers\Robo\Plugin\Tasks\RemoteFilesBackup;
use DigipolisGent\Robo\Task\Deploy\Ssh\Auth\AbstractAuth;
trait RemoteFilesBackupTrait
@@ -20,7 +20,7 @@ trait RemoteFilesBackupTrait
* @param string $cwd
* The working directory to execute the commands in.
*
- * @return \DigipolisGent\Robo\Helpers\RemoteFilesBackup
+ * @return \DigipolisGent\Robo\Helpers\Tasks\RemoteFilesBackup
*/
protected function taskRemoteFilesBackup($host, AbstractAuth $auth, $backupDir, $cwd)
{
diff --git a/src/Util/AddToContainerInterface.php b/src/Util/AddToContainerInterface.php
new file mode 100644
index 0000000..78dd1b2
--- /dev/null
+++ b/src/Util/AddToContainerInterface.php
@@ -0,0 +1,7 @@
+host = $host;
+ $this->user = $user;
+ $this->privateKeyFile = $privateKeyFile;
+ $this->remoteSettings = $remoteSettings;
+ $this->currentProjectRoot = $currentProjectRoot;
+ }
+
+ public function getHost(): string {
+ return $this->host;
+ }
+
+ public function getUser(): string
+ {
+ return $this->user;
+ }
+
+ public function getPrivateKeyFile(): string
+ {
+ return $this->privateKeyFile;
+ }
+
+ public function getRemoteSettings(): array
+ {
+ return $this->remoteSettings;
+ }
+
+ public function getCurrentProjectRoot(): string
+ {
+ return $this->currentProjectRoot;
+ }
+
+ public function setHost(string $host): void
+ {
+ $this->host = $host;
+ }
+
+ public function setUser(string $user): void
+ {
+ $this->user = $user;
+ }
+
+ public function setPrivateKeyFile(string $privateKeyFile): void
+ {
+ $this->privateKeyFile = $privateKeyFile;
+ }
+
+ public function setRemoteSettings(array $remoteSettings): void
+ {
+ $this->remoteSettings = $remoteSettings;
+ }
+
+ public function setCurrentProjectRoot(string $currentProjectRoot): void
+ {
+ $this->currentProjectRoot = $currentProjectRoot;
+ }
+}
diff --git a/src/Util/RemoteHelper.php b/src/Util/RemoteHelper.php
deleted file mode 100644
index 1dc91bc..0000000
--- a/src/Util/RemoteHelper.php
+++ /dev/null
@@ -1,333 +0,0 @@
- 'HOSTNAME',
- 'environment_matcher' => '\\DigipolisGent\\Robo\\Helpers\\Util\\EnvironmentMatcher::regexMatch',
- ];
-
- protected $time;
-
- protected $projectRoots = [];
-
- public function __construct(int $time, ConfigInterface $config, PropertiesHelper $propertiesHelper)
- {
- $this->time = $time;
- $this->setConfig($config);
- $this->setPropertiesHelper($propertiesHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get('digipolis.time'),
- $container->get('config'),
- $container->get(PropertiesHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- public function getTime()
- {
- return $this->time;
- }
-
-
- /**
- * Get the settings from the 'remote' config key, with the tokens replaced.
- *
- * @param string $host
- * The IP address of the server to get the settings for.
- * @param string $user
- * The SSH user used to connect to the server.
- * @param string $keyFile
- * The path to the private key file used to connect to the server.
- * @param string $app
- * The name of the app these settings apply to.
- * @param string|null $timestamp
- * The timestamp to use. Defaults to the request time.
- *
- * @return array
- * The settings for this server and app.
- */
- public function getRemoteSettings($host, $user, $keyFile, $app, $timestamp = null)
- {
- $this->propertiesHelper->readProperties();
- $defaults = [
- 'user' => $user,
- 'private-key' => $keyFile,
- 'app' => $app,
- 'createbackup' => true,
- 'time' => is_null($timestamp) ? $this->time : $timestamp,
- ];
-
- // Set up destination config.
- $replacements = array(
- '[user]' => $user,
- '[private-key]' => $keyFile,
- '[app]' => $app,
- '[time]' => is_null($timestamp) ? $this->time : $timestamp,
- );
- if (is_string($host)) {
- $replacements['[server]'] = $host;
- $defaults['server'] = $host;
- }
- if (is_array($host)) {
- foreach ($host as $key => $server) {
- $replacements['[server-' . $key . ']'] = $server;
- $defaults['server-' . $key] = $server;
- }
- }
-
- $settings = $this->processEnvironmentOverrides(
- $this->tokenReplace($this->getConfig()->get('remote'), $replacements) + $defaults
- );
-
- // Reverse the symlinks so the `current` symlink is the last one to be
- // created.
- $settings['symlinks'] = array_reverse($settings['symlinks'], true);
-
- return $settings;
- }
-
- /**
- * Get the settings from the 'local' config key, with the tokens replaced.
- *
- * @param string $app
- * The name of the app these settings apply to.
- * @param string|null $timestamp
- * The timestamp to use. Defaults to the request time.
- *
- * @return array
- * The settings for the local environment and app.
- */
- public function getLocalSettings($app = null, $timestamp = null)
- {
- $this->propertiesHelper->readProperties();
- $defaults = [
- 'app' => $app,
- 'time' => is_null($timestamp) ? $this->time : $timestamp,
- 'project_root' => $this->getConfig()->get('digipolis.root.project'),
- 'web_root' => $this->getConfig()->get('digipolis.root.web'),
- ];
-
- // Set up destination config.
- $replacements = array(
- '[project_root]' => $this->getConfig()->get('digipolis.root.project'),
- '[web_root]' => $this->getConfig()->get('digipolis.root.web'),
- '[app]' => $app,
- '[time]' => is_null($timestamp) ? $this->time : $timestamp,
- );
- return $this->tokenReplace($this->getConfig()->get('local'), $replacements) + $defaults;
- }
-
-
- /**
- * Process environment-specific overrides.
- *
- * @param array $settings
- * @return array
- *
- * @see self::getRemoteSettings
- */
- protected function processEnvironmentOverrides($settings)
- {
- $settings += static::$defaultEnvironmentOverrideSettings;
- if (!isset($settings['environment_overrides']) || !$settings['environment_overrides']) {
- return $settings;
- }
-
- $server = $this->getFirstServer($settings);
- if (!$server) {
- return $settings;
- }
-
- // Parse the env var on the server.
- $auth = new KeyFile($settings['user'], $settings['private-key']);
- $fullOutput = '';
- $this->taskSsh($server, $auth)
- ->exec(
- (string) CommandBuilder::create('echo')
- ->addRawArgument('$' . $settings['environment_env_var']),
- function ($output) use (&$fullOutput) {
- $fullOutput .= $output;
- }
- )
- ->run();
- $envVarValue = substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput)));
- foreach ($settings['environment_overrides'] as $environmentMatch => $overrides) {
- if (call_user_func($settings['environment_matcher'], $environmentMatch, $envVarValue)) {
- $settings = ArrayMerger::doMerge($settings, $overrides);
- }
- }
- return $settings;
- }
-
- /**
- * Get the first server entry from the remote settings.
- *
- * @param array $settings
- *
- * @return string|bool
- * First server if found, false otherwise.
- *
- * @see self::processEnvironmentOverrides
- */
- protected function getFirstServer($settings)
- {
- foreach ($settings as $key => $value) {
- if (preg_match('/^server/', $key) === 1) {
- return $value;
- }
- }
- return false;
- }
-
- /**
- * Helper functions to replace tokens in an array.
- *
- * @param string|array $input
- * The array or string containing the tokens to replace.
- * @param array $replacements
- * The token replacements.
- *
- * @return string|array
- * The input with the tokens replaced with their values.
- */
- protected function tokenReplace($input, $replacements)
- {
- if (is_string($input)) {
- return strtr($input, $replacements);
- }
- if (is_scalar($input) || empty($input)) {
- return $input;
- }
- foreach ($input as &$i) {
- $i = $this->tokenReplace($i, $replacements);
- }
- return $input;
- }
-
- public function getCurrentProjectRoot($worker, AbstractAuth $auth, $remote)
- {
- $key = $worker . ':' . $auth->getUser() . ':' . $remote['releasesdir'];
- if (!array_key_exists($key, $this->projectRoots)) {
- $fullOutput = '';
- $this->taskSsh($worker, $auth)
- ->remoteDirectory($remote['releasesdir'], true)
- ->exec(
- (string) CommandBuilder::create('ls')
- ->addFlag('1')
- ->pipeOutputTo(
- CommandBuilder::create('sort')
- ->addFlag('r')
- ->pipeOutputTo(
- CommandBuilder::create('head')
- ->addFlag('1')
- )
- ),
- function ($output) use (&$fullOutput) {
- $fullOutput .= $output;
- }
- )
- ->run();
- $this->projectRoots[$key] = $remote['releasesdir'] . '/' . substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput)));
- }
- return $this->projectRoots[$key];
- }
-
- /**
- * Check if the current release has robo available.
- *
- * @param string $worker
- * The server to check the release on.
- * @param \DigipolisGent\Robo\Helpers\Traits\AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool
- */
- public function currentReleaseHasRobo($worker, AbstractAuth $auth, $remote)
- {
- $currentProjectRoot = $this->getCurrentProjectRoot($worker, $auth, $remote);
- return $this->taskSsh($worker, $auth)
- ->remoteDirectory($currentProjectRoot, true)
- ->exec(
- (string) CommandBuilder::create('ls')
- ->addArgument('vendor/bin/robo')
- ->pipeOutputTo(
- CommandBuilder::create('grep')
- ->addArgument('robo')
- )
- )
- ->run()
- ->wasSuccessful();
- }
-
- /**
- * Timeouts can be overwritten in properties.yml under the `timeout` key.
- *
- * @param string $setting
- *
- * @return int
- */
- public function getTimeoutSetting($setting)
- {
- $timeoutSettings = $this->getTimeoutSettings();
- return isset($timeoutSettings[$setting]) ? $timeoutSettings[$setting] : static::DEFAULT_TIMEOUT;
- }
-
- protected function getTimeoutSettings()
- {
- $this->propertiesHelper->readProperties();
- return $this->getConfig()->get('timeouts', []) + $this->getDefaultTimeoutSettings();
- }
-
- protected function getDefaultTimeoutSettings()
- {
- // Refactor this to default.properties.yml
- return [
- 'presymlink_mirror_dir' => 60,
- 'synctask_rsync' => 1800,
- 'backup_files' => 300,
- 'backup_database' => 300,
- 'remove_backup' => 300,
- 'restore_files_backup' => 300,
- 'restore_db_backup' => 60,
- 'pre_restore_remove_files' => 300,
- 'clean_dir' => 30,
- 'clear_op_cache' => 30,
- 'compress_old_release' => 300,
- ];
- }
-}
diff --git a/src/Util/TaskFactory/AbstractApp.php b/src/Util/TaskFactory/AbstractApp.php
deleted file mode 100644
index ef07063..0000000
--- a/src/Util/TaskFactory/AbstractApp.php
+++ /dev/null
@@ -1,97 +0,0 @@
-setConfig($config);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static($container->get('config'));
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Install the site in the current folder.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- * @param bool $force
- * Whether or not to force the install even when the site is present.
- *
- * @return \Robo\Contract\TaskInterface
- * The install task.
- */
- abstract public function installTask($worker, AbstractAuth $auth, $remote, $extra = [], $force = false);
-
- /**
- * Executes database updates of the site in the current folder.
- *
- * Executes database updates of the site in the current folder. Sets
- * the site in maintenance mode before the update and takes in out of
- * maintenance mode after.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The update task.
- */
- abstract public function updateTask($worker, AbstractAuth $auth, $remote);
-
- /**
- * Check if a site is already installed
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool
- * Whether or not the site is installed.
- */
- abstract public function isSiteInstalled($worker, AbstractAuth $auth, $remote);
-
- /**
- * Clear cache of the site.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The clear cache task or false if no clear cache task exists.
- */
- abstract public function clearCacheTask($worker, $auth, $remote);
-}
diff --git a/src/Util/TaskFactory/Backup.php b/src/Util/TaskFactory/Backup.php
deleted file mode 100644
index c113fd1..0000000
--- a/src/Util/TaskFactory/Backup.php
+++ /dev/null
@@ -1,334 +0,0 @@
-setRemoteHelper($remoteHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get(RemoteHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Create a backup of files (storage folder) and database.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The backup task.
- */
- public function backupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
- $backupConfig = $this->getBackupConfig();
- $backupDir = $remote['backupsdir'] . '/' . $remote['time'];
- $collection = $this->collectionBuilder();
- if ($opts['files']) {
- $collection
- ->taskRemoteFilesBackup($worker, $auth, $backupDir, $remote['filesdir'])
- ->backupFile($this->backupFileName('.tar.gz'))
- ->excludeFromBackup($backupConfig['exclude_from_backup'])
- ->backupSubDirs($backupConfig['file_backup_subdirs'])
- ->timeout($this->remoteHelper->getTimeoutSetting('backup_files'));
- }
-
- if ($opts['data']) {
- $currentProjectRoot = $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote);
- $collection
- ->taskRemoteDatabaseBackup($worker, $auth, $backupDir, $currentProjectRoot)
- ->backupFile($this->backupFileName('.sql'))
- ->timeout($this->remoteHelper->getTimeoutSetting('backup_database'));
- }
- return $collection;
- }
-
- /**
- * Restore a backup of files (storage folder) and database.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The restore backup task.
- */
- public function restoreBackupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
-
- $currentProjectRoot = $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote);
- $backupDir = $remote['backupsdir'] . '/' . $remote['time'];
-
- $collection = $this->collectionBuilder();
-
- // Restore the files backup.
- $preRestoreBackup = $this->preRestoreBackupTask($worker, $auth, $remote, $opts);
- if ($preRestoreBackup) {
- $collection->addTask($preRestoreBackup);
- }
-
- if ($opts['files']) {
- $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']);
- $collection
- ->taskSsh($worker, $auth)
- ->remoteDirectory($remote['filesdir'], true)
- ->timeout($this->remoteHelper->getTimeoutSetting('restore_files_backup'))
- ->exec(
- (string) CommandBuilder::create('tar')
- ->addFlag('xkz')
- ->addFlag('f', $backupDir . '/' . $filesBackupFile)
- );
- }
-
- // Restore the db backup.
- if ($opts['data']) {
- $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']);
- $collection
- ->taskSsh($worker, $auth)
- ->remoteDirectory($currentProjectRoot, true)
- ->timeout($this->remoteHelper->getTimeoutSetting('restore_db_backup'))
- ->exec(
- (string) CommandBuilder::create('vendor/bin/robo digipolis:database-restore')
- ->addOption('source', $backupDir . '/' . $dbBackupFile)
- );
- }
- return $collection;
- }
-
-
- /**
- * Pre restore backup task.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The pre restore backup task, false if no pre restore backup tasks need
- * to run.
- */
- protected function preRestoreBackupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
- if ($opts['files']) {
- $backupConfig = $this->getBackupConfig();
- $removeFiles = CommandBuilder::create('rm')->addFlag('rf');
- if (!$backupConfig['file_backup_subdirs']) {
- $removeFiles->addArgument('./*');
- $removeFiles->addArgument('./.??*');
- }
- foreach ($backupConfig['file_backup_subdirs'] as $subdir) {
- $removeFiles->addArgument($subdir . '/*');
- $removeFiles->addArgument($subdir . '/.??*');
- }
-
- return $this->taskSsh($worker, $auth)
- ->remoteDirectory($remote['filesdir'], true)
- // Files dir can be pretty big on large sites.
- ->timeout($this->remoteHelper->getTimeoutSetting('pre_restore_remove_files'))
- ->exec((string) $removeFiles);
- }
-
- return false;
- }
-
- /**
- * Remove a backup.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The backup task.
- */
- public function removeBackupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- $backupDir = $remote['backupsdir'] . '/' . $remote['time'];
-
- $collection = $this->collectionBuilder();
- $collection->taskSsh($worker, $auth)
- ->timeout($this->remoteHelper->getTimeoutSetting('remove_backup'))
- ->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('rf')
- ->addArgument($backupDir)
- );
-
- return $collection;
- }
-
- /**
- * Download a backup of files (storage folder) and database.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The download backup task.
- */
- public function downloadBackupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
- $backupDir = $remote['backupsdir'] . '/' . $remote['time'];
-
- $collection = $this->collectionBuilder();
- $collection
- ->taskSFTP($worker, $auth);
-
- // Download files.
- if ($opts['files']) {
- $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']);
- $collection->get($backupDir . '/' . $filesBackupFile, $filesBackupFile);
- }
-
- // Download data.
- if ($opts['data']) {
- $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']);
- $collection->get($backupDir . '/' . $dbBackupFile, $dbBackupFile);
- }
- return $collection;
- }
-
- /**
- * Upload a backup of files (storage folder) and database to a server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The upload backup task.
- */
- public function uploadBackupTask(
- $worker,
- AbstractAuth $auth,
- $remote,
- $opts = ['files' => false, 'data' => false]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
- $backupDir = $remote['backupsdir'] . '/' . $remote['time'];
- $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']);
- $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']);
-
- $collection = $this->collectionBuilder();
- $collection
- ->taskSsh($worker, $auth)
- ->exec((string) CommandBuilder::create('mkdir')->addFlag('p')->addArgument($backupDir))
- ->taskSFTP($worker, $auth);
- if ($opts['files']) {
- $collection->put($backupDir . '/' . $filesBackupFile, $filesBackupFile);
- }
- if ($opts['data']) {
- $collection->put($backupDir . '/' . $dbBackupFile, $dbBackupFile);
- }
- return $collection;
- }
-
- /**
- * Generate a backup filename based on the given time.
- *
- * @param string $extension
- * The extension to append to the filename. Must include leading dot.
- * @param int|null $timestamp
- * The timestamp to generate the backup name from. Defaults to the request
- * time.
- *
- * @return string
- * The generated filename.
- */
- public function backupFileName($extension, $timestamp = null)
- {
- if (is_null($timestamp)) {
- $timestamp = $this->remoteHelper->getTime();
- }
- return $timestamp . '_' . date('Y_m_d_H_i_s', $timestamp) . $extension;
- }
-}
diff --git a/src/Util/TaskFactory/BackupConfigTrait.php b/src/Util/TaskFactory/BackupConfigTrait.php
deleted file mode 100644
index c45fedf..0000000
--- a/src/Util/TaskFactory/BackupConfigTrait.php
+++ /dev/null
@@ -1,26 +0,0 @@
-getCustomEventHandlers('digipolis-backup-config');
- $backupConfig = [
- 'file_backup_subdirs' => [],
- 'exclude_from_backup' => [],
- ];
- foreach ($handlers as $handler) {
- $handlerConfig = $handler();
- if (isset($handlerConfig['file_backup_subdirs'])) {
- $backupConfig['file_backup_subdirs'] = array_merge($backupConfig['file_backup_subdirs'], $handlerConfig['file_backup_subdirs']);
- }
-
- if (isset($handlerConfig['exclude_from_backup'])) {
- $backupConfig['exclude_from_backup'] = array_merge($backupConfig['exclude_from_backup'], $handlerConfig['exclude_from_backup']);
- }
- }
- }
-
-}
diff --git a/src/Util/TaskFactory/Build.php b/src/Util/TaskFactory/Build.php
deleted file mode 100644
index 3f3201b..0000000
--- a/src/Util/TaskFactory/Build.php
+++ /dev/null
@@ -1,58 +0,0 @@
-setPropertiesHelper($propertiesHelper);
- $this->setRemoteHelper($remoteHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get(PropertiesHelper::class),
- $container->get(RemoteHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Build a site and package it.
- *
- * @param string $archivename
- * Name of the archive to create.
- *
- * @return \Robo\Contract\TaskInterface
- * The deploy task.
- */
- public function buildTask($archivename = null)
- {
- $this->propertiesHelper->readProperties();
- $archive = is_null($archivename) ? $this->remoteHelper->getTime() . '.tar.gz' : $archivename;
- $collection = $this->collectionBuilder();
- $collection
- ->taskPackageProject($archive);
- return $collection;
- }
-}
diff --git a/src/Util/TaskFactory/Cache.php b/src/Util/TaskFactory/Cache.php
deleted file mode 100644
index de2ea86..0000000
--- a/src/Util/TaskFactory/Cache.php
+++ /dev/null
@@ -1,83 +0,0 @@
-setAppTaskFactory($appTaskFactory);
- $this->setRemoteHelper($remoteHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get(AbstractApp::class),
- $container->get(RemoteHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Clear cache of the site.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The clear cache task or false if no clear cache task exists.
- */
- public function clearCacheTask($worker, $auth, $remote)
- {
- return $this->appTaskFactory->clearCacheTask($worker, $auth, $remote);
- }
-
- /**
- * Clear OPcache on the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The clear OPcache task.
- */
- public function clearOpCacheTask($worker, AbstractAuth $auth, $remote)
- {
- $clearOpcache = CommandBuilder::create('vendor/bin/robo digipolis:clear-op-cache')->addArgument($remote['opcache']['env']);
- if (isset($remote['opcache']['host'])) {
- $clearOpcache->addOption('host', $remote['opcache']['host']);
- }
- return $this->taskSsh($worker, $auth)
- ->remoteDirectory($remote['rootdir'], true)
- ->timeout($this->remoteHelper->getTimeoutSetting('clear_op_cache'))
- ->exec((string) $clearOpcache);
- }
-}
diff --git a/src/Util/TaskFactory/Deploy.php b/src/Util/TaskFactory/Deploy.php
deleted file mode 100644
index 1369037..0000000
--- a/src/Util/TaskFactory/Deploy.php
+++ /dev/null
@@ -1,539 +0,0 @@
-setAppTaskFactory($appTaskFactory);
- $this->setBackupTaskFactory($backupTaskFactory);
- $this->setBuildTaskFactory($buildTaskFactory);
- $this->setCacheTaskFactory($cacheTaskFactory);
- $this->setPropertiesHelper($propertiesHelper);
- $this->setRemoteHelper($remoteHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get(AbstractApp::class),
- $container->get(Backup::class),
- $container->get(Build::class),
- $container->get(Cache::class),
- $container->get(PropertiesHelper::class),
- $container->get(RemoteHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Build a site and push it to the servers.
- *
- * @param array $arguments
- * Variable amount of arguments. The last argument is the path to the
- * the private key file (ssh), the penultimate is the ssh user. All
- * arguments before that are server IP's to deploy to.
- * @param array $opts
- * The options for this task.
- *
- * @return \Robo\Contract\TaskInterface
- * The deploy task.
- */
- public function deployTask(
- array $arguments,
- $opts
- ) {
- // Define variables.
- $opts += ['force-install' => false];
- $privateKeyFile = array_pop($arguments);
- $user = array_pop($arguments);
- $servers = $arguments;
- $worker = is_null($opts['worker']) ? reset($servers) : $opts['worker'];
- $remote = $this->remoteHelper->getRemoteSettings($servers, $user, $privateKeyFile, $opts['app']);
- $releaseDir = $remote['releasesdir'] . '/' . $remote['time'];
- $auth = new KeyFile($user, $privateKeyFile);
- $archive = $remote['time'] . '.tar.gz';
- $backupOpts = ['files' => false, 'data' => true];
-
- $collection = $this->collectionBuilder();
-
- // Build the archive to deploy.
- $collection->addTask($this->buildTaskFactory->buildTask($archive));
-
- // Create a backup and a rollback task if a site is already installed.
- if ($remote['createbackup'] && $this->appTaskFactory->isSiteInstalled($worker, $auth, $remote) && $this->remoteHelper->currentReleaseHasRobo($worker, $auth, $remote)) {
- // Create a backup.
- $collection->addTask($this->backupTaskFactory->backupTask($worker, $auth, $remote, $backupOpts));
-
- // Create a rollback for this backup for when the deploy fails.
- $collection->rollback(
- $this->backupTaskFactory->restoreBackupTask(
- $worker,
- $auth,
- $remote,
- $backupOpts
- )
- );
- }
-
- // Push the package to the servers and create the required symlinks.
- foreach ($servers as $server) {
- // Remove this release on rollback.
- $collection->rollback($this->removeFailedReleaseTask($server, $auth, $remote, $releaseDir));
-
- // Clear opcache (if present) on rollback.
- if (isset($remote['opcache']) && (!array_key_exists('clear', $remote['opcache']) || $remote['opcache']['clear'])) {
- $collection->rollback($this->cacheTaskFactory->clearOpCacheTask($server, $auth, $remote));
- }
-
- // Push the package.
- $collection->addTask($this->pushPackageTask($server, $auth, $remote, $archive));
-
- // Add any tasks to execute before creating the symlinks.
- $preSymlink = $this->preSymlinkTask($server, $auth, $remote);
- if ($preSymlink) {
- $collection->addTask($preSymlink);
- }
-
- // Switch the current symlink to the previous release on rollback.
- $collection->rollback($this->switchPreviousTask($server, $auth, $remote));
-
- // Create the symlinks.
- $collection->addTask($this->symlinksTask($server, $auth, $remote));
- $postSymlink = $this->postSymlinkTask($server, $auth, $remote);
- if ($postSymlink) {
- $collection->addTask($postSymlink);
- }
- }
-
- // Initialize the site (update or install).
- $collection->addTask($this->initRemoteTask($worker, $auth, $remote, $opts, $opts['force-install']));
-
- // Clear cache after update or install.
- $clearCache = $this->cacheTaskFactory->clearCacheTask($worker, $auth, $remote);
- if ($clearCache) {
- $collection->addTask($clearCache);
- }
-
- // Clear OPcache if present.
- if (isset($remote['opcache']) && (!array_key_exists('clear', $remote['opcache']) || $remote['opcache']['clear'])) {
- foreach ($servers as $server) {
- $collection->addTask($this->cacheTaskFactory->clearOpCacheTask($server, $auth, $remote));
- }
- }
-
- foreach ($servers as $server) {
- // Compress old releases if configured.
- if (isset($remote['compress_old_releases']) && $remote['compress_old_releases']) {
- // The current release (the one we're replacing).
- $currentRelease = $this->remoteHelper->getCurrentProjectRoot($server, $auth, $remote);
- // Strip the releases dir from the current release, so the tar
- // contains relative paths.
- $relativeCurrentRelease = str_replace($remote['releasesdir'] . '/', '', $currentRelease);
- $collection->addTask(
- $this->taskSsh($server, $auth)
- ->remoteDirectory($remote['releasesdir'])
- ->exec((string) CommandBuilder::create('tar')
- ->addFlag('c')
- ->addFlag('z')
- ->addFlag('f', $relativeCurrentRelease . '.tar.gz')
- ->addArgument($relativeCurrentRelease)
- ->onSuccess(
- CommandBuilder::create('chown')
- ->addFlag('R')
- ->addArgument($auth->getUser() . ':' . $auth->getUser())
- ->addArgument($relativeCurrentRelease)
- ->onSuccess(CommandBuilder::create('chmod')
- ->addFlag('R')
- ->addArgument('a+rwx')
- ->addArgument($relativeCurrentRelease)
- ->onSuccess(CommandBuilder::create('rm')
- ->addFlag('rf')
- ->addArgument($relativeCurrentRelease)
- )
- )
- )
- ->onFailure(
- CommandBuilder::create('rm')
- ->addFlag('r')
- ->addFlag('f')
- ->addArgument($relativeCurrentRelease . '.tar.gz')
- )
- )->timeout($this->remoteHelper->getTimeoutSetting('compress_old_release'))
- );
- }
- // Clean release and backup dirs on the servers.
- $collection->completion($this->cleanDirsTask($server, $auth, $remote));
- }
-
- // Clear the site's cache on rollback too.
- if ($clearCache) {
- $collection->completion($clearCache);
- }
-
- return $collection;
- }
-
- /**
- * Tasks to execute before creating the symlinks.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The presymlink task, false if no pre symlink tasks need to run.
- */
- protected function preSymlinkTask($worker, AbstractAuth $auth, $remote)
- {
- $collection = $this->collectionBuilder();
- foreach ($remote['symlinks'] as $symlink) {
- $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($worker, $auth, $remote, $symlink);
- if ($preIndividualSymlinkTask) {
- $collection->addTask($preIndividualSymlinkTask);
- }
- }
- return $collection;
- }
-
- /**
- * Tasks to execute before creating an individual symlink.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- * @param string $symlink
- * The symlink in format "target:link".
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The presymlink task, false if no pre symlink task needs to run.
- */
- protected function preIndividualSymlinkTask($worker, AbstractAuth $auth, $remote, $symlink)
- {
- $projectRoot = $remote['rootdir'];
- $collection = $this->collectionBuilder();
- $collection->taskSsh($worker, $auth)
- ->remoteDirectory($projectRoot, true)
- ->timeout($this->remoteHelper->getTimeoutSetting('presymlink_mirror_dir'));
- list($target, $link) = explode(':', $symlink);
- if ($link === $remote['currentdir']) {
- return;
- }
- // If the link we're going to create is an existing directory,
- // mirror that directory on the symlink target and then delete it
- // before creating the symlink
- $collection->exec(
- (string) CommandBuilder::create('vendor/bin/robo digipolis:mirror-dir')
- ->addArgument($link)
- ->addArgument($target)
- );
- $collection->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('rf')
- ->addArgument($link)
- );
-
- return $collection;
- }
-
-
-
- /**
- * Create all required symlinks on the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The symlink task.
- */
- protected function symlinksTask($worker, AbstractAuth $auth, $remote)
- {
- $collection = $this->collectionBuilder();
- foreach ($remote['symlinks'] as $link) {
- $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($worker, $auth, $remote, $link);
- if ($preIndividualSymlinkTask) {
- $collection->addTask($preIndividualSymlinkTask);
- }
- list($target, $linkname) = explode(':', $link);
- $collection->taskSsh($worker, $auth)
- ->exec(
- (string) CommandBuilder::create('ln')
- ->addFlag('s')
- ->addFlag('T')
- ->addFlag('f')
- ->addArgument($target)
- ->addArgument($linkname)
- );
- }
- return $collection;
- }
-
- /**
- * Tasks to execute after creating the symlinks.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return bool|\Robo\Contract\TaskInterface
- * The postsymlink task, false if no post symlink tasks need to run.
- */
- protected function postSymlinkTask($worker, AbstractAuth $auth, $remote)
- {
- if (isset($remote['postsymlink_filechecks']) && $remote['postsymlink_filechecks']) {
- $projectRoot = $remote['rootdir'];
- $collection = $this->collectionBuilder();
- $collection->taskSsh($worker, $auth)
- ->remoteDirectory($projectRoot, true)
- ->timeout($this->remoteHelper->getTimeoutSetting('postsymlink_filechecks'));
- foreach ($remote['postsymlink_filechecks'] as $file) {
- // If this command fails, the collection will fail, which will
- // trigger a rollback.
- $builder = CommandBuilder::create('ls')
- ->addArgument($file)
- ->pipeOutputTo('grep')
- ->addArgument($file)
- ->onFailure(
- CommandBuilder::create('echo')
- ->addArgument('[ERROR] ' . $file . ' was not found.')
- ->onFinished('exit')
- ->addArgument('1')
- );
- $collection->exec((string) $builder);
- }
- return $collection;
- }
- return false;
- }
-
- /**
- * Install or update a remote site.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- * @param array $extra
- * Extra parameters to pass to site install.
- * @param bool $force
- * Whether or not to force the install even when the site is present.
- *
- * @return \Robo\Contract\TaskInterface
- * The init remote task.
- */
- protected function initRemoteTask($worker, AbstractAuth $auth, $remote, $extra = [], $force = false)
- {
- $collection = $this->collectionBuilder();
- if (!$this->appTaskFactory->isSiteInstalled($worker, $auth, $remote) || $force) {
- $this->say($force ? 'Forcing site install.' : 'Site status failed.');
- $this->say('Triggering install script.');
-
- $collection->addTask($this->appTaskFactory->installTask($worker, $auth, $remote, $extra, $force));
- return $collection;
- }
- $collection->addTask($this->appTaskFactory->updateTask($worker, $auth, $remote, $extra));
- return $collection;
- }
-
- /**
- * Build a site and package it.
- *
- * @param string $archivename
- * Name of the archive to create.
- *
- * @return \Robo\Contract\TaskInterface
- * The deploy task.
- */
- protected function buildTask($archivename = null)
- {
- $this->propertiesHelper->readProperties();
- $archive = is_null($archivename) ? $this->time . '.tar.gz' : $archivename;
- $collection = $this->collectionBuilder();
- $collection
- ->taskPackageProject($archive);
- return $collection;
- }
-
- /**
- * Remove a failed release from the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- * @param string|null $releaseDirname
- * The path of the release dir to remove.
- *
- * @return \Robo\Contract\TaskInterface
- * The remove release task.
- */
- protected function removeFailedReleaseTask($worker, AbstractAuth $auth, $remote, $releaseDirname = null)
- {
- $releaseDir = is_null($releaseDirname)
- ? $remote['releasesdir'] . '/' . $remote['time']
- : $releaseDirname;
- return $this->taskRemoteRemoveRelease($worker, $auth, null, $releaseDir);
- }
-
-
-
- /**
- * Push a package to the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- * @param string|null $archivename
- * The path to the package to push.
- *
- * @return \Robo\Contract\TaskInterface
- * The push package task.
- */
- protected function pushPackageTask($worker, AbstractAuth $auth, $remote, $archivename = null)
- {
- $archive = is_null($archivename)
- ? $remote['time'] . '.tar.gz'
- : $archivename;
- $releaseDir = $remote['releasesdir'] . '/' . $remote['time'];
- $collection = $this->collectionBuilder();
- $collection->taskPushPackage($worker, $auth)
- ->destinationFolder($releaseDir)
- ->package($archive);
-
- $collection->taskSsh($worker, $auth)
- ->remoteDirectory($releaseDir, true)
- ->exec((string) CommandBuilder::create('chmod')
- ->addArgument('u+rx')
- ->addArgument('vendor/bin/robo')
- );
-
- return $collection;
- }
-
- /**
- * Switch the current symlink to the previous release on the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The switch previous task.
- */
- protected function switchPreviousTask($worker, AbstractAuth $auth, $remote)
- {
- return $this->taskRemoteSwitchPrevious(
- $worker,
- $auth,
- $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote),
- $remote['releasesdir'],
- $remote['currentdir']
- );
- }
-
- /**
- * Clean the release and backup directories on the server.
- *
- * @param string $worker
- * The server to install the site on.
- * @param AbstractAuth $auth
- * The ssh authentication to connect to the server.
- * @param array $remote
- * The remote settings for this server.
- *
- * @return \Robo\Contract\TaskInterface
- * The clean directories task.
- */
- protected function cleanDirsTask($worker, AbstractAuth $auth, $remote)
- {
- $cleandirLimit = isset($remote['cleandir_limit']) ? max(1, $remote['cleandir_limit']) : '';
- $collection = $this->collectionBuilder();
- $collection->taskRemoteCleanDirs($worker, $auth, $remote['rootdir'], $remote['releasesdir'], ($cleandirLimit ? ($cleandirLimit + 1) : false));
-
- if ($remote['createbackup']) {
- $collection->taskRemoteCleanDirs($worker, $auth, $remote['rootdir'], $remote['backupsdir'], ($cleandirLimit ? ($cleandirLimit) : false));
- }
-
- return $collection;
- }
-
-}
diff --git a/src/Util/TaskFactory/Sync.php b/src/Util/TaskFactory/Sync.php
deleted file mode 100644
index 1bdc73f..0000000
--- a/src/Util/TaskFactory/Sync.php
+++ /dev/null
@@ -1,412 +0,0 @@
-setBackupTaskFactory($backupTaskFactory);
- $this->setBuildTaskFactory($buildTaskFactory);
- $this->setCacheTaskFactory($cacheTaskFactory);
- $this->setRemoteHelper($remoteHelper);
- }
-
- public static function create(DefinitionContainerInterface $container)
- {
- $object = new static(
- $container->get(Backup::class),
- $container->get(Build::class),
- $container->get(Cache::class),
- $container->get(RemoteHelper::class)
- );
- $object->setBuilder(CollectionBuilder::create($container, $object));
-
- return $object;
- }
-
- /**
- * Sync the database and files between two sites.
- *
- * @param string $sourceUser
- * SSH user to connect to the source server.
- * @param string $sourceHost
- * IP address of the source server.
- * @param string $sourceKeyFile
- * Private key file to use to connect to the source server.
- * @param string $destinationUser
- * SSH user to connect to the destination server.
- * @param string $destinationHost
- * IP address of the destination server.
- * @param string $destinationKeyFile
- * Private key file to use to connect to the destination server.
- * @param string $sourceApp
- * The name of the source app we're syncing. Used to determine the
- * directory to sync.
- * @param string $destinationApp
- * The name of the destination app we're syncing. Used to determine the
- * directory to sync to.
- *
- * @return \Robo\Contract\TaskInterface
- * The sync task.
- */
- public function syncTask(
- $sourceUser,
- $sourceHost,
- $sourceKeyFile,
- $destinationUser,
- $destinationHost,
- $destinationKeyFile,
- $sourceApp = 'default',
- $destinationApp = 'default',
- $opts = ['files' => false, 'data' => false, 'rsync' => true]
- ) {
- if (!$opts['files'] && !$opts['data']) {
- $opts['files'] = true;
- $opts['data'] = true;
- }
-
- $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync'];
-
- $sourceRemote = $this->remoteHelper->getRemoteSettings(
- $sourceHost,
- $sourceUser,
- $sourceKeyFile,
- $sourceApp
- );
- $sourceAuth = new KeyFile($sourceUser, $sourceKeyFile);
-
- $destinationRemote = $this->remoteHelper->getRemoteSettings(
- $destinationHost,
- $destinationUser,
- $destinationKeyFile,
- $destinationApp
- );
- $destinationAuth = new KeyFile($destinationUser, $destinationKeyFile);
-
- $collection = $this->collectionBuilder();
-
- if ($opts['files'] && $opts['rsync']) {
- // Files are rsync'ed, no need to sync them through backups later.
- $opts['files'] = false;
- $collection->addTask(
- $this->rsyncAllFilesTask(
- $sourceAuth,
- $sourceHost,
- $sourceKeyFile,
- $sourceRemote,
- $destinationAuth,
- $destinationHost,
- $destinationKeyFile,
- $destinationRemote
- )
- );
- }
-
- if ($opts['data'] || $opts['files']) {
- // Create a backup on the source host.
- $collection->addTask(
- $this->backupTaskFactory->backupTask(
- $sourceHost,
- $sourceAuth,
- $sourceRemote,
- $opts
- )
- );
- // Download the backup from the source host to the local machine.
- $collection->addTask(
- $this->backupTaskFactory->downloadBackupTask(
- $sourceHost,
- $sourceAuth,
- $sourceRemote,
- $opts
- )
- );
- // Remove the backup from the source host.
- $collection->addTask(
- $this->backupTaskFactory->removeBackupTask(
- $sourceHost,
- $sourceAuth,
- $sourceRemote,
- $opts
- )
- );
- // Upload the backup to the destination host.
- $collection->addTask(
- $this->backupTaskFactory->uploadBackupTask(
- $destinationHost,
- $destinationAuth,
- $destinationRemote,
- $opts
- )
- );
- // Restore the backup on the destination host.
- $collection->addTask(
- $this->backupTaskFactory->restoreBackupTask(
- $destinationHost,
- $destinationAuth,
- $destinationRemote,
- $opts
- )
- );
- // Remove the backup from the destination host.
- $collection->completion(
- $this->backupTaskFactory->removeBackupTask(
- $destinationHost,
- $destinationAuth,
- $destinationRemote,
- $opts
- )
- );
-
- // Finally remove the local backups.
- $dbBackupFile = $this->backupTaskFactory->backupFileName('.sql.gz', $sourceRemote['time']);
- $removeLocalBackup = CommandBuilder::create('rm')
- ->addFlag('f')
- ->addArgument($dbBackupFile);
- if ($opts['files']) {
- $removeLocalBackup->addArgument($this->backupTaskFactory->backupFileName('.tar.gz', $sourceRemote['time']));
- }
-
- $collection->completion(
- $this->taskExecStack()
- ->exec((string) $removeLocalBackup)
- );
- }
-
- if ($clearCache = $this->cacheTaskFactory->clearCacheTask($destinationHost, $destinationAuth, $destinationRemote)) {
- $collection->completion($clearCache);
- }
-
- return $collection;
- }
-
- protected function rsyncAllFilesTask(
- AbstractAuth $sourceAuth,
- $sourceHost,
- $sourceKeyFile,
- $sourceRemote,
- AbstractAuth $destinationAuth,
- $destinationHost,
- $destinationKeyFile,
- $destinationRemote
- ) {
- $tmpKeyFile = '~/.ssh/' . uniqid('robo_', true) . '.id_rsa';
- $destinationUser = $destinationAuth->getUser();
- $sourceUser = $sourceAuth->getUser();
- $collection = $this->collectionBuilder();
- // Generate a temporary key.
- $collection->addTask(
- $this->generateKeyPair($tmpKeyFile)
- );
-
- $collection->completion(
- $this->removeKeyPair($tmpKeyFile)
- );
-
- // Install it on the destination host.
- $collection->addTask(
- $this->installPublicKeyOnDestination(
- $tmpKeyFile,
- $destinationUser,
- $destinationHost,
- $destinationKeyFile
- )
- );
-
- // Remove it from the destination host when we're done.
- $collection->completion(
- $this->removePublicKeyFromDestination(
- $tmpKeyFile,
- $destinationHost,
- $destinationAuth
- )
- );
-
- // Install the private key on the source host.
- $collection->addTask(
- $this->installPrivateKeyOnSource(
- $tmpKeyFile,
- $sourceHost,
- $sourceUser,
- $sourceKeyFile
- )
- );
-
- // Remove the private key from the source host.
- $collection->completion(
- $this->removePrivateKeyFromSource(
- $tmpKeyFile,
- $sourceHost,
- $sourceAuth
- )
- );
-
- $backupConfig = $this->getBackupConfig();
- $dirs = ($backupConfig['file_backup_subdirs'] ? $backupConfig['file_backup_subdirs'] : ['']);
-
- foreach ($dirs as $dir) {
- $dir .= ($dir !== '' ? '/' : '');
- $collection->addTask(
- $this->rsyncDirectory(
- $dir,
- $tmpKeyFile,
- $sourceHost,
- $sourceAuth,
- $sourceRemote,
- $destinationHost,
- $destinationAuth,
- $destinationRemote
- )
- );
- }
-
- return $collection;
- }
-
- protected function generateKeyPair($privateKey)
- {
- return $this->taskExec(
- (string) CommandBuilder::create('ssh-keygen')
- ->addFlag('q')
- ->addFlag('t', 'rsa')
- ->addFlag('b', 4096)
- ->addRawFlag('N', '""')
- ->addRawFlag('f', $privateKey)
- ->addFlag('C', 'robo:' . md5($privateKey))
- );
- }
-
- protected function removeKeyPair($privateKey)
- {
- return $this->taskExecStack()
- ->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('f')
- ->addRawArgument($privateKey)
- ->addRawArgument($privateKey . '.pub')
- );
- }
-
- protected function installPublicKeyOnDestination($privateKey, $destinationUser, $destinationHost, $destinationKeyFile)
- {
- return $this->taskExec(
- (string) CommandBuilder::create('cat')
- ->addRawArgument($privateKey . '.pub')
- ->pipeOutputTo(
- CommandBuilder::create('ssh')
- ->addArgument($destinationUser . '@' . $destinationHost)
- ->addFlag('o', 'StrictHostKeyChecking=no')
- ->addRawFlag('i', $destinationKeyFile)
- )
- ->addArgument(
- CommandBuilder::create('mkdir')
- ->addFlag('p')
- ->addRawArgument('~/.ssh')
- ->onSuccess(
- CommandBuilder::create('cat')
- ->chain('~/.ssh/authorized_keys', '>>')
- )
- )
- );
- }
-
- protected function removePublicKeyFromDestination($privateKey, $destinationHost, AbstractAuth $destinationAuth)
- {
- return $this->taskSsh($destinationHost, $destinationAuth)
- ->exec(
- (string) CommandBuilder::create('sed')
- ->addFlag('i', '/robo:' . md5($privateKey) . '/d')
- ->addRawArgument('~/.ssh/authorized_keys')
- );
- }
-
- protected function installPrivateKeyOnSource($privateKey, $sourceHost, $sourceUser, $sourceKeyFile)
- {
- return $this->taskRsync()
- ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $sourceKeyFile . '`"')
- ->fromPath($privateKey)
- ->toHost($sourceHost)
- ->toUser($sourceUser)
- ->toPath('~/.ssh')
- ->archive()
- ->compress()
- ->checksum()
- ->wholeFile();
- }
-
- protected function removePrivateKeyFromSource($privateKey, $sourceHost, AbstractAuth $sourceAuth)
- {
- return $this->taskSsh($sourceHost, $sourceAuth)
- ->exec(
- (string) CommandBuilder::create('rm')
- ->addFlag('f')
- ->addRawArgument($privateKey)
- );
- }
-
- protected function rsyncDirectory($dir, $privateKey, $sourceHost, AbstractAuth $sourceAuth, $sourceSettings, $destinationHost, AbstractAuth $destinationAuth, $destinationSettings)
- {
- $rsync = $this->taskRsync()
- ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `cd -P ' . $sourceSettings['currentdir'] . '/.. && vendor/bin/robo digipolis:realpath ' . $privateKey . '`"')
- ->fromPath($sourceSettings['filesdir'] . '/' . $dir)
- ->toHost($destinationHost)
- ->toUser($destinationAuth->getUser())
- ->toPath($destinationSettings['filesdir'] . '/' . $dir)
- ->archive()
- ->delete()
- ->rawArg('--copy-links --keep-dirlinks')
- ->compress()
- ->checksum()
- ->wholeFile();
- $backupConfig = $this->getBackupConfig();
- foreach ($backupConfig['exclude_from_backup'] as $exclude) {
- $rsync->exclude($exclude);
- }
-
- return $this->taskSsh($sourceHost, $sourceAuth)
- ->timeout($this->remoteHelper->getTimeoutSetting('synctask_rsync'))
- ->exec($rsync);
- }
-}
diff --git a/src/Util/TimeHelper.php b/src/Util/TimeHelper.php
new file mode 100644
index 0000000..b2a1603
--- /dev/null
+++ b/src/Util/TimeHelper.php
@@ -0,0 +1,47 @@
+time = time();
+ }
+
+ /**
+ * Get the singleton instance.
+ *
+ * @return static
+ */
+ public static function getInstance(): static
+ {
+ if (!static::$instance) {
+ static::$instance = new static();
+ }
+ return static::$instance;
+ }
+
+ /**
+ * Get the timestamp.
+ *
+ * @return int
+ */
+ public function getTime()
+ {
+ return $this->time;
+ }
+}
diff --git a/src/default.properties.yml b/src/default.properties.yml
new file mode 100644
index 0000000..989066d
--- /dev/null
+++ b/src/default.properties.yml
@@ -0,0 +1,21 @@
+remote:
+ appdir: '/home/[user]/apps/[app]'
+ releasesdir: '${remote.appdir}/releases'
+ rootdir: '${remote.releasesdir}/[time]'
+ webdir: '${remote.rootdir}'
+ currentdir: '${remote.appdir}/current'
+ configdir: '${remote.appdir}/config'
+ filesdir: '${remote.appdir}/files'
+ backupsdir: '${remote.appdir}/backups'
+ createbackup: false
+ symlinks:
+ - '${remote.webdir}:${remote.currentdir}'
+ opcache:
+ env: 'fcgi'
+ host: '/usr/local/multi-php/[user]/run/[user].sock'
+ cleandir_limit: 2
+ postsymlink_filechecks:
+ - '${remote.rootdir}/vendor/autoload.php'
+ environment_overrides:
+ ^staging:
+ cleandir_limit: 1