2 namespace Drush\Commands\core;
4 use Consolidation\AnnotatedCommand\CommandData;
5 use Drush\Commands\DrushCommands;
7 use Drush\Exceptions\UserAbortException;
8 use Consolidation\SiteAlias\HostPath;
9 use Consolidation\SiteAlias\SiteAliasManagerAwareInterface;
10 use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
11 use Drush\Backend\BackendPathEvaluator;
12 use Drush\Config\ConfigLocator;
13 use Symfony\Component\Console\Event\ConsoleCommandEvent;
15 class RsyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface
17 use SiteAliasManagerAwareTrait;
20 * These are arguments after the aliases and paths have been evaluated.
24 public $sourceEvaluatedPath;
26 public $targetEvaluatedPath;
27 /** @var BackendPathEvaluator */
28 protected $pathEvaluator;
30 public function __construct()
32 // TODO: once the BackendInvoke service exists, inject it here
33 // and use it to get the path evaluator
34 $this->pathEvaluator = new BackendPathEvaluator();
38 * Rsync Drupal code or files to/from another server using ssh.
41 * @param $source A site alias and optional path. See rsync documentation and example.site.yml.
42 * @param $target A site alias and optional path. See rsync documentation and example.site.yml.',
43 * @param $extra Additional parameters after the ssh statement.
45 * @option exclude-paths List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows).
46 * @option include-paths List of paths to include, seperated by : (Unix-based systems) or ; (Windows).
47 * @option mode The unary flags to pass to rsync; --mode=rultz implies rsync -rultz. Default is -akz.
48 * @usage drush rsync @dev @stage
49 * Rsync Drupal root from Drush alias dev to the alias stage.
50 * @usage drush rsync ./ @stage:%files/img
51 * Rsync all files in the current directory to the 'img' directory in the file storage folder on the Drush alias stage.
52 * @usage drush rsync @dev @stage -- --exclude=*.sql --delete
53 * Rsync Drupal root from the Drush alias dev to the alias stage, excluding all .sql files and delete all files on the destination that are no longer on the source.
54 * @usage drush rsync @dev @stage --ssh-options="-o StrictHostKeyChecking=no" -- --delete
55 * Customize how rsync connects with remote host via SSH. rsync options like --delete are placed after a --.
56 * @aliases rsync,core-rsync
57 * @topics docs:aliases
59 public function rsync($source, $target, array $extra, $options = ['exclude-paths' => self::REQ, 'include-paths' => self::REQ, 'mode' => 'akz'])
61 // Prompt for confirmation. This is destructive.
62 if (!\Drush\Drush::simulate()) {
63 $this->output()->writeln(dt("You will delete files in !target and replace with data from !source", ['!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!target' => $this->targetEvaluatedPath->fullyQualifiedPath()]));
64 if (!$this->io()->confirm(dt('Do you want to continue?'))) {
65 throw new UserAbortException();
69 $rsync_options = $this->rsyncOptions($options);
70 $parameters = array_merge([$rsync_options], $extra);
71 $parameters[] = $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash();
72 $parameters[] = $this->targetEvaluatedPath->fullyQualifiedPath();
74 $ssh_options = Drush::config()->get('ssh.options', '');
75 $exec = "rsync -e 'ssh $ssh_options'". ' '. implode(' ', array_filter($parameters));
76 $exec_result = drush_op_system($exec);
78 if ($exec_result == 0) {
79 drush_backend_set_result($this->targetEvaluatedPath->fullyQualifiedPath());
81 throw new \Exception(dt("Could not rsync from !source to !dest", ['!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!dest' => $this->targetEvaluatedPath->fullyQualifiedPath()]));
85 public function rsyncOptions($options)
87 $verbose = $paths = '';
88 // Process --include-paths and --exclude-paths options the same way
89 foreach (['include', 'exclude'] as $include_exclude) {
90 // Get the option --include-paths or --exclude-paths and explode to an array of paths
91 // that we will translate into an --include or --exclude option to pass to rsync
92 $inc_ex_path = explode(PATH_SEPARATOR, @$options[$include_exclude . '-paths']);
93 foreach ($inc_ex_path as $one_path_to_inc_ex) {
94 if (!empty($one_path_to_inc_ex)) {
95 $paths .= ' --' . $include_exclude . '="' . $one_path_to_inc_ex . '"';
100 $mode = '-'. $options['mode'];
101 if ($this->output()->isVerbose()) {
103 $verbose = ' --stats --progress';
106 return implode(' ', array_filter([$mode, $verbose, $paths]));
110 * Evaluate the path aliases in the source and destination
111 * parameters. We do this in the pre-command-event so that
112 * we can set up the configuration object to include options
113 * from the source and target aliases, if any, so that these
114 * values may participate in configuration injection.
116 * @hook command-event core:rsync
117 * @param ConsoleCommandEvent $event
121 public function preCommandEvent(ConsoleCommandEvent $event)
123 $input = $event->getInput();
124 $this->sourceEvaluatedPath = $this->injectAliasPathParameterOptions($input, 'source');
125 $this->targetEvaluatedPath = $this->injectAliasPathParameterOptions($input, 'target');
128 protected function injectAliasPathParameterOptions($input, $parameterName)
130 // The Drush configuration object is a ConfigOverlay; fetch the alias
131 // context, that already has the options et. al. from the
132 // site-selection alias ('drush @site rsync ...'), @self.
133 $aliasConfigContext = $this->getConfig()->getContext(ConfigLocator::ALIAS_CONTEXT);
134 $manager = $this->siteAliasManager();
136 $aliasName = $input->getArgument($parameterName);
137 $evaluatedPath = HostPath::create($manager, $aliasName);
138 $this->pathEvaluator->evaluate($evaluatedPath);
140 $aliasRecord = $evaluatedPath->getAliasRecord();
142 // If the path is remote, then we will also inject the global
143 // options into the alias config context so that we pick up
144 // things like ssh-options.
145 if ($aliasRecord->isRemote()) {
146 $aliasConfigContext->combine($aliasRecord->export());
149 return $evaluatedPath;
153 * Validate that passed aliases are valid.
155 * @hook validate core-rsync
156 * @param \Consolidation\AnnotatedCommand\CommandData $commandData
160 public function validate(CommandData $commandData)
162 if ($this->sourceEvaluatedPath->isRemote() && $this->targetEvaluatedPath->isRemote()) {
163 $msg = dt("Cannot specify two remote aliases. Instead, use one of the following alternate options:\n\n `drush {source} rsync @self {target}`\n `drush {source} rsync @self {fulltarget}\n\nUse the second form if the site alias definitions are not available at {source}.", ['source' => $source, 'target' => $target, 'fulltarget' => $this->targetEvaluatedPath->fullyQualifiedPath()]);
164 throw new \Exception($msg);