diff --git a/src/Application.php b/src/Application.php
index e73f3da74..4d0ba38d8 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -99,6 +99,8 @@ protected function getCommands()
$commands[] = new Command\Activity\ActivityLogCommand();
$commands[] = new Command\App\AppConfigGetCommand();
$commands[] = new Command\App\AppListCommand();
+ $commands[] = new Command\Archive\ArchiveExportCommand();
+ $commands[] = new Command\Archive\ArchiveImportCommand();
$commands[] = new Command\Auth\AuthInfoCommand();
$commands[] = new Command\Auth\AuthTokenCommand();
$commands[] = new Command\Auth\LogoutCommand();
diff --git a/src/Command/Archive/ArchiveExportCommand.php b/src/Command/Archive/ArchiveExportCommand.php
new file mode 100644
index 000000000..1e0eded94
--- /dev/null
+++ b/src/Command/Archive/ArchiveExportCommand.php
@@ -0,0 +1,473 @@
+setName('archive:export')
+ ->setDescription('Export an archive from an environment')
+ ->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'The filename for the archive')
+ ->addOption('exclude-services', null, InputOption::VALUE_NONE, 'Exclude services')
+ ->addOption('exclude-mounts', null, InputOption::VALUE_NONE, 'Exclude mounts')
+ ->addOption('include-variables', null, InputOption::VALUE_NONE, 'Include variables')
+ ->addOption('include-sensitive-values', null, InputOption::VALUE_NONE, 'Include sensitive variable values');
+ $this->addProjectOption();
+ $this->addEnvironmentOption();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->validateInput($input);
+
+ /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
+ $questionHelper = $this->getService('question_helper');
+ /** @var \Platformsh\Cli\Service\Filesystem $fs */
+ $fs = $this->getService('fs');
+
+ $this->stdErr->writeln(sprintf(
+ 'Archiving data from the project %s, environment %s',
+ $this->api()->getProjectLabel($this->getSelectedProject()),
+ $this->api()->getEnvironmentLabel($this->getSelectedEnvironment())
+ ));
+ $this->stdErr->writeln('');
+
+ $environment = $this->getSelectedEnvironment();
+ $deployment = $this->api()->getCurrentDeployment($environment, true);
+
+ $archiveId = sprintf('archive-%s--%s', $environment->machine_name, $environment->project);
+ $archiveFilename = (string) $input->getOption('file');
+ if ($archiveFilename === '') {
+ $archiveFilename = $archiveId . '.tar.gz';
+ }
+ if (file_exists($archiveFilename)) {
+ $this->stdErr->writeln(sprintf('The file already exists: %s', $archiveFilename));
+ if (!$questionHelper->confirm('Overwrite?')) {
+ return 1;
+ }
+ }
+ if (!$fs->canWrite($archiveFilename)) {
+ $this->stdErr->writeln('File not writable: ' . $archiveFilename . '');
+
+ return 1;
+ }
+
+ $excludeServices = (bool) $input->getOption('exclude-services');
+ $serviceSupport = [
+ 'mysql' => 'using "db:dump"',
+ 'postgresql' => 'using "db:dump"',
+ 'mongodb' => 'using "mongodump"',
+ 'network-storage' => 'via mounts',
+ ];
+ $supportedServices = [];
+ $unsupportedServices = [];
+ $ignoredServices = [];
+ if (!$excludeServices) {
+ foreach ($deployment->services as $name => $service) {
+ list($type, ) = explode(':', $service->type, 2);
+ if (isset($serviceSupport[$type]) && !empty($service->disk)) {
+ $supportedServices[$name] = $type;
+ } elseif (empty($service->disk)) {
+ $ignoredServices[$name] = $type;
+ } else {
+ $unsupportedServices[$name] = $type;
+ }
+ }
+ }
+
+ if (!empty($supportedServices)) {
+ $this->stdErr->writeln('Supported services:');
+ foreach ($supportedServices as $name => $type) {
+ $this->stdErr->writeln(sprintf(
+ ' - %s (%s), %s',
+ $name,
+ $type,
+ $serviceSupport[$type]
+ ));
+ }
+ $this->stdErr->writeln('');
+ }
+
+ if (!empty($ignoredServices)) {
+ $this->stdErr->writeln('Ignored services, without disk storage:');
+ foreach ($ignoredServices as $name => $type) {
+ $this->stdErr->writeln(
+ sprintf(' - %s (%s)', $name, $type)
+ );
+ }
+ $this->stdErr->writeln('');
+ }
+
+ if (!empty($unsupportedServices)) {
+ $this->stdErr->writeln('Unsupported services:');
+ foreach ($unsupportedServices as $name => $type) {
+ $this->stdErr->writeln(
+ sprintf(' - %s (%s)', $name, $type)
+ );
+ }
+ $this->stdErr->writeln('');
+ }
+
+ $apps = [];
+ $hasMounts = false;
+ $excludeMounts = (bool) $input->getOption('exclude-mounts');
+ $includeVariables = (bool) $input->getOption('include-variables');
+ foreach ($deployment->webapps as $name => $webApp) {
+ $app = new App($webApp, $environment);
+ $apps[$name] = $app;
+ $hasMounts = !$excludeMounts && ($hasMounts || count($app->getMounts()));
+ }
+
+ $nothingToDo = true;
+
+ if ($includeVariables) {
+ $this->stdErr->writeln('Environment metadata (including variables) will be saved.');
+ $nothingToDo = false;
+ }
+
+ if ($hasMounts && !$excludeMounts) {
+ $this->stdErr->writeln('Files from mounts will be downloaded.');
+ $nothingToDo = false;
+ }
+
+ if (!$excludeServices && !empty($supportedServices)) {
+ $this->stdErr->writeln('Data from the above supported service(s) will be saved.');
+ $nothingToDo = false;
+ }
+
+ if ($nothingToDo) {
+ $this->stdErr->writeln('There is nothing to export.');
+ return 1;
+ }
+
+ $this->stdErr->writeln('');
+
+ $this->stdErr->writeln('Warning>');
+ $this->stdErr->writeln('Your site may be changing data during archiving, resulting in inconsistencies.');
+ $this->stdErr->writeln('This tool is not suitable for making consistent backups (instead, see ' . $this->config()->get('application.executable') . ' snapshot:create).');
+
+ $this->stdErr->writeln('');
+
+ if (!$questionHelper->confirm('Are you sure you want to continue?')) {
+ return 1;
+ }
+
+ $tmpDir = $fs->makeTempDir('archive-');
+ $archiveDir = $tmpDir . '/' . $archiveId;
+ if (!mkdir($archiveDir)) {
+ $this->stdErr->writeln(sprintf('Failed to create archive directory: %s', $archiveDir));
+
+ return 1;
+ }
+ $this->debug('Using archive directory: ' . $archiveDir);
+
+ $metadata = [
+ 'time' => date('c'),
+ 'version' => self::ARCHIVE_VERSION,
+ 'cli_version' => $this->config()->getVersion(),
+ 'project' => $this->getSelectedProject()->getProperties(),
+ 'environment' => $environment->getProperties(),
+ 'deployment' => $deployment->getProperties(),
+ ];
+
+ if ($includeVariables) {
+ $includeSensitive = $input->getOption('include-sensitive-values');
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Copying project-level variables');
+ foreach ($this->getSelectedProject()->getVariables() as $var) {
+ $metadata['variables']['project'][$var->name] = $var->getProperties();
+ if ($var->is_sensitive && !$var->hasProperty('value')) {
+ if (!$includeSensitive) {
+ $this->stdErr->writeln(sprintf(' Warning: cannot save value for sensitive project-level variable %s', $var->name));
+ $this->stdErr->writeln(' Use --include-sensitive-values to try to fetch this via SSH');
+ continue;
+ }
+ if (!$var->visible_runtime) {
+ $this->stdErr->writeln(sprintf(' Warning: cannot save value for sensitive project-level variable %s', $var->name));
+ $this->stdErr->writeln(' It is not marked as visible at runtime.');
+ continue;
+ }
+ $value = false;
+ foreach ($apps as $app) {
+ try {
+ $value = $this->fetchSensitiveValue($app->getSshUrl(), $var->name, $var->is_json);
+ } catch (\RuntimeException $e) {
+ $this->stdErr->writeln(sprintf(' Error: Failed to find value for sensitive project-level variable %s in app %s', $var->name, $app->getName()));
+ continue;
+ }
+ break;
+ }
+ if ($value !== false) {
+ $metadata['variables']['project'][$var->name]['value'] = $value;
+ }
+ }
+ }
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Copying environment-level variables');
+ foreach ($environment->getVariables() as $envVar) {
+ $metadata['variables']['environment'][$envVar->name] = $envVar->getProperties();
+ if ($envVar->is_sensitive && !$envVar->hasProperty('value')) {
+ if ($includeSensitive) {
+ $value = false;
+ foreach ($apps as $app) {
+ try {
+ $value = $this->fetchSensitiveValue($app->getSshUrl(), $envVar->name, $envVar->is_json);
+ } catch (\RuntimeException $e) {
+ $this->stdErr->writeln(sprintf(' Error: Failed to find value for sensitive environment-level variable %s in app %s', $envVar->name, $app->getName()));
+ continue;
+ }
+ break;
+ }
+ if ($value !== false) {
+ $metadata['variables']['environment'][$envVar->name]['value'] = $value;
+ }
+ } else {
+ $this->stdErr->writeln(sprintf(' Warning: cannot save value for sensitive environment-level variable %s', $envVar->name));
+ $this->stdErr->writeln(' Use --include-sensitive-values to try to fetch this via SSH');
+ }
+ }
+ }
+ }
+
+ foreach ($supportedServices as $serviceName => $type) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Archiving service ' . $serviceName . '');
+
+ // Find a relationship to the service.
+ $relationshipName = false;
+ $appName = false;
+ foreach ($apps as $app) {
+ $relationshipName = $this->getRelationshipNameForService($app, $serviceName);
+ if ($relationshipName !== false) {
+ $appName = $app->getName();
+ break;
+ }
+ }
+ if ($relationshipName === false || $appName === false) {
+ $this->stdErr->writeln('No app defines a relationship to the service %s (%s)');
+
+ return 1;
+ }
+ if ($type === 'mysql' || $type === 'pgsql') {
+ // Get a list of schemas for this relationship.
+ $schemas = $this->getSchemas($deployment->getService($serviceName), $relationshipName);
+
+ mkdir($archiveDir . '/services/' . $serviceName, 0755, true);
+
+ if (count($schemas) === 0) {
+ $schemas = [null];
+ }
+
+ foreach ($schemas as $schema) {
+ $filename = $appName . '--' . $relationshipName;
+ $args = [
+ '--directory' => $archiveDir . '/services/' . $serviceName,
+ '--project' => $this->getSelectedProject()->id,
+ '--environment' => $this->getSelectedEnvironment()->id,
+ '--app' => $appName,
+ '--relationship' => $relationshipName,
+ '--yes' => true,
+ // gzip is not enabled because the archive will be gzipped anyway
+ ];
+ if ($schema !== null) {
+ $args['--schema'] = $schema;
+ $filename .= '--' . $schema;
+ }
+ $filename .= '.sql';
+ $args['--file'] = $filename;
+ $exitCode = $this->runOtherCommand('db:dump', $args);
+ if ($exitCode !== 0) {
+
+ return $exitCode;
+ }
+ $metadata['services'][$serviceName]['_type'] = $type;
+ $metadata['services'][$serviceName]['dumps'][] = [
+ 'filename' => 'services/' . $serviceName . '/' . $filename,
+ 'app' => $appName,
+ 'schema' => $schema,
+ 'relationship' => $relationshipName,
+ ];
+ }
+ }
+ if ($type === 'mongodb') {
+ $args = [
+ '--directory' => $archiveDir . '/services/' . $serviceName,
+ '--project' => $this->getSelectedProject()->id,
+ '--environment' => $this->getSelectedEnvironment()->id,
+ '--app' => $appName,
+ '--relationship' => $relationshipName,
+ '--yes' => true,
+ '--stdout' => true,
+ // gzip is not enabled because the archive will be gzipped anyway
+ ];
+ $filename = $appName . '--' . $relationshipName . '.bson';
+ // @todo dump directly to a file without the buffer
+ $buffer = new BufferedOutput();
+ $exitCode = $this->runOtherCommand('service:mongo:dump', $args, $buffer);
+ if ($exitCode !== 0) {
+
+ return $exitCode;
+ }
+ (new Filesystem())->dumpFile($filename, $buffer->fetch());
+ $metadata['services'][$serviceName]['_type'] = $type;
+ $metadata['services'][$serviceName]['dumps'][] = [
+ 'filename' => 'services/' . $serviceName . '/' . $filename,
+ 'app' => $appName,
+ 'relationship' => $relationshipName,
+ ];
+ }
+ }
+
+ if ($hasMounts && !$excludeMounts) {
+ /** @var \Platformsh\Cli\Service\Mount $mountService */
+ $mountService = $this->getService('mount');
+ /** @var \Platformsh\Cli\Service\Rsync $rsync */
+ $rsync = $this->getService('rsync');
+ $rsyncOptions = [
+ 'verbose' => $output->isVeryVerbose(),
+ 'quiet' => !$output->isVerbose(),
+ ];
+ foreach ($apps as $app) {
+ $sourcePaths = [];
+ $mounts = $mountService->normalizeMounts($app->getMounts());
+ foreach ($mounts as $path => $mount) {
+ if (isset($metadata['mounts'][$path])) {
+ continue;
+ }
+ if ($mount['source'] === 'local' && isset($mount['source_path'])) {
+ if (isset($sourcePaths[$mount['source_path']])) {
+ continue;
+ }
+ $sourcePaths[$mount['source_path']] = true;
+ }
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Copying from mount ' . $path . '');
+ $destination = $archiveDir . '/mounts/' . trim($path, '/');
+ mkdir($destination, 0755, true);
+ $rsync->syncDown($app->getSshUrl(), ltrim($path, '/'), $destination, $rsyncOptions);
+ $metadata['mounts'][$path] = [
+ 'app' => $app->getName(),
+ 'path' => 'mounts/' . trim($path, '/'),
+ ];
+ }
+ }
+ }
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Writing metadata');
+ (new Filesystem())->dumpFile($archiveDir . '/archive.json', json_encode($metadata, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Packing and compressing archive');
+ $fs->archiveDir($tmpDir, $archiveFilename);
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Archive: ' . $archiveFilename . '');
+
+ if (($absolute = realpath($archiveFilename)) && ($projectRoot = $this->getProjectRoot()) && strpos($absolute, $projectRoot) === 0) {
+ /** @var \Platformsh\Cli\Service\Git $git */
+ $git = $this->getService('git');
+ if (!$git->checkIgnore($absolute, $projectRoot)) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Warning: the archive file is not excluded by Git');
+ if ($pos = strrpos($archiveFilename, '.tar.gz')) {
+ $extension = substr($archiveFilename, $pos);
+ $this->stdErr->writeln(' You should probably exclude these files using .gitignore:');
+ $this->stdErr->writeln(' *' . $extension);
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * @param \Platformsh\Cli\Model\RemoteContainer\App $app
+ * @param string $serviceName
+ *
+ * @return string|false
+ */
+ private function getRelationshipNameForService(App $app, $serviceName)
+ {
+ $config = $app->getConfig()->getNormalized();
+ $relationships = isset($config['relationships']) ? $config['relationships'] : [];
+ foreach ($relationships as $relationshipName => $relationshipTarget) {
+ list($targetService,) = explode(':', $relationshipTarget);
+ if ($targetService === $serviceName) {
+ return $relationshipName;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a list of schemas configured for the service.
+ *
+ * @param \Platformsh\Client\Model\Deployment\Service $service
+ * @param string $relationshipName
+ *
+ * @return array
+ */
+ private function getSchemas(Service $service, $relationshipName)
+ {
+ if (empty($service->configuration['schemas'])) {
+ return [];
+ }
+
+ $schemas = $service->configuration['schemas'];
+
+ // Filter the list by the schemas accessible from the endpoint.
+ if (isset($service->configuration['endpoints'][$relationshipName]['privileges'])) {
+ $schemas = array_intersect(
+ $schemas,
+ array_keys($service->configuration['endpoints'][$relationshipName]['privileges'])
+ );
+ }
+
+ return $schemas;
+ }
+
+ /**
+ * @param string $sshUrl
+ * @param string $varName
+ * @param bool $is_json
+ *
+ * @return mixed
+ */
+ private function fetchSensitiveValue($sshUrl, $varName, $is_json)
+ {
+ /** @var \Platformsh\Cli\Service\RemoteEnvVars $remoteEnvVars */
+ $remoteEnvVars = $this->getService('remote_env_vars');
+ if (substr($varName, 0, 4) === 'env:') {
+ return $remoteEnvVars->getEnvVar(substr($varName, 4), $sshUrl, true);
+ }
+
+ $variables = $remoteEnvVars->getArrayEnvVar('VARIABLES', $sshUrl);
+ if (array_key_exists($varName, $variables)) {
+ return $is_json ? json_encode($variables[$varName]) : $variables[$varName];
+ }
+
+ throw new \RuntimeException('Variable not found: ' . $varName);
+ }
+}
diff --git a/src/Command/Archive/ArchiveImportCommand.php b/src/Command/Archive/ArchiveImportCommand.php
new file mode 100644
index 000000000..b9e9000b2
--- /dev/null
+++ b/src/Command/Archive/ArchiveImportCommand.php
@@ -0,0 +1,366 @@
+setName('archive:import')
+ ->setDescription('Import an archive')
+ ->addArgument('file', InputArgument::REQUIRED, 'The archive filename')
+ ->addOption('include-variables', null, InputOption::VALUE_NONE, 'Import environment-level variables')
+ ->addOption('include-project-variables', null, InputOption::VALUE_NONE, 'Import project-level variables');
+ $this->addProjectOption();
+ $this->addEnvironmentOption();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->validateInput($input);
+ $filename = (string) $input->getArgument('file');
+ if (!file_exists($filename)) {
+ $this->stdErr->writeln(sprintf('File not found: %s', $filename));
+
+ return 1;
+ }
+ if (!is_readable($filename)) {
+ $this->stdErr->writeln(sprintf('Not readable: %s', $filename));
+
+ return 1;
+ }
+ if (substr($filename, -7) !== '.tar.gz') {
+ $this->stdErr->writeln(sprintf('Unexpected format: %s (expected: .tar.gz)', $filename));
+
+ return 1;
+ }
+
+ /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
+ $questionHelper = $this->getService('question_helper');
+ /** @var \Platformsh\Cli\Service\Filesystem $fs */
+ $fs = $this->getService('fs');
+
+ $environment = $this->getSelectedEnvironment();
+
+ $this->stdErr->writeln(sprintf(
+ 'Importing into environment %s on the project %s',
+ $this->api()->getEnvironmentLabel($environment),
+ $this->api()->getProjectLabel($this->getSelectedProject())
+ ));
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Warning:>');
+ $this->stdErr->writeln(sprintf('Any data on %s may be deleted. This action cannot be undone.', $this->api()->getEnvironmentLabel($environment, 'comment')));
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('also please note:>');
+ $this->stdErr->writeln('Currently, data is imported, but no existing data is deleted (so you may see inconsistent results).');
+ $this->stdErr->writeln('');
+
+ if (!$questionHelper->confirm('Are you sure you want to continue?', false)) {
+ return 1;
+ }
+
+ $tmpDir = $fs->makeTempDir('archive-');
+ $fs->extractArchive($filename, $tmpDir);
+
+ $this->debug('Extracted archive to: ' . $tmpDir);
+
+ foreach ((array) scandir($tmpDir) as $filename) {
+ if (!empty($filename) && $filename[0] !== '.' && is_dir($tmpDir . '/' . $filename)) {
+ $archiveId = $filename;
+ break;
+ }
+ }
+ if (empty($archiveId)) {
+ $this->stdErr->writeln('Error: Failed to identify archive subdirectory');
+
+ return 1;
+ }
+ $archiveDir = $tmpDir . '/' . $archiveId;
+
+ $metadata = file_get_contents($archiveDir . '/archive.json');
+ if ($metadata === false || !($metadata = json_decode($metadata, true))) {
+ $this->stdErr->writeln('Error: Failed to read archive metadata');
+
+ return 1;
+ }
+
+ if ($metadata['version'] < ArchiveExportCommand::ARCHIVE_VERSION) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Error: The archive is outdated so it cannot be imported.');
+ $this->stdErr->writeln(sprintf(' Archive version: %s (from CLI version: %s)', $metadata['version'], $metadata['cli_version']));
+ $this->stdErr->writeln(sprintf(' Current version: %s (CLI version: %s)', ArchiveExportCommand::ARCHIVE_VERSION, $this->config()->getVersion()));
+
+ return 1;
+ }
+
+ $activities = [];
+
+ if (!empty($metadata['variables']['environment'])
+ && ($input->getOption('include-variables') || ($input->isInteractive() && $questionHelper->confirm("\nImport environment-level variables?", false)))) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Importing environment-level variables');
+
+ foreach ($metadata['variables']['environment'] as $name => $var) {
+ $this->stdErr->writeln(' Processing variable ' . $name . '');
+ if (!array_key_exists('value', $var)) {
+ if ($var['is_sensitive']) {
+ $this->stdErr->writeln(' Skipping sensitive variable ' . $name . '');
+ continue;
+ }
+ $this->stdErr->writeln(' Error: no variable value found.');
+ continue;
+ }
+ if ($current = $environment->getVariable($name)) {
+ $this->stdErr->writeln(' The variable already exists.');
+ if ($this->variablesAreEqual($current->getProperties(), $var)) {
+ $this->stdErr->writeln(' No change required.');
+ continue;
+ }
+
+ if ($questionHelper->confirm(' Do you want to update it?')) {
+ $result = $current->update($var);
+ } else {
+ continue;
+ }
+ } else {
+ $result = $this->createEnvironmentVariableFromProperties($var, $environment);
+
+ }
+ $this->stdErr->writeln(' Done');
+ $activities = array_merge($activities, $result->getActivities());
+ }
+ }
+
+ if (!empty($metadata['variables']['project'])
+ && ($input->getOption('include-project-variables') || ($input->isInteractive() && $questionHelper->confirm("\nImport project-level variables?", false)))) {
+ $project = $this->getSelectedProject();
+
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Importing project-level variables');
+
+ foreach ($metadata['variables']['project'] as $name => $var) {
+ $this->stdErr->writeln(' Processing variable ' . $name . '');
+ if (!array_key_exists('value', $var)) {
+ if ($var['is_sensitive']) {
+ $this->stdErr->writeln(' Skipping sensitive variable ' . $name . '');
+ continue;
+ }
+ $this->stdErr->writeln(' Error: no variable value found.');
+ continue;
+ }
+ if ($current = $project->getVariable($name)) {
+ $this->stdErr->writeln(' The variable already exists.');
+ if ($this->variablesAreEqual($current->getProperties(), $var)) {
+ $this->stdErr->writeln(' No change required.');
+ continue;
+ }
+
+ if (!$questionHelper->confirm(' Do you want to update it?')) {
+ return 1;
+ }
+
+ $result = $current->update($var);
+ } else {
+ $result = $this->createProjectVariableFromProperties($var, $project);
+ }
+ $this->stdErr->writeln(' Done');
+ $activities = array_merge($activities, $result->getActivities());
+ }
+ }
+
+ $success = true;
+
+ if (!empty($metadata['services'])) {
+ foreach ($metadata['services'] as $serviceName => $serviceInfo) {
+ if (in_array($serviceInfo['_type'], ['mysql', 'pgsql'])) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Importing data for service ' . $serviceName . '');
+
+ foreach ($serviceInfo['dumps'] as $dumpInfo) {
+ if (!empty($dumpInfo['schema'])) {
+ $this->stdErr->writeln('Processing schema: ' . $dumpInfo['schema'] . '');
+ }
+ $this->importDatabaseDump($archiveDir, $dumpInfo);
+ }
+ } elseif ($serviceInfo['_type'] === 'mongodb') {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Importing data for service ' . $serviceName . '');
+
+ foreach ($serviceInfo['dumps'] as $dumpInfo) {
+ $this->importMongoDump($archiveDir, $dumpInfo);
+ }
+ }
+ }
+ }
+
+ if (!empty($metadata['mounts'])) {
+ /** @var \Platformsh\Cli\Service\Rsync $rsync */
+ $rsync = $this->getService('rsync');
+ $rsyncOptions = [
+ 'verbose' => $output->isVeryVerbose(),
+ 'quiet' => !$output->isVerbose(),
+ ];
+ $deployment = $environment->getCurrentDeployment();
+ foreach ($metadata['mounts'] as $path => $info) {
+ $this->stdErr->writeln('');
+ $this->stdErr->writeln('Importing files to mount ' . $path . '');
+ $app = new App($deployment->getWebApp($info['app']), $environment);
+ $rsync->syncUp($app->getSshUrl(), $archiveDir . '/' . $info['path'], $path, $rsyncOptions);
+ }
+ }
+
+ if (!empty($activities) && $this->shouldWait($input)) {
+ /** @var \Platformsh\Cli\Service\ActivityMonitor $activityMonitor */
+ $activityMonitor = $this->getService('activity_monitor');
+ $success = $activityMonitor->waitMultiple($activities, $this->getSelectedProject());
+ }
+
+ return $success ? 0 : 1;
+ }
+
+ /**
+ * @param array $properties
+ * @param \Platformsh\Client\Model\Environment $environment
+ *
+ * @return \Platformsh\Client\Model\Result
+ */
+ private function createEnvironmentVariableFromProperties(array $properties, Environment $environment)
+ {
+ $var = $properties;
+ unset($var['project'], $var['environment'], $var['created_at'], $var['updated_at'], $var['id'], $var['attributes'], $var['inherited']);
+
+ return Variable::create($var, $environment->getLink('#manage-variables'), $this->api()->getHttpClient());
+ }
+
+ /**
+ * @param array $properties
+ * @param \Platformsh\Client\Model\Project $project
+ *
+ * @return \Platformsh\Client\Model\Result
+ */
+ private function createProjectVariableFromProperties(array $properties, Project $project)
+ {
+ $var = $properties;
+ unset($var['project'], $var['environment'], $var['created_at'], $var['updated_at'], $var['id'], $var['attributes']);
+
+ return ProjectLevelVariable::create($var, $project->getLink('#manage-variables'), $this->api()->getHttpClient());
+ }
+
+
+ /**
+ * Checks if two variables are equal.
+ *
+ * @param array $var1Properties
+ * @param array $var2Properties
+ *
+ * @return bool
+ */
+ private function variablesAreEqual(array $var1Properties, $var2Properties)
+ {
+ $keys = [
+ 'value',
+ 'is_json',
+ 'is_enabled',
+ 'is_sensitive',
+ 'is_inheritable',
+ 'visible_build',
+ 'visible_runtime',
+ ];
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $var1Properties)
+ && (!array_key_exists($key, $var2Properties) || $var1Properties[$key] !== $var2Properties[$key])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $commandName
+ * @param array $args
+ * @param string $suffix
+ */
+ private function runCliCommandViaShell($commandName, array $args = [], $suffix = '')
+ {
+ $args = array_merge([
+ (new PhpExecutableFinder())->find(false) ?: 'php',
+ $GLOBALS['argv'][0],
+ $commandName,
+ ], $args);
+ $command = implode(' ', array_map('escapeshellarg', $args)) . $suffix;
+
+ /** @var \Platformsh\Cli\Service\Shell $shell */
+ $shell = $this->getService('shell');
+ $exitCode = $shell->executeSimple($command);
+ if ($exitCode !== 0) {
+ throw new \RuntimeException("Command failed with exit code $exitCode:" . $command);
+ }
+ }
+
+ /**
+ * @param string $archiveDir
+ * @param array $dumpInfo
+ */
+ private function importDatabaseDump($archiveDir, array $dumpInfo)
+ {
+ if (!file_exists($archiveDir . '/' . $dumpInfo['filename'])) {
+ throw new \RuntimeException('Dump file not found: ' . $archiveDir . '/' . $dumpInfo['filename']);
+ }
+ $args = [
+ '--project=' . $this->getSelectedProject()->id,
+ '--environment=' . $this->getSelectedEnvironment()->id,
+ '--app=' . $dumpInfo['app'],
+ '--relationship=' . $dumpInfo['relationship'],
+ '--yes',
+ ];
+ if (!empty($dumpInfo['schema'])) {
+ $args[] = '--schema=' . $dumpInfo['schema'];
+ }
+ if ($this->stdErr->isVerbose()) {
+ $args[] = '--verbose';
+ }
+ $this->runCliCommandViaShell('db:sql', $args, ' < ' . escapeshellarg($archiveDir . '/' . $dumpInfo['filename']));
+ }
+
+ /**
+ * @param string $archiveDir
+ * @param array $dumpInfo
+ */
+ private function importMongoDump($archiveDir, array $dumpInfo)
+ {
+ if (!file_exists($archiveDir . '/' . $dumpInfo['filename'])) {
+ throw new \RuntimeException('Dump file not found: ' . $archiveDir . '/' . $dumpInfo['filename']);
+ }
+ $args = [
+ '--project=' . $this->getSelectedProject()->id,
+ '--environment=' . $this->getSelectedEnvironment()->id,
+ '--app=' . $dumpInfo['app'],
+ '--relationship=' . $dumpInfo['relationship'],
+ '--yes',
+ ];
+ if ($this->stdErr->isVerbose()) {
+ $args[] = '--verbose';
+ }
+ $this->runCliCommandViaShell('service:mongo:restore', $args, ' < ' . escapeshellarg($archiveDir . '/' . $dumpInfo['filename']));
+ }
+}
diff --git a/src/Command/CompletionCommand.php b/src/Command/CompletionCommand.php
index 2a8bd5076..e71fda619 100644
--- a/src/Command/CompletionCommand.php
+++ b/src/Command/CompletionCommand.php
@@ -146,6 +146,11 @@ protected function runCompletion()
Completion::TYPE_OPTION,
[$this, 'getAppNames']
),
+ new Completion\ShellPathCompletion(
+ 'archive:import',
+ 'file',
+ Completion::TYPE_ARGUMENT
+ ),
new Completion\ShellPathCompletion(
Completion::ALL_COMMANDS,
'identity-file',
diff --git a/src/Service/Filesystem.php b/src/Service/Filesystem.php
index cc215b8e0..bc6bb13b4 100644
--- a/src/Service/Filesystem.php
+++ b/src/Service/Filesystem.php
@@ -503,10 +503,52 @@ public function validateDirectory($directory, $writable = false)
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException(sprintf('Directory not found: %s', $directory));
- } elseif (!is_readable($directory)) {
+ }
+ elseif (!is_readable($directory)) {
throw new \InvalidArgumentException(sprintf('Directory not readable: %s', $directory));
- } elseif ($writable && !is_writable($directory)) {
+ }
+ elseif ($writable && !is_writable($directory)) {
throw new \InvalidArgumentException(sprintf('Directory not writable: %s', $directory));
}
}
+
+ /**
+ * Makes a temporary directory.
+ *
+ * @param string $prefix
+ * @param bool $autoCleanup
+ *
+ * @return string
+ */
+ public function makeTempDir($prefix = 'tmp-', $autoCleanup = true)
+ {
+ $tmpName = tempnam(sys_get_temp_dir(), $prefix);
+ if ($tmpName !== false && is_file($tmpName)) {
+ $this->fs->remove($tmpName);
+ }
+ if (!$tmpName || !mkdir($tmpName)) {
+ throw new \RuntimeException(sprintf('Failed to create temporary directory: %s', $tmpName));
+ }
+
+ if ($autoCleanup) {
+ register_shutdown_function(function () use ($tmpName) {
+ if (file_exists($tmpName)) {
+ $this->fs->remove($tmpName);
+ }
+ });
+ if (function_exists('pcntl_signal')) {
+ declare(ticks = 1);
+ /** @noinspection PhpComposerExtensionStubsInspection */
+ pcntl_signal(SIGINT, function () use ($tmpName) {
+ if (file_exists($tmpName)) {
+ $this->fs->remove($tmpName);
+ }
+ exit(130);
+ });
+ }
+
+ }
+
+ return $tmpName;
+ }
}
diff --git a/src/Service/RemoteEnvVars.php b/src/Service/RemoteEnvVars.php
index d34b493b7..6a71cafe2 100644
--- a/src/Service/RemoteEnvVars.php
+++ b/src/Service/RemoteEnvVars.php
@@ -38,15 +38,16 @@ public function __construct(Ssh $ssh, CacheProvider $cache, Shell $shellHelper,
* @param string $sshUrl The SSH URL to the application.
* @param bool $refresh Whether to refresh the cache.
* @param int $ttl The cache lifetime of the result.
+ * @param bool $prefix Whether to prepend the service.env_prefix.
*
* @throws \Symfony\Component\Process\Exception\RuntimeException
* If the SSH command fails.
*
* @return string The environment variable or an empty string.
*/
- public function getEnvVar($variable, $sshUrl, $refresh = false, $ttl = 3600)
+ public function getEnvVar($variable, $sshUrl, $refresh = false, $ttl = 3600, $prefix = true)
{
- $varName = $this->config->get('service.env_prefix') . $variable;
+ $varName = $prefix ? $this->config->get('service.env_prefix') . $variable : $variable;
$cacheKey = 'env-' . $sshUrl . '-' . $varName;
$cached = $this->cache->fetch($cacheKey);
if ($refresh || $cached === false) {
@@ -69,12 +70,14 @@ public function getEnvVar($variable, $sshUrl, $refresh = false, $ttl = 3600)
* @param string $variable
* @param string $sshUrl
* @param bool $refresh
+ * @param int $ttl
+ * @param bool $prefix
*
* @return array
*/
- public function getArrayEnvVar($variable, $sshUrl, $refresh = false)
+ public function getArrayEnvVar($variable, $sshUrl, $refresh = false, $ttl = 3600, $prefix = true)
{
- $value = $this->getEnvVar($variable, $sshUrl, $refresh);
+ $value = $this->getEnvVar($variable, $sshUrl, $refresh, $ttl, $prefix);
return json_decode(base64_decode($value), true) ?: [];
}