2 namespace Drush\SiteAlias;
4 use Symfony\Component\Yaml\Yaml;
5 use Dflydev\DotAccessData\Data;
8 * Find all legacy alias files and convert them to an equivalent '.yml' file.
10 * We will check the respective mod date of the legacy file and the generated
11 * file, and update the generated file when the legacy file changes.
13 class LegacyAliasConverter
16 * @var SiteAliasFileDiscovery
33 protected $simulate = false;
38 protected $convertedFileMap = [];
41 * LegacyAliasConverter constructor.
43 * @param SiteAliasFileDiscovery $discovery Provide the same discovery
44 * object as used by the SiteAliasFileLoader to ensure that the same
45 * search locations are used for both classed.
47 public function __construct(SiteAliasFileDiscovery $discovery)
49 $this->discovery = $discovery;
56 public function isSimulate()
58 return $this->simulate;
62 * @param bool $simulate
64 public function setSimulate($simulate)
66 $this->simulate = $simulate;
70 * @param string $target
71 * A directory to write to. If not provided, writes go into same dir as the corresponding legacy file.
73 public function setTargetDir($target)
75 $this->target = $target;
78 public function convertOnce()
80 if ($this->converted) {
83 return $this->convert();
86 public function convert()
88 $this->converted = true;
89 $legacyFiles = $this->discovery->findAllLegacyAliasFiles();
91 if (!$this->checkAnyNeedsConversion($legacyFiles)) {
95 // We reconvert all legacy files together, because the aliases
96 // in the legacy files might be written into multiple different .yml
97 // files, depending on the naming conventions followed.
98 $convertedFiles = $this->convertAll($legacyFiles);
99 $this->writeAll($convertedFiles);
101 return $convertedFiles;
104 protected function checkAnyNeedsConversion($legacyFiles)
106 foreach ($legacyFiles as $legacyFile) {
107 $convertedFile = $this->determineConvertedFilename($legacyFile);
108 if ($this->checkNeedsConversion($legacyFile, $convertedFile)) {
115 protected function convertAll($legacyFiles)
118 foreach ($legacyFiles as $legacyFile) {
119 $convertedFile = $this->determineConvertedFilename($legacyFile);
120 $conversionResult = $this->convertLegacyFile($legacyFile);
121 $result = static::arrayMergeRecursiveDistinct($result, $conversionResult);
123 // If the conversion did not generate a similarly-named .yml file, then
124 // make sure that one is created simply to record the mod date.
125 if (!isset($result[$convertedFile])) {
126 $result[$convertedFile] = [];
132 protected function writeAll($convertedFiles)
134 foreach ($convertedFiles as $path => $data) {
135 $contents = $this->getContents($path, $data);
137 // Write the converted file to the target directory
138 // if a target directory was set.
139 if (!empty($this->target)) {
140 $path = $this->target . '/' . basename($path);
142 $this->writeOne($path, $contents);
146 protected function getContents($path, $data)
150 return Yaml::dump($data, PHP_INT_MAX, $indent, false, true);
153 $recoverSource = $this->recoverLegacyFile($path);
154 if (!$recoverSource) {
155 $recoverSource = 'the source alias file';
158 # This is a placeholder file used to track when $recoverSource was converted.
159 # If you delete $recoverSource, then you may delete this file.
165 protected function writeOne($path, $contents)
167 $checksumPath = $this->checksumPath($path);
168 if ($this->safeToWrite($path, $contents, $checksumPath)) {
169 file_put_contents($path, $contents);
170 $this->saveChecksum($checksumPath, $path, $contents);
175 * Without any safeguards, the conversion process could be very
176 * dangerous to users who modify their converted alias files (as we
177 * would encourage them to do, if the goal is to convert!).
179 * This method determines whether it is safe to write to the converted
180 * alias file at the specified path. If the user has modified the target
181 * file, then we will not overwrite it.
183 protected function safeToWrite($path, $contents, $checksumPath)
185 // Bail if simulate mode is enabled.
186 if ($this->isSimulate()) {
190 // If the target file does not exist, it is always safe to write.
191 if (!file_exists($path)) {
195 // If the user deletes the checksum file, then we will never
196 // overwrite the file again. This also covers potential collisions,
197 // where the user might not realize that a legacy alias file
198 // would write to a new site.yml file they created manually.
199 if (!file_exists($checksumPath)) {
203 // Read the data that exists at the target path, and calculate
204 // the checksum of what exists there.
205 $previousContents = file_get_contents($path);
206 $previousChecksum = $this->calculateChecksum($previousContents);
207 $previousWrittenChecksum = $this->readChecksum($checksumPath);
209 // If the checksum of what we wrote before is the same as
210 // the checksum we cached in the checksum file, then there has
211 // been no user modification of this file, and it is safe to
213 return $previousChecksum == $previousWrittenChecksum;
216 public function saveChecksum($checksumPath, $path, $contents)
218 $name = basename($path);
220 # Checksum for converted Drush alias file $name.
221 # Delete this checksum file or modify $name to prevent further updates to it.
223 $checksum = $this->calculateChecksum($contents);
224 @mkdir(dirname($checksumPath));
225 file_put_contents($checksumPath, "{$comment}\n{$checksum}");
228 protected function readChecksum($checksumPath)
230 $checksumContents = file_get_contents($checksumPath);
231 $checksumContents = preg_replace('/^#.*/m', '', $checksumContents);
233 return trim($checksumContents);
236 protected function checksumPath($path)
238 return dirname($path) . '/.checksums/' . basename($path, '.yml') . '.md5';
241 protected function calculateChecksum($data)
246 protected function determineConvertedFilename($legacyFile)
248 $convertedFile = preg_replace('#\.alias(|es)\.drushrc\.php$#', '.site.yml', $legacyFile);
249 // Sanity check: if no replacement was done on the filesystem, then
250 // we will presume that no conversion is needed here after all.
251 if ($convertedFile == $legacyFile) {
254 // If a target directory was set, then the converted file will
255 // be written there. This will be done in writeAll(); we will strip
256 // off everything except for the basename here. If no target
257 // directory was set, then we will keep the path to the converted
258 // file so that it may be written to the correct location.
259 if (!empty($this->target)) {
260 $convertedFile = basename($convertedFile);
262 $this->cacheConvertedFilePath($legacyFile, $convertedFile);
263 return $convertedFile;
266 protected function cacheConvertedFilePath($legacyFile, $convertedFile)
268 $this->convertedFileMap[basename($convertedFile)] = basename($legacyFile);
271 protected function recoverLegacyFile($convertedFile)
273 if (!isset($this->convertedFileMap[basename($convertedFile)])) {
276 return $this->convertedFileMap[basename($convertedFile)];
279 protected function checkNeedsConversion($legacyFile, $convertedFile)
281 // If determineConvertedFilename did not return a valid result,
282 // then force no conversion.
283 if (!$convertedFile) {
287 // Sanity check: the source file must exist.
288 if (!file_exists($legacyFile)) {
292 // If the target file does not exist, then force a conversion
293 if (!file_exists($convertedFile)) {
297 // We need to re-convert if the legacy file has been modified
298 // more recently than the converted file.
299 return filemtime($legacyFile) > filemtime($convertedFile);
302 protected function convertLegacyFile($legacyFile)
306 // Include the legacy file. In theory, this will define $aliases &/or $options.
307 if (((@include $legacyFile) === false) || (!isset($aliases) && !isset($options))) {
308 // TODO: perhaps we should log a warning?
312 // Decide whether this is a single-alias file or a multiple-alias file.
313 if (preg_match('#\.alias\.drushrc\.php$#', $legacyFile)) {
314 return $this->convertSingleAliasLegacyFile($legacyFile, $options ?: current($aliases));
316 return $this->convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options);
319 protected function convertSingleAliasLegacyFile($legacyFile, $options)
321 $aliasName = basename($legacyFile, '.alias.drushrc.php');
323 return $this->convertAlias($aliasName, $options, dirname($legacyFile));
326 protected function convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options)
329 foreach ($aliases as $aliasName => $data) {
330 // 'array_merge' is how Drush 8 combines these records.
331 $data = array_merge($options, $data);
332 $convertedAlias = $this->convertAlias($aliasName, $data, dirname($legacyFile));
333 $result = static::arrayMergeRecursiveDistinct($result, $convertedAlias);
338 protected function convertAlias($aliasName, $data, $dir = '')
341 // We allow $aliasname to be:
344 // - group.sitename.env
345 // In the case of the last, we will convert to
346 // 'group-sitename.env' (and so on for any additional dots).
347 // First, we will strip off the 'env' if it is present.
348 if (preg_match('/(.*)\.([^.]+)$/', $aliasName, $matches)) {
349 $aliasName = $matches[1];
352 // Convert all remaining dots to dashes.
353 $aliasName = strtr($aliasName, '.', '-');
355 $data = $this->fixSiteData($data);
357 return $this->convertSingleFileAlias($aliasName, $env, $data, $dir);
360 protected function fixSiteData($data)
362 $keyMap = $this->keyConversion();
365 foreach ($data as $key => $value) {
366 if ($key[0] == '#') {
368 } elseif (!isset($keyMap[$key])) {
369 $options[$key] = $data[$key];
375 foreach ($keyMap as $fromKey => $toKey) {
376 if (isset($data[$fromKey]) && ($fromKey != $toKey)) {
377 $data[$toKey] = $data[$fromKey];
378 unset($data[$fromKey]);
382 if (!empty($options)) {
383 $data['options'] = $options;
385 if (isset($data['paths'])) {
386 $data['paths'] = $this->removePercentFromKey($data['paths']);
390 return $this->remapData($data);
393 protected function remapData($data)
395 $converter = new Data($data);
397 foreach ($this->dataRemap() as $from => $to) {
398 if ($converter->has($from)) {
399 $converter->set($to, $converter->get($from));
400 $converter->remove($from);
404 return $converter->export();
408 * Anything in the key of the returned array is converted
409 * and written to a new top-level item in the result.
411 * Anything NOT identified by the key in the returned array
412 * is moved to the 'options' element.
414 protected function keyConversion()
417 'remote-host' => 'host',
418 'remote-user' => 'user',
421 'path-aliases' => 'paths',
426 * This table allows for flexible remapping from one location
427 * in the original alias to any other location in the target
430 * n.b. Most arbitrary data from the original alias will have
431 * been moved into the 'options' element before this remapping
432 * table is consulted.
434 protected function dataRemap()
437 'options.ssh-options' => 'ssh.options',
441 protected function removePercentFromKey($data)
447 return ltrim($item, '%');
454 protected function convertSingleFileAlias($aliasName, $env, $data, $dir = '')
456 $filename = $this->outputFilename($aliasName, '.site.yml', $dir);
464 protected function outputFilename($name, $extension, $dir = '')
466 $filename = "{$name}{$extension}";
467 // Just return the filename part if no directory was provided. Also,
468 // the directoy is irrelevant if a target directory is set.
469 if (empty($dir) || !empty($this->target)) {
472 return "$dir/$filename";
476 * Merges arrays recursively while preserving.
478 * @param array $array1
479 * @param array $array2
483 * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
484 * @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
486 protected static function arrayMergeRecursiveDistinct(
491 foreach ($array2 as $key => &$value) {
492 $merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
499 * Process the value in an arrayMergeRecursiveDistinct - make a recursive
502 private static function mergeRecursiveValue(&$merged, $key, $value)
504 if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
505 return self::arrayMergeRecursiveDistinct($merged[$key], $value);