
| Current Path : /var/www/html/vendor/drush/drush/src/Commands/core/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/html/vendor/drush/drush/src/Commands/core/ArchiveRestoreCommands.php |
<?php
namespace Drush\Commands\core;
use Consolidation\SiteAlias\SiteAlias;
use Consolidation\SiteAlias\SiteAliasManagerAwareInterface;
use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Drupal;
use DrupalFinder\DrupalFinder;
use Drush\Backend\BackendPathEvaluator;
use Drush\Boot\DrupalBootLevels;
use Drush\Commands\DrushCommands;
use Drush\Drush;
use Drush\Exceptions\UserAbortException;
use Consolidation\SiteAlias\HostPath;
use Drush\Sql\SqlBase;
use Drush\Utils\FsUtils;
use Exception;
use PharData;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Throwable;
class ArchiveRestoreCommands extends DrushCommands implements SiteAliasManagerAwareInterface
{
use SiteAliasManagerAwareTrait;
private Filesystem $filesystem;
private array $siteStatus;
private ?string $destinationPath = null;
private const COMPONENT_CODE = 'code';
private const COMPONENT_FILES = 'files';
private const COMPONENT_DATABASE = 'database';
private const SQL_DUMP_FILE_NAME = 'database.sql';
private const SITE_SUBDIR = 'default';
private const TEMP_DIR_NAME = 'uncompressed';
/**
* Restore (import) your code, files, and database.
*
* @command archive:restore
* @validate-php-extension Phar
* @aliases arr
*
* @option destination-path The base path to restore the code/files into.
* @option overwrite Overwrite files if exists when un-compressing an archive.
* @option site-subdir Site subdirectory to put settings.local.php into.
* @option setup-database-connection Sets up the database connection in settings.local.php file if either --db-url option or set of specific --db-* options are provided.
* @option code Import code.
* @option code-source-path Import code from specified directory. Has higher priority over "path" argument.
* @option files Import Drupal files.
* @option files-source-path Import Drupal files from specified directory. Has higher priority over "path" argument.
* @option files-destination-relative-path Import Drupal files into specified directory relative to Composer root.
* @option db Import database.
* @option db-source-path Import database from specified dump file. Has higher priority over "path" argument.
* @option db-name Destination database name.
* @option db-port Destination database port.
* @option db-host Destination database host.
* @option db-user Destination database user.
* @option db-password Destination database user password.
* @option db-prefix Destination database prefix.
* @option db-driver Destination database driver.
*
* @usage drush archive:restore /path/to/archive.tar.gz
* Restore the site from /path/to/archive.tar.gz archive file.
* @usage drush archive:restore /path/to/archive.tar.gz --destination-path=/path/to/restore
* Restore the site from /path/to/archive.tar.gz archive file into /path/to/restore directory.
* @usage drush archive:restore /path/to/archive.tar.gz --code --destination-path=/path/to/restore
* Restore the code from /path/to/archive.tar.gz archive file into /path/to/restore directory.
* @usage drush archive:restore /path/to/archive.tar.gz --code-source-path=/code/source/path
* Restore database and Drupal files from /path/to/archive.tar.gz archive file and the code from /code/source/path directory.
* @usage drush archive:restore /path/to/archive.tar.gz --files --destination-path=/path/to/restore
* Restore the Drupal files from /path/to/archive.tar.gz archive file into /path/to/restore directory
* @usage drush archive:restore /path/to/archive.tar.gz --files-source-path=/files/source/path
* Restore code and database from /path/to/archive.tar.gz archive file and the Drupal files from /files/source/path directory.
* @usage drush archive:restore /path/to/archive.tar.gz --files-destination-relative-path=web/site/foo-bar/files
* Restore the Drupal files from /path/to/archive.tar.gz archive file into web/site/foo-bar/files site's subdirectory.
* @usage drush archive:restore /path/to/archive.tar.gz --db
* Restore the database from /path/to/archive.tar.gz archive file.
* @usage drush archive:restore /path/to/archive.tar.gz --db-source-path=/path/to/database.sql
* Restore code and Drupal files from /path/to/archive.tar.gz archive file and the database from /path/to/database.sql dump file.
* @usage drush archive:restore /path/to/archive.tar.gz --db-url=mysql://user:password@localhost/database_name --destination-path=/path/to/restore
* Restore code, database and Drupal files from /path/to/archive.tar.gz archive file into /path/to/restore directory using database URL.
*
* @optionset_sql
* @optionset_table_selection
*
* @bootstrap none
*
* @param string|null $path
* The full path to a single archive file (*.tar.gz) or a directory with components to import.
* May contain the following components generated by `archive:dump` command:
* 1) code ("code" directory);
* 2) database dump file ("database/database.sql" file);
* 3) Drupal files ("files" directory).
* @param string|null $site
* Destination site alias. Defaults to @self.
* @param array $options
*
* @throws \Exception
*/
public function restore(
string $path = null,
?string $site = null,
array $options = [
'destination-path' => null,
'overwrite' => false,
'site-subdir' => self::SITE_SUBDIR,
'setup-database-connection' => true,
'code' => false,
'code-source-path' => null,
'files' => false,
'files-source-path' => null,
'files-destination-relative-path' => null,
'db' => false,
'db-source-path' => null,
'db-driver' => 'mysql',
'db-port' => null,
'db-host' => null,
'db-name' => null,
'db-user' => null,
'db-password' => null,
'db-prefix' => null,
]
): void {
$siteAlias = $this->getSiteAlias($site);
if (!$siteAlias->isLocal()) {
throw new Exception(
dt(
'Could not restore archive !path into site !site: restoring an archive into a local site is not supported.',
['!path' => $path, '!site' => $site]
)
);
}
if (!$options['code'] && !$options['files'] && !$options['db']) {
$options['code'] = $options['files'] = $options['db'] = true;
}
if (($options['code'] || $options['files']) && !self::programExists('rsync')) {
throw new Exception(
dt('Could not restore the code or the Drupal files: "rsync" program not found')
);
}
$this->filesystem = new Filesystem();
$extractDir = $this->getExtractDir($path);
foreach (['code' => 'code', 'db' => 'database', 'files' => 'files'] as $component => $label) {
if (!$options[$component]) {
continue;
}
// Validate requested components have sources.
if (null === $extractDir && null === $options[$component . '-source-path']) {
throw new Exception(
dt(
'Missing either "path" input or "!component_path" option for the !label component.',
[
'!component' => $component,
'!label' => $label,
]
)
);
}
}
if ($options['destination-path']) {
$this->destinationPath = $options['destination-path'];
}
// If the destination path was not specified, extract over the current site
if (!$this->destinationPath) {
$bootstrapManager = Drush::bootstrapManager();
$this->destinationPath = $bootstrapManager->getComposerRoot();
}
// If there isn't a current site either, then extract to the cwd
if (!$this->destinationPath) {
$siteDirName = basename(basename($path, '.tgz'), 'tar.gz');
$this->destinationPath = Path::join(getcwd(), $siteDirName);
}
if ($options['code'] && is_dir($this->destinationPath)) {
if (!$options['overwrite']) {
throw new Exception(
dt('Destination path !path already exists (use "--overwrite" option).', ['!path' => $this->destinationPath])
);
}
if (
!$this->io()->confirm(
dt(
'Destination path !path already exists. Are you sure you want to delete !path directory before restoring the archive into it?',
[
'!path' => $this->destinationPath,
]
)
)
) {
throw new UserAbortException();
}
// Remove destination if --overwrite option is set.
$this->filesystem->remove($this->destinationPath);
}
// Create the destination if it does not already exist
if (!is_dir($this->destinationPath) && !mkdir($this->destinationPath)) {
throw new Exception(dt('Failed creating destination directory "!destination"', ['!destination' => $this->destinationPath]));
}
$this->destinationPath = realpath($this->destinationPath);
if ($options['code']) {
$codeComponentPath = $options['code-source-path'] ?? Path::join($extractDir, self::COMPONENT_CODE);
$this->importCode($codeComponentPath);
}
if ($options['files']) {
$filesComponentPath = $options['files-source-path'] ?? Path::join($extractDir, self::COMPONENT_FILES);
$this->importFiles($filesComponentPath, $options);
}
if ($options['db']) {
$databaseComponentPath = $options['db-source-path'] ?? Path::join($extractDir, self::COMPONENT_DATABASE, self::SQL_DUMP_FILE_NAME);
$this->importDatabase($databaseComponentPath, $options);
}
$this->logger()->info(dt('Done!'));
}
/**
* Extracts the archive.
*
* @param string|null $path
* The path to the archive file.
*
* @return string|null
*
* @throws \Exception
*/
protected function getExtractDir(?string $path): ?string
{
if (null === $path) {
return null;
}
if (is_dir($path)) {
return $path;
}
$this->logger()->info('Extracting the archive...');
if (!is_file($path)) {
throw new Exception(dt('File !path is not found.', ['!path' => $path]));
}
if (!preg_match('/\.tar\.gz$/', $path) && !preg_match('/\.tgz$/', $path)) {
throw new Exception(dt('File !path is not a *.tar.gz file.', ['!path' => $path]));
}
['filename' => $archiveFileName] = pathinfo($path);
$archiveFileName = str_replace('.tar', '', $archiveFileName);
$extractDir = Path::join(FsUtils::tmpDir(), $archiveFileName);
$this->filesystem->mkdir($extractDir);
$archive = new PharData($path);
$archive->extractTo($extractDir);
$this->logger()->info(dt('The archive successfully extracted into !path', ['!path' => $extractDir]));
return $extractDir;
}
/**
* Imports the code to the site.
*
* @param string $source
* The path to the code files directory.
*
* @throws \Exception
*/
protected function importCode(string $source): void
{
$this->logger()->info('Importing code...');
if (!is_dir($source)) {
throw new Exception(dt('Directory !path not found.', ['!path' => $source]));
}
$this->rsyncFiles($source, $this->getDestinationPath());
$composerJsonPath = Path::join($this->getDestinationPath(), 'composer.json');
if (is_file($composerJsonPath)) {
$this->logger()->success(
dt('composer.json is found (!path), install Composer dependencies with composer install.'),
['!path' => $composerJsonPath]
);
}
}
/**
* Imports Drupal files to the site.
*
* @param string $source
* The path to the source directory.
* @param array $options
* The options.
*
* @throws \Exception
*/
protected function importFiles(string $source, array $options): void
{
$this->logger()->info('Importing files...');
if (!is_dir($source)) {
throw new Exception(dt('The source directory !path not found for files.', ['!path' => $source]));
}
$destinationAbsolute = $this->fileImportAbsolutePath($options['files-destination-relative-path']);
if (
is_dir($destinationAbsolute) &&
(!$options['code'] || !$options['overwrite']) &&
!$this->io()->confirm(
dt(
'Destination Drupal files path !path already exists. Are you sure you want restore Drupal files archive into it?',
[
'!path' => $destinationAbsolute,
]
)
)
) {
throw new UserAbortException();
}
$this->filesystem->mkdir($destinationAbsolute);
$this->rsyncFiles($source, $destinationAbsolute);
}
/**
* Determines the path where files should be extracted.
*
* @param null|string $destinationRelative
* The relative path to the Drupal files directory.
*
* @return string
* The absolute path to the Drupal files directory.
*
* @throws \Exception
*/
protected function fileImportAbsolutePath(?string $destinationRelative): string
{
// If the user specified the path to the files directory, use that.
if ($destinationRelative) {
return Path::join($this->getDestinationPath(), $destinationRelative);
}
// If we are extracting over an existing site, query Drupal to get the files path
$bootstrapManager = Drush::bootstrapManager();
$path = $bootstrapManager->getComposerRoot();
if (!empty($path)) {
try {
$bootstrapManager->doBootstrap(DrupalBootLevels::FULL);
return Drupal::service('file_system')->realpath('public://');
} catch (Throwable $t) {
$this->logger()->warning('Could not bootstrap Drupal site at destination to determine file path');
}
}
// Find the Drupal root for the archived code, and assume sites/default/files.
$drupalRootPath = $this->getDrupalRootPath();
if ($drupalRootPath) {
return Path::join($drupalRootPath, 'sites/default/files');
}
throw new Exception(
dt(
'Can\'t detect relative path for Drupal files for destination "!destination": missing --files-destination-relative-path option.',
['!destination' => $this->getDestinationPath()]
)
);
}
/**
* Returns the absolute path to Drupal root.
*
* @return string|null
*/
protected function getDrupalRootPath(): ?string
{
$composerRoot = $this->getDestinationPath();
$drupalFinder = new DrupalFinder();
if (!$drupalFinder->locateRoot($composerRoot)) {
return null;
}
return $drupalFinder->getDrupalRoot();
}
/**
* Returns the destination path.
*
* @return string
*/
protected function getDestinationPath(): string
{
return $this->destinationPath;
}
/**
* Returns SiteAlias object by the site alias name.
*
* @param string|null $site
* The site alias.
*
* @return \Consolidation\SiteAlias\SiteAlias
*
* @throws \Exception
*/
protected function getSiteAlias(?string $site): SiteAlias
{
$pathEvaluator = new BackendPathEvaluator();
/** @var \Consolidation\SiteAlias\SiteAliasManager $manager */
$manager = $this->siteAliasManager();
if (null !== $site) {
$site .= ':%root';
}
$evaluatedPath = HostPath::create($manager, $site);
$pathEvaluator->evaluate($evaluatedPath);
return $evaluatedPath->getSiteAlias();
}
/**
* Copies files from the source to the destination.
*
* @param string $source
* The source path.
* @param string $destination
* The destination path.
*
* @throws \Exception
*/
protected function rsyncFiles(string $source, string $destination): void
{
$source = rtrim($source, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$destination = rtrim($destination, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (
!$this->io()->confirm(
dt(
'Are you sure you want to sync files from "!source" to "!destination"?',
[
'!source' => $source,
'!destination' => $destination,
]
)
)
) {
throw new UserAbortException();
}
if (!is_dir($source)) {
throw new Exception(dt('The source directory !path not found.', ['!path' => $source]));
}
$this->logger()->info(
dt(
'Copying files from "!source" to "!destination"...',
[
'!source' => $source,
'!destination' => $destination,
]
)
);
$options[] = '-akz';
if ($this->output()->isVerbose()) {
$options[] = '--stats';
$options[] = '--progress';
$options[] = '-v';
}
$command = sprintf(
'rsync %s %s %s',
implode(' ', $options),
$source,
$destination
);
/** @var \Consolidation\SiteProcess\ProcessBase $process */
$process = $this->processManager()->shell($command);
$process->run($process->showRealtime());
if ($process->isSuccessful()) {
return;
}
throw new Exception(
dt(
'Failed to copy files from !source to !destination: !error',
[
'!source' => $source,
'!destination' => $destination,
'!error' => $process->getErrorOutput(),
]
)
);
}
/**
* Imports the database dump to the site.
*
* @param string $databaseDumpPath
* The path to the database dump file.
* @param array $options
* The command options.
*
* @throws \Drush\Exceptions\UserAbortException
* @throws \Exception
*/
protected function importDatabase(string $databaseDumpPath, array $options): void
{
$this->logger()->info('Importing database...');
if (!is_file($databaseDumpPath)) {
throw new Exception(dt('Database dump file !path not found.', ['!path' => $databaseDumpPath]));
}
$sqlOptions = [];
if (isset($options['db-url'])) {
$sqlOptions = ['db-url' => $options['db-url']];
} else if ($options['db-name']) {
$connection = [
'driver' => $options['db-driver'],
'port' => $options['db-port'],
'prefix' => $options['db-prefix'],
'host' => $options['db-host'],
'database' => $options['db-name'],
'username' => $options['db-user'],
'password' => $options['db-password'],
];
$sqlOptions = [
'databases' => [
'default' => [
'default' => $connection,
],
],
];
} else if ($options['destination-path']) {
throw new Exception('Database connection settings are required if --destination-path option is provided');
} else {
$bootstrapManager = Drush::bootstrapManager();
$bootstrapManager->doBootstrap(DrupalBootLevels::CONFIGURATION);
}
try {
$sql = SqlBase::create($sqlOptions);
$isDbExist = $sql->dbExists();
$databaseSpec = $sql->getDbSpec();
} catch (Throwable $t) {
throw new Exception(dt('Failed to get database specification: !error', ['!error' => $t->getMessage()]));
}
if (
$isDbExist &&
!$this->io()->confirm(
dt(
'Are you sure you want to drop the database "!database" (username: !user, password: !password, port: !port, prefix: !prefix) and import the database dump "!path"?',
[
'!path' => $databaseDumpPath,
'!database' => $databaseSpec['database'],
'!user' => $databaseSpec['username'],
'!password' => isset($databaseSpec['password']) ? '******' : '[not set]',
'!port' => $databaseSpec['port'] ?: dt('n/a'),
'!prefix' => $databaseSpec['prefix'] ?: dt('n/a'),
]
)
)
) {
throw new UserAbortException();
}
if ($isDbExist && !$sql->drop($sql->listTablesQuoted())) {
throw new Exception(
dt('Failed to drop database !database.', ['!database' => $databaseSpec['database']])
);
} elseif (!$sql->createdb(true)) {
throw new Exception(
dt('Failed to create database !database.', ['!database' => $databaseSpec['database']])
);
}
$sql->setDbSpec($databaseSpec);
if (!$sql->query('', $databaseDumpPath)) {
throw new Exception(dt('Database import has failed: !error', ['!error' => $sql->getProcess()->getErrorOutput()]));
}
if ($sqlOptions) {
// Setup settings.local.php file since database connection settings provided via options.
$this->setupLocalSettingsPhp($databaseSpec, $options);
}
}
/**
* Sets up settings.local.php file.
*
* 1. Creates settings.php file (a copy of default.settings.php) in the site's subdirectory if not exists;
* 2. Makes sure settings.php has an active (i.e. uncommented) "include settings.local.php file" directive;
* 3. Updates settings.local.php file to include database connection settings provided via command's options.
*
* @param array $databaseSpec
* The database connection specification.
* @param array $options
* The command options.
*
* @throws Exception
*/
private function setupLocalSettingsPhp(array $databaseSpec, array $options): void
{
$drupalRootPath = $this->getDrupalRootPath();
if (!$drupalRootPath) {
throw new Exception(
dt('Failed to detect Drupal docroot path for path !path', ['!path' => $this->getDestinationPath()])
);
}
$siteSubdir = Path::join($drupalRootPath, 'sites', $options['site-subdir']);
$this->filesystem->mkdir($siteSubdir);
$settingsPhpPath = Path::join($siteSubdir, 'settings.php');
if (!is_file($settingsPhpPath)) {
// Create settings.php file as a copy of default.settings.php file.
$defaultSettingsPath = Path::join($drupalRootPath, 'sites', self::SITE_SUBDIR, 'default.settings.php');
$this->logger()->info('Copying !from to !to...', ['!from' => $defaultSettingsPath, '!to' => $settingsPhpPath]);
copy(
$defaultSettingsPath,
$settingsPhpPath
);
}
$drushSignature = '// Added by Drush archive:restore command.';
// Make sure settings.php has an active (i.e. uncommented) "include settings.local.php file" directive.
$settingsPhpContent = file_get_contents($settingsPhpPath);
if (preg_match('/\# if \(file_exists.+?settings\.local\.php.+?\# }/ms', $settingsPhpContent, $matches)) {
$uncommentedLocalSettingsInclude = $drushSignature . "\n" . str_replace('# ', '', $matches[0]);
$settingsPhpIncludeLocalContent = str_replace(
$matches[0],
$uncommentedLocalSettingsInclude,
$settingsPhpContent
);
$this->logger()->info(sprintf('Updating %s to include settings.local.php file...', $settingsPhpPath));
if (!file_put_contents($settingsPhpPath, $settingsPhpIncludeLocalContent)) {
throw new Exception(dt('Failed to save updated !path', ['!path' => $settingsPhpPath]));
}
}
$databaseSpecExported = var_export($databaseSpec, true);
$settingsLocalPhpPath = Path::join($siteSubdir, 'settings.local.php');
$settingsLocalPhpDatabaseConnection = <<<EOT
$drushSignature
\$databases['default']['default'] = $databaseSpecExported;
EOT;
if (!is_file($settingsLocalPhpPath)) {
// Create settings.local.php file with database connection settings provided via command's options.
$this->logger()->info('Creating !path with database connection settings...', ['!path' => $settingsLocalPhpPath]);
$settingsLocalPhpModifiedContent = '<?php' . $settingsLocalPhpDatabaseConnection;
$this->saveSettingsLocalPhp($settingsLocalPhpPath, $settingsLocalPhpModifiedContent);
return;
}
$settingsLocalPhpContent = file_get_contents($settingsLocalPhpPath);
if (false === strpos($settingsLocalPhpContent, $drushSignature)) {
$this->logger()->info('Adding database connection settings to !path...', ['!path' => $settingsLocalPhpPath]);
$settingsLocalPhpModifiedContent = $settingsLocalPhpContent . $settingsLocalPhpDatabaseConnection;
$this->saveSettingsLocalPhp($settingsLocalPhpPath, $settingsLocalPhpModifiedContent);
return;
}
$this->logger()->info('Updating database connection settings in !path...', ['!path' => $settingsLocalPhpPath]);
$settingsLocalPhpModifiedContent = preg_replace(
'/' . preg_quote($drushSignature, '/') . '.+?\);/ms',
$settingsLocalPhpDatabaseConnection,
$settingsLocalPhpContent
);
$this->saveSettingsLocalPhp($settingsLocalPhpPath, $settingsLocalPhpModifiedContent);
}
/**
* Saves settings.local.php file with actual database connection settings.
*
* @param string $path
* @param string $content
* @throws Exception
*/
private function saveSettingsLocalPhp(string $path, string $content): void
{
if (!file_put_contents($path, $content)) {
throw new Exception(dt('Failed to create or update !path.', ['!path' => $path]));
}
}
}