Skip to content

Commit

Permalink
Fixes #5991: Adds Convert Symlinks Option (#6030) (#6043)
Browse files Browse the repository at this point in the history
* Start with a failing test, to show that we have correctly replicated the problem.

* Convert symlinks in the archive to avoid failures in the PharData class.

* Move test to its own file.

* Remove extraneous test steps in "setup" method of ArchiveTest that are not doing anything not already covered in the archive dump test.

* Remove unnecessary logic

* Update src/Commands/core/ArchiveDumpCommands.php



* Update tests/functional/ArchiveSymlinkTest.php



* Update src/Commands/core/ArchiveDumpCommands.php



---------

Co-authored-by: Greg Anderson <[email protected]>
Co-authored-by: Moshe Weitzman <[email protected]>
  • Loading branch information
3 people authored Jun 20, 2024
1 parent 1cbb21f commit d2f2c67
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 42 deletions.
67 changes: 65 additions & 2 deletions src/Commands/core/ArchiveDumpCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class ArchiveDumpCommands extends DrushCommands
#[CLI\Option(name: 'destination', description: 'The full path and filename in which the archive should be stored. Any relative path will be calculated from Drupal root (usually <info>web</info> for drupal/recommended-project projects). If omitted, it will be saved to the configured temp directory.')]
#[CLI\Option(name: 'overwrite', description: 'Overwrite destination file if exists.')]
#[CLI\Option(name: 'code', description: 'Archive codebase.')]
#[CLI\Option(name: 'convert-symlinks', description: 'Replace all symlinks with copies of the files/directories that they point to. Default is to only convert symlinks that point outside the project root.')]
#[CLI\Option(name: 'exclude-code-paths', description: 'Comma-separated list of paths (or regular expressions matching paths) to exclude from the code archive.')]
#[CLI\Option(name: 'extra-dump', description: 'Add custom arguments/options to the dumping of the database (e.g. <info>mysqldump</info> command).')]
#[CLI\Option(name: 'files', description: 'Archive Drupal files.')]
Expand Down Expand Up @@ -98,6 +99,7 @@ public function dump(array $options = [
'generatorversion' => InputOption::VALUE_REQUIRED,
'exclude-code-paths' => InputOption::VALUE_REQUIRED,
'extra-dump' => self::REQ,
'convert-symlinks' => false,
]): string
{
$this->prepareArchiveDir();
Expand Down Expand Up @@ -129,6 +131,8 @@ public function dump(array $options = [
];
}

$this->convertSymlinks($options['convert-symlinks']);

return $this->createArchiveFile($components, $options);
}

Expand All @@ -146,9 +150,9 @@ protected function prepareArchiveDir(): void
/**
* Creates the archive file and returns the absolute path.
*
* @param array $archiveComponents
* @param $archiveComponents
* The list of components (files) to include into the archive file.
* @param array $options
* @param $options
* The command options.
*
* @return string
Expand All @@ -169,6 +173,7 @@ private function createArchiveFile(array $archiveComponents, array $options): st
$archive = new PharData($archivePath);

$this->createManifestFile($options);

$archive->buildFromDirectory($this->archiveDir);

$this->logger()->info(dt('Compressing archive...'));
Expand Down Expand Up @@ -237,6 +242,64 @@ private function createManifestFile(array $options): void
);
}

/**
* Converts symlinks to the linked files/folders for an archive.
*
* @param bool $convert_symlinks
* Whether to convert all symlinks.
*
*/
public function convertSymlinks(
bool $convert_symlinks,
): void {
// If symlinks are disabled, convert symlinks to full content.
$this->logger()->info(dt('Converting symlinks...'));

$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->archiveDir),
RecursiveIteratorIterator::SELF_FIRST
);

foreach ($iterator as $file) {
if (
$file->isLink() && ($convert_symlinks || strpos(
$file->getLinkTarget(),
$this->archiveDir
) !== 0)
) {
$target = readlink($file->getPathname());

if (is_file($target)) {
$content = file_get_contents($target);
unlink($file->getPathname());
file_put_contents($file->getPathname(), $content);
} elseif (is_dir($target)) {
$path = $file->getPathname();
unlink($path);
mkdir($path, 0755);
foreach (
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$target,
\RecursiveDirectoryIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::SELF_FIRST
) as $item
) {
if ($item->isDir()) {
mkdir($path . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
} else {
copy(
$item->getPathname(),
$path . DIRECTORY_SEPARATOR . $iterator->getSubPathname()
);
}
}
}
}
}
}

