2 namespace Drush\SiteAlias;
4 use Symfony\Component\Yaml\Yaml;
5 use Dflydev\DotAccessData\Data;
6 use Consolidation\SiteAlias\SiteAliasFileDiscovery;
9 * Find all legacy alias files and convert them to an equivalent '.yml' file.
11 * We will check the respective mod date of the legacy file and the generated
12 * file, and update the generated file when the legacy file changes.
14 class LegacyAliasConverter
17 * @var SiteAliasFileDiscovery
34 protected $simulate = false;
39 protected $convertedFileMap = [];
42 * LegacyAliasConverter constructor.
44 * @param SiteAliasFileDiscovery $discovery Provide the same discovery
45 * object as used by the SiteAliasFileLoader to ensure that the same
46 * search locations are used for both classed.
48 public function __construct(SiteAliasFileDiscovery $discovery)
50 $this->discovery = $discovery;
57 public function isSimulate()
59 return $this->simulate;
63 * @param bool $simulate
65 public function setSimulate($simulate)
67 $this->simulate = $simulate;
71 * @param string $target
72 * A directory to write to. If not provided, writes go into same dir as the corresponding legacy file.
74 public function setTargetDir($target)
76 $this->target = $target;
79 public function convertOnce()
81 if ($this->converted) {
84 return $this->convert();
87 public function convert()
89 $this->converted = true;
90 $legacyFiles = $this->discovery->findAllLegacyAliasFiles();
92 if (!$this->checkAnyNeedsConversion($legacyFiles)) {
96 // We reconvert all legacy files together, because the aliases
97 // in the legacy files might be written into multiple different .yml
98 // files, depending on the naming conventions followed.
99 $convertedFiles = $this->convertAll($legacyFiles);
100 $this->writeAll($convertedFiles);
102 return $convertedFiles;
105 protected function checkAnyNeedsConversion($legacyFiles)
107 foreach ($legacyFiles as $legacyFile) {
108 $convertedFile = $this->determineConvertedFilename($legacyFile);
109 if ($this->checkNeedsConversion($legacyFile, $convertedFile)) {
116 protected function convertAll($legacyFiles)
119 foreach ($legacyFiles as $legacyFile) {
120 $convertedFile = $this->determineConvertedFilename($legacyFile);
121 $conversionResult = $this->convertLegacyFile($legacyFile);
122 $result = static::arrayMergeRecursiveDistinct($result, $conversionResult);
124 // If the conversion did not generate a similarly-named .yml file, then
125 // make sure that one is created simply to record the mod date.
126 if (!isset($result[$convertedFile])) {
127 $result[$convertedFile] = [];
133 protected function writeAll($convertedFiles)
135 foreach ($convertedFiles as $path => $data) {
136 $contents = $this->getContents($path, $data);
138 // Write the converted file to the target directory
139 // if a target directory was set.
140 if (!empty($this->target)) {
141 $path = $this->target . '/' . basename($path);
143 $this->writeOne($path, $contents);
147 protected function getContents($path, $data)
151 return Yaml::dump($data, PHP_INT_MAX, $indent, false, true);
154 $recoverSource = $this->recoverLegacyFile($path);
155 if (!$recoverSource) {
156 $recoverSource = 'the source alias file';
159 # This is a placeholder file used to track when $recoverSource was converted.
160 # If you delete $recoverSource, then you may delete this file.
166 protected function writeOne($path, $contents)
168 $checksumPath = $this->checksumPath($path);
169 if ($this->safeToWrite($path, $contents, $checksumPath)) {
170 file_put_contents($path, $contents);
171 $this->saveChecksum($checksumPath, $path, $contents);
176 * Without any safeguards, the conversion process could be very
177 * dangerous to users who modify their converted alias files (as we
178 * would encourage them to do, if the goal is to convert!).
180 * This method determines whether it is safe to write to the converted
181 * alias file at the specified path. If the user has modified the target
182 * file, then we will not overwrite it.
184 protected function safeToWrite($path, $contents, $checksumPath)
186 // Bail if simulate mode is enabled.
187 if ($this->isSimulate()) {
191 // If the target file does not exist, it is always safe to write.
192 if (!file_exists($path)) {
196 // If the user deletes the checksum file, then we will never
197 // overwrite the file again. This also covers potential collisions,
198 // where the user might not realize that a legacy alias file
199 // would write to a new site.yml file they created manually.
200 if (!file_exists($checksumPath)) {
204 // Read the data that exists at the target path, and calculate
205 // the checksum of what exists there.
206 $previousContents = file_get_contents($path);
207 $previousChecksum = $this->calculateChecksum($previousContents);
208 $previousWrittenChecksum = $this->readChecksum($checksumPath);
210 // If the checksum of what we wrote before is the same as
211 // the checksum we cached in the checksum file, then there has
212 // been no user modification of this file, and it is safe to
214 return $previousChecksum == $previousWrittenChecksum;
217 public function saveChecksum($checksumPath, $path, $contents)
219 $name = basename($path);
221 # Checksum for converted Drush alias file $name.
222 # Delete this checksum file or modify $name to prevent further updates to it.
224 $checksum = $this->calculateChecksum($contents);
225 @mkdir(dirname($checksumPath));
226 file_put_contents($checksumPath, "{$comment}\n{$checksum}");
229 protected function readChecksum($checksumPath)
231 $checksumContents = file_get_contents($checksumPath);
232 $checksumContents = preg_replace('/^#.*/m', '', $checksumContents);
234 return trim($checksumContents);
237 protected function checksumPath($path)
239 return dirname($path) . '/.checksums/' . basename($path, '.yml') . '.md5';
242 protected function calculateChecksum($data)
247 protected function determineConvertedFilename($legacyFile)
249 $convertedFile = preg_replace('#\.alias(|es)\.drushrc\.php$#', '.site.yml', $legacyFile);
250 // Sanity check: if no replacement was done on the filesystem, then
251 // we will presume that no conversion is needed here after all.
252 if ($convertedFile == $legacyFile) {
255 // If a target directory was set, then the converted file will
256 // be written there. This will be done in writeAll(); we will strip
257 // off everything except for the basename here. If no target
258 // directory was set, then we will keep the path to the converted
259 // file so that it may be written to the correct location.
260 if (!empty($this->target)) {
261 $convertedFile = basename($convertedFile);
263 $this->cacheConvertedFilePath($legacyFile, $convertedFile);
264 return $convertedFile;
267 protected function cacheConvertedFilePath($legacyFile, $convertedFile)
269 $this->convertedFileMap[basename($convertedFile)] = basename($legacyFile);
272 protected function recoverLegacyFile($convertedFile)
274 if (!isset($this->convertedFileMap[basename($convertedFile)])) {
277 return $this->convertedFileMap[basename($convertedFile)];
280 protected function checkNeedsConversion($legacyFile, $convertedFile)
282 // If determineConvertedFilename did not return a valid result,
283 // then force no conversion.
284 if (!$convertedFile) {
288 // Sanity check: the source file must exist.
289 if (!file_exists($legacyFile)) {
293 // If the target file does not exist, then force a conversion
294 if (!file_exists($convertedFile)) {
298 // We need to re-convert if the legacy file has been modified
299 // more recently than the converted file.
300 return filemtime($legacyFile) > filemtime($convertedFile);
303 protected function convertLegacyFile($legacyFile)
307 // Include the legacy file. In theory, this will define $aliases &/or $options.
308 if (((@include $legacyFile) === false) || (!isset($aliases) && !isset($options))) {
309 // TODO: perhaps we should log a warning?
313 // Decide whether this is a single-alias file or a multiple-alias file.
314 if (preg_match('#\.alias\.drushrc\.php$#', $legacyFile)) {
315 return $this->convertSingleAliasLegacyFile($legacyFile, $options ?: current($aliases));
317 return $this->convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options);
320 protected function convertSingleAliasLegacyFile($legacyFile, $options)
322 $aliasName = basename($legacyFile, '.alias.drushrc.php');
324 return $this->convertAlias($aliasName, $options, dirname($legacyFile));
327 protected function convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options)
330 foreach ($aliases as $aliasName => $data) {
331 // 'array_merge' is how Drush 8 combines these records.
332 $data = array_merge($options, $data);
333 $convertedAlias = $this->convertAlias($aliasName, $data, dirname($legacyFile));
334 $result = static::arrayMergeRecursiveDistinct($result, $convertedAlias);
339 protected function convertAlias($aliasName, $data, $dir = '')
342 // We allow $aliasname to be:
345 // - group.sitename.env
346 // In the case of the last, we will convert to
347 // 'group-sitename.env' (and so on for any additional dots).
348 // First, we will strip off the 'env' if it is present.
349 if (preg_match('/(.*)\.([^.]+)$/', $aliasName, $matches)) {
350 $aliasName = $matches[1];
353 // Convert all remaining dots to dashes.
354 $aliasName = strtr($aliasName, '.', '-');
356 $data = $this->fixSiteData($data);
358 return $this->convertSingleFileAlias($aliasName, $env, $data, $dir);
361 protected function fixSiteData($data)
363 $keyMap = $this->keyConversion();
366 foreach ($data as $key => $value) {
367 if ($key[0] == '#') {
369 } elseif (!isset($keyMap[$key])) {
370 $options[$key] = $data[$key];
376 foreach ($keyMap as $fromKey => $toKey) {
377 if (isset($data[$fromKey]) && ($fromKey != $toKey)) {
378 $data[$toKey] = $data[$fromKey];
379 unset($data[$fromKey]);
383 if (!empty($options)) {
384 $data['options'] = $options;
386 if (isset($data['paths'])) {
387 $data['paths'] = $this->removePercentFromKey($data['paths']);
391 return $this->remapData($data);
394 protected function remapData($data)
396 $converter = new Data($data);
398 foreach ($this->dataRemap() as $from => $to) {
399 if ($converter->has($from)) {
400 $converter->set($to, $converter->get($from));
401 $converter->remove($from);
405 return $converter->export();
409 * Anything in the key of the returned array is converted
410 * and written to a new top-level item in the result.
412 * Anything NOT identified by the key in the returned array
413 * is moved to the 'options' element.
415 protected function keyConversion()
418 'remote-host' => 'host',
419 'remote-user' => 'user',
422 'path-aliases' => 'paths',
427 * This table allows for flexible remapping from one location
428 * in the original alias to any other location in the target
431 * n.b. Most arbitrary data from the original alias will have
432 * been moved into the 'options' element before this remapping
433 * table is consulted.
435 protected function dataRemap()
438 'options.ssh-options' => 'ssh.options',
442 protected function removePercentFromKey($data)
448 return ltrim($item, '%');
455 protected function convertSingleFileAlias($aliasName, $env, $data, $dir = '')
457 $filename = $this->outputFilename($aliasName, '.site.yml', $dir);
465 protected function outputFilename($name, $extension, $dir = '')
467 $filename = "{$name}{$extension}";
468 // Just return the filename part if no directory was provided. Also,
469 // the directoy is irrelevant if a target directory is set.
470 if (empty($dir) || !empty($this->target)) {
473 return "$dir/$filename";
477 * Merges arrays recursively while preserving.
479 * @param array $array1
480 * @param array $array2
484 * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
485 * @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
487 protected static function arrayMergeRecursiveDistinct(
492 foreach ($array2 as $key => &$value) {
493 $merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
500 * Process the value in an arrayMergeRecursiveDistinct - make a recursive
503 private static function mergeRecursiveValue(&$merged, $key, $value)
505 if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
506 return self::arrayMergeRecursiveDistinct($merged[$key], $value);