2 namespace Drush\Commands\sql;
4 use Consolidation\AnnotatedCommand\CommandData;
5 use Drush\Commands\DrushCommands;
7 use Drush\Exceptions\UserAbortException;
8 use Consolidation\SiteAlias\AliasRecord;
9 use Consolidation\SiteAlias\SiteAliasManagerAwareInterface;
10 use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
11 use Symfony\Component\Config\Definition\Exception\Exception;
12 use Webmozart\PathUtil\Path;
14 class SqlSyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface
16 use SiteAliasManagerAwareTrait;
19 * Copy DB data from a source site to a target site. Transfers data via rsync.
23 * @param $source A site-alias or the name of a subdirectory within /sites whose database you want to copy from.
24 * @param $target A site-alias or the name of a subdirectory within /sites whose database you want to replace.
25 * @optionset_table_selection
26 * @option no-dump Do not dump the sql database; always use an existing dump file.
27 * @option no-sync Do not rsync the database dump file from source to target.
28 * @option runner Where to run the rsync command; defaults to the local site. Can also be 'source' or 'target'.
29 * @option create-db Create a new database before importing the database dump on the target machine.
30 * @option db-su Account to use when creating a new database (e.g. root).
31 * @option db-su-pw Password for the db-su account.
32 * @option source-dump The path for retrieving the sql-dump on source machine.
33 * @option target-dump The path for storing the sql-dump on target machine.
34 * @option extra-dump Add custom arguments/options to the dumping of the database (e.g. mysqldump command).
35 * @usage drush sql:sync @source @self
36 * Copy the database from the site with the alias 'source' to the local site.
37 * @usage drush sql:sync @self @target
38 * Copy the database from the local site to the site with the alias 'target'.
39 * @usage drush sql:sync #prod #dev
40 * Copy the database from the site in /sites/prod to the site in /sites/dev (multisite installation).
41 * @topics docs:aliases,docs:policy,docs:configuration,docs:example-sync-via-http
44 public function sqlsync($source, $target, $options = ['no-dump' => false, 'no-sync' => false, 'runner' => self::REQ, 'create-db' => false, 'db-su' => self::REQ, 'db-su-pw' => self::REQ, 'target-dump' => self::REQ, 'source-dump' => self::OPT])
46 $manager = $this->siteAliasManager();
47 $sourceRecord = $manager->get($source);
48 $targetRecord = $manager->get($target);
50 $backend_options = [];
51 $global_options = Drush::redispatchOptions() + ['strict' => 0];
53 // Create target DB if needed.
54 if ($options['create-db']) {
55 $this->logger()->notice(dt('Starting to create database on target.'));
56 $return = drush_invoke_process($target, 'sql-create', [], $global_options, $backend_options);
57 if ($return['error_status']) {
58 throw new \Exception(dt('sql-create failed.'));
62 $source_dump_path = $this->dump($options, $global_options, $sourceRecord, $backend_options);
64 $target_dump_path = $this->rsync($options, $backend_options, $sourceRecord, $targetRecord, $source_dump_path);
66 $this->import($global_options, $target_dump_path, $targetRecord, $backend_options);
70 * @hook validate sql-sync
73 public function validate(CommandData $commandData)
75 $source = $commandData->input()->getArgument('source');
76 $target = $commandData->input()->getArgument('target');
77 // Get target info for confirmation prompt.
78 $manager = $this->siteAliasManager();
79 if (!$sourceRecord = $manager->get($source)) {
80 throw new \Exception(dt('Error: no alias record could be found for source !source', ['!source' => $source]));
82 if (!$targetRecord = $manager->get($target)) {
83 throw new \Exception(dt('Error: no alias record could be found for target !target', ['!target' => $target]));
85 if (!$source_db_name = $this->databaseName($sourceRecord)) {
86 throw new \Exception(dt('Error: no database record could be found for source !source', ['!source' => $source]));
88 if (!$target_db_name = $this->databaseName($targetRecord)) {
89 throw new \Exception(dt('Error: no database record could be found for target !target', ['!target' => $target]));
91 $txt_source = ($sourceRecord->remoteHost() ? $sourceRecord->remoteHost() . '/' : '') . $source_db_name;
92 $txt_target = ($targetRecord->remoteHost() ? $targetRecord->remoteHost() . '/' : '') . $target_db_name;
94 if ($commandData->input()->getOption('no-dump') && !$commandData->input()->getOption('source-dump')) {
95 throw new \Exception(dt('The --source-dump option must be supplied when --no-dump is specified.'));
98 if ($commandData->input()->getOption('no-sync') && !$commandData->input()->getOption('target-dump')) {
99 throw new \Exception(dt('The --target-dump option must be supplied when --no-sync is specified.'));
102 if (!Drush::simulate()) {
103 $this->output()->writeln(dt("You will destroy data in !target and replace with data from !source.", [
104 '!source' => $txt_source,
105 '!target' => $txt_target
107 if (!$this->io()->confirm(dt('Do you really want to continue?'))) {
108 throw new UserAbortException();
113 public function databaseName(AliasRecord $record)
115 if ($record->isRemote() && preg_match('#\.simulated$#', $record->remoteHost())) {
116 return 'simulated_db';
118 $values = drush_invoke_process($record, "core-status", [], [], ['integrate' => false, 'override-simulated' => true]);
119 if (is_array($values) && ($values['error_status'] == 0)) {
120 return $values['object']['db-name'];
125 * Perform sql-dump on source unless told otherwise.
128 * @param $global_options
129 * @param $sourceRecord
130 * @param $backend_options
133 * Path to the source dump file.
136 public function dump($options, $global_options, $sourceRecord, $backend_options)
138 $dump_options = $global_options + [
140 'result-file' => $options['source-dump'] ?: 'auto',
142 if (!$options['no-dump']) {
143 $this->logger()->notice(dt('Starting to dump database on source.'));
144 $return = drush_invoke_process($sourceRecord, 'sql-dump', [], $dump_options, $backend_options);
145 if ($return['error_status']) {
146 throw new \Exception(dt('sql-dump failed.'));
147 } elseif (Drush::simulate()) {
148 $source_dump_path = '/simulated/path/to/dump.tgz';
150 $source_dump_path = $return['object'];
151 if (!is_string($source_dump_path)) {
152 throw new \Exception(dt('The Drush sql-dump command did not report the path to the dump file produced. Try upgrading the version of Drush you are using on the source machine.'));
156 $source_dump_path = $options['source-dump'];
158 return $source_dump_path;
162 * @param array $options
163 * @param array $backend_options
164 * @param AliasRecord $sourceRecord
165 * @param AliasRecord $targetRecord
166 * @param $source_dump_path
168 * Path to the target file.
171 public function rsync($options, $backend_options, AliasRecord $sourceRecord, AliasRecord $targetRecord, $source_dump_path)
173 $do_rsync = !$options['no-sync'];
174 // Determine path/to/dump on target.
175 if ($options['target-dump']) {
176 $target_dump_path = $options['target-dump'];
177 $backend_options['interactive'] = false; // @temporary: See https://github.com/drush-ops/drush/pull/555
178 } elseif (!$sourceRecord->isRemote() && !$targetRecord->isRemote()) {
179 $target_dump_path = $source_dump_path;
182 $tmp = '/tmp'; // Our fallback plan.
183 $this->logger()->notice(dt('Starting to discover temporary files directory on target.'));
184 $return = drush_invoke_process($targetRecord, 'core-status', [], [], ['integrate' => false, 'override-simulated' => true]);
185 if (!$return['error_status'] && isset($return['object']['drush-temp'])) {
186 $tmp = $return['object']['drush-temp'];
188 $target_dump_path = Path::join($tmp, basename($source_dump_path));
189 $backend_options['interactive'] = false; // No need to prompt as target is a tmp file.
194 if (!$options['no-dump']) {
195 // Cleanup if this command created the dump file.
196 $rsync_options[] = '--remove-source-files';
198 if (!$runner = $options['runner']) {
199 $runner = $sourceRecord->isRemote() && $targetRecord->isRemote() ? $targetRecord : '@self';
201 if ($runner == 'source') {
202 $runner = $sourceRecord;
204 if (($runner == 'target') || ($runner == 'destination')) {
205 $runner = $targetRecord;
207 // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync.
208 // Alternatively, add options like ssh.options to a site alias (usually on the machine that initiates the sql-sync).
209 $return = drush_invoke_process($runner, 'core-rsync', array_merge([$sourceRecord->name() . ":$source_dump_path", $targetRecord->name() . ":$target_dump_path", '--'], $rsync_options), [], $backend_options);
210 $this->logger()->notice(dt('Copying dump file from source to target.'));
211 if ($return['error_status']) {
212 throw new \Exception(dt('core-rsync failed.'));
215 return $target_dump_path;
219 * Import file into target.
221 * @param $global_options
222 * @param $target_dump_path
223 * @param $targetRecord
224 * @param $backend_options
226 public function import($global_options, $target_dump_path, $targetRecord, $backend_options)
228 $this->logger()->notice(dt('Starting to import dump file onto target database.'));
229 $query_options = $global_options + [
230 'file' => $target_dump_path,
231 'file-delete' => true,
233 $return = drush_invoke_process($targetRecord, 'sql-query', [], $query_options, $backend_options);
234 if ($return['error_status']) {
235 throw new Exception(dt('Failed to import !dump into target.', ['!dump' => $target_dump_path]));