/**
* Returns TRUE if the site is a "web" docroot site.
*
Expand Down
70 changes: 70 additions & 0 deletions tests/functional/ArchiveSymlinkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Unish;

use Drush\Commands\core\ArchiveDumpCommands;
use Drush\Commands\core\ArchiveRestoreCommands;
use Drush\Commands\core\StatusCommands;
use PharData;
use Symfony\Component\Filesystem\Path;
use Unish\Utils\FSUtils;

/**
* @group slow
* @group commands
* @group archive
*/
class ArchiveSymlinkTest extends CommandUnishTestCase
{
use FSUtils;

protected string $archivePath;
protected array $archiveDumpOptions;
protected string $linktarget;
protected string $linkdestination;

/**
* @inheritdoc
*/
public function setUp(): void
{
$this->setUpDrupal(1, true);
$this->archiveDumpOptions = [
'db' => null,
'files' => null,
'code' => null,
'exclude-code-paths' => 'sut/sites/.+/settings.php,(?!sut|composer\.json|composer\.lock).*',
];

$this->archivePath = Path::join($this->getSandbox(), 'archive.tar.gz');

$this->linktarget = Path::join($this->getSandbox(), 'symlinktest.txt');
$this->linkdestination = Path::join($this->webroot(), 'symlinkdest.txt');

file_put_contents($this->linktarget, "This is a symlink target file.");
symlink($this->linktarget, $this->linkdestination);
}

public function tearDown(): void
{
unlink($this->linktarget);
unlink($this->linkdestination);
}

public function testArchiveDumpSymlinkReplaceCommand(): void
{
// The symlinks written in setup would cause the PharData class to
// fail if we did not replace them before archiving.
// @see https://github.com/drush-ops/drush/pull/6030
$this->drush(
ArchiveDumpCommands::DUMP,
[],
array_merge($this->archiveDumpOptions, [
'destination' => $this->archivePath,
'overwrite' => null,
])
);
}
}
40 changes: 0 additions & 40 deletions tests/functional/ArchiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ class ArchiveTest extends CommandUnishTestCase
use FSUtils;

protected string $archivePath;
protected string $restorePath;
protected string $extractPath;
protected array $archiveDumpOptions;
protected array $archiveRestoreOptions;
protected array $fixtureDatabaseSettings;

/**
* @inheritdoc
Expand All @@ -39,42 +35,6 @@ public function setUp(): void
];

$this->archivePath = Path::join($this->getSandbox(), 'archive.tar.gz');
$this->drush(
'archive:dump',
[],
array_merge($this->archiveDumpOptions, [
'destination' => $this->archivePath,
'overwrite' => null,
])
);
$actualArchivePath = Path::canonicalize($this->getOutput());
$this->assertEquals($this->archivePath, $actualArchivePath);

$this->restorePath = Path::join($this->getSandbox(), 'restore');
$this->removeDir($this->restorePath);

$this->extractPath = Path::join($this->getSandbox(), 'extract');
$this->removeDir($this->extractPath);
$archive = new PharData($this->archivePath);
$archive->extractTo($this->extractPath);

$this->drush(
'status',
[],
['format' => 'json']
);
$this->fixtureDatabaseSettings = json_decode($this->getOutput(), true);
$this->fixtureDatabaseSettings['db-name'] = 'archive_dump_restore_test_' . mt_rand();
$dbUrlParts = explode(':', self::getDbUrl());
$this->fixtureDatabaseSettings['db-password'] = substr($dbUrlParts[2], 0, (int)strpos($dbUrlParts[2], '@'));
$fixtureDbUrl = self::getDbUrl() . '/' . $this->fixtureDatabaseSettings['db-name'];

$this->archiveRestoreOptions = [
'destination-path' => $this->restorePath,
'overwrite' => null,
'site-subdir' => 'dev',
'db-url' => $fixtureDbUrl,
];
}

public function testArchiveDumpCommand(): void
Expand Down

0 comments on commit d2f2c67

Please sign in to comment.