Skip to content

Commit b6cff8d

Browse files
committed
feat: add database:import command
1 parent 671b713 commit b6cff8d

4 files changed

Lines changed: 180 additions & 31 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"symfony/event-dispatcher": "^5.4",
3030
"symfony/filesystem": "^5.4",
3131
"symfony/finder": "^4.4.24",
32+
"symfony/polyfill-php80": "^1.24",
3233
"symfony/process": "^5.4",
3334
"symfony/yaml": "^5.4",
3435
"tightenco/collect": "^8.0"

src/Command/Database/AbstractDatabaseCommand.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
use Symfony\Component\Console\Exception\RuntimeException;
1717
use Symfony\Component\Console\Input\InputInterface;
1818
use Ymir\Cli\Command\AbstractCommand;
19+
use Ymir\Cli\Command\Network\AddBastionHostCommand;
1920
use Ymir\Cli\Console\OutputInterface;
21+
use Ymir\Cli\Process\Process;
22+
use Ymir\Cli\Support\Arr;
23+
use Ymir\Cli\Tool\Ssh;
2024

2125
abstract class AbstractDatabaseCommand extends AbstractCommand
2226
{
@@ -44,4 +48,51 @@ protected function determineDatabaseServer(string $question, InputInterface $inp
4448

4549
return $database;
4650
}
51+
52+
/**
53+
* Determine the password to use to connect to the database server with the given user.
54+
*/
55+
protected function determinePassword(InputInterface $input, OutputInterface $output, string $user): string
56+
{
57+
$password = $this->getStringArgument($input, 'password');
58+
59+
if (empty($password)) {
60+
$password = $output->askHidden(sprintf('What\'s the "<comment>%s</comment>" password?', $user));
61+
}
62+
63+
return $password;
64+
}
65+
66+
/**
67+
* Determine the user to use to connect to the database server.
68+
*/
69+
protected function determineUser(InputInterface $input, OutputInterface $output): string
70+
{
71+
$user = $this->getStringArgument($input, 'user');
72+
73+
if (empty($user)) {
74+
$user = $output->ask('Which user do you want to use to connect to the database server?', 'ymir');
75+
}
76+
77+
return $user;
78+
}
79+
80+
/**
81+
* Start a SSH tunnel to a private database server.
82+
*/
83+
protected function startSshTunnel(array $databaseServer): Process
84+
{
85+
$network = $this->apiClient->getNetwork(Arr::get($databaseServer, 'network.id'));
86+
87+
if (!is_array($network->get('bastion_host'))) {
88+
throw new RuntimeException(sprintf('The database server network does\'t have a bastion host to connect to. You can add one to the network with the "%s" command.', AddBastionHostCommand::NAME));
89+
}
90+
91+
$tunnel = Ssh::tunnelBastionHost($network->get('bastion_host'), 3305, $databaseServer['endpoint'], 3306);
92+
93+
// Need to wait a bit while SSH connection opens
94+
sleep(1);
95+
96+
return $tunnel;
97+
}
4798
}

src/Command/Database/ExportDatabaseCommand.php

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@
2020
use Symfony\Component\Filesystem\Filesystem;
2121
use Ymir\Cli\ApiClient;
2222
use Ymir\Cli\CliConfiguration;
23-
use Ymir\Cli\Command\Network\AddBastionHostCommand;
2423
use Ymir\Cli\Console\OutputInterface;
2524
use Ymir\Cli\Process\Process;
2625
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
27-
use Ymir\Cli\Support\Arr;
28-
use Ymir\Cli\Tool\Ssh;
2926

3027
class ExportDatabaseCommand extends AbstractDatabaseCommand
3128
{
@@ -60,7 +57,7 @@ protected function configure()
6057
{
6158
$this
6259
->setName(self::NAME)
63-
->setDescription('Export a database')
60+
->setDescription('Export a database to a local .sql.gz file')
6461
->addArgument('database', InputArgument::OPTIONAL, 'The ID or name of the database server to export a database from')
6562
->addArgument('name', InputArgument::OPTIONAL, 'The name of the database to export')
6663
->addArgument('user', InputArgument::OPTIONAL, 'The user used to connect to the database server')
@@ -74,16 +71,8 @@ protected function perform(InputInterface $input, OutputInterface $output)
7471
{
7572
$databaseServer = $this->determineDatabaseServer('Which database server would you like to export a database from?', $input, $output);
7673
$name = $this->determineDatabaseName($databaseServer, $input, $output);
77-
$user = $this->getStringArgument($input, 'user');
78-
$password = $this->getStringArgument($input, 'password');
79-
80-
if (empty($user)) {
81-
$user = $output->ask('Which user do you want to use to connect to the database server?', 'ymir');
82-
}
83-
84-
if (empty($password)) {
85-
$password = $output->askHidden(sprintf('What\'s the "<comment>%s</comment>" password?', $user));
86-
}
74+
$user = $this->determineUser($input, $output);
75+
$password = $this->determinePassword($input, $output, $user);
8776

8877
$filename = sprintf('%s_%s.sql.gz', $name, Carbon::now()->toDateString());
8978

@@ -101,9 +90,6 @@ protected function perform(InputInterface $input, OutputInterface $output)
10190
$tunnel = $this->startSshTunnel($databaseServer);
10291
$host = '127.0.0.1';
10392
$port = '3305';
104-
105-
// Need to wait a bit while SSH connection opens
106-
sleep(1);
10793
}
10894

10995
$output->infoWithDelayWarning(sprintf('Exporting "<comment>%s</comment>" database', $name));
@@ -132,18 +118,4 @@ private function determineDatabaseName(array $databaseServer, InputInterface $in
132118

133119
return $output->choice('Which database would you like to export?', $this->apiClient->getDatabases($databaseServer['id']));
134120
}
135-
136-
/**
137-
* Start a SSH tunnel to a private database server.
138-
*/
139-
private function startSshTunnel(array $databaseServer): Process
140-
{
141-
$network = $this->apiClient->getNetwork(Arr::get($databaseServer, 'network.id'));
142-
143-
if (!is_array($network->get('bastion_host'))) {
144-
throw new RuntimeException(sprintf('The database server network does\'t have a bastion host to connect to. You can add one to the network with the "%s" command.', AddBastionHostCommand::NAME));
145-
}
146-
147-
return Ssh::tunnelBastionHost($network->get('bastion_host'), 3305, $databaseServer['endpoint'], 3306);
148-
}
149121
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir command-line tool.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Cli\Command\Database;
15+
16+
use Symfony\Component\Console\Exception\RuntimeException;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Filesystem\Filesystem;
20+
use Ymir\Cli\ApiClient;
21+
use Ymir\Cli\CliConfiguration;
22+
use Ymir\Cli\Console\OutputInterface;
23+
use Ymir\Cli\Process\Process;
24+
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
25+
26+
class ImportDatabaseCommand extends AbstractDatabaseCommand
27+
{
28+
/**
29+
* The name of the command.
30+
*
31+
* @var string
32+
*/
33+
public const NAME = 'database:import';
34+
35+
/**
36+
* The file system.
37+
*
38+
* @var Filesystem
39+
*/
40+
private $filesystem;
41+
42+
/**
43+
* Constructor.
44+
*/
45+
public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration)
46+
{
47+
parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
48+
49+
$this->filesystem = $filesystem;
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
protected function configure()
56+
{
57+
$this
58+
->setName(self::NAME)
59+
->setDescription('Import a local .sql or .sql.gz file to a database')
60+
->addArgument('file', InputArgument::REQUIRED, 'The path to the local .sql or .sql.gz file')
61+
->addArgument('database', InputArgument::OPTIONAL, 'The ID or name of the database server to import a database to')
62+
->addArgument('name', InputArgument::OPTIONAL, 'The name of the database to import')
63+
->addArgument('user', InputArgument::OPTIONAL, 'The user used to connect to the database server')
64+
->addArgument('password', InputArgument::OPTIONAL, 'The password of the user connecting to the database server');
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
protected function perform(InputInterface $input, OutputInterface $output)
71+
{
72+
$file = $this->getStringArgument($input, 'file');
73+
74+
if (!str_ends_with($file, '.sql') && !str_ends_with($file, '.sql.gz')) {
75+
throw new RuntimeException('You may only import .sql or .sql.gz files');
76+
} elseif (!$this->filesystem->exists($file)) {
77+
throw new RuntimeException(sprintf('File "%s" doesn\'t exist', $file));
78+
}
79+
80+
$databaseServer = $this->determineDatabaseServer('Which database server would you like to import a database to?', $input, $output);
81+
$host = $databaseServer['endpoint'];
82+
$name = $this->determineDatabaseName($databaseServer, $input, $output);
83+
$port = 3306;
84+
$tunnel = null;
85+
86+
$user = $this->determineUser($input, $output);
87+
$password = $this->determinePassword($input, $output, $user);
88+
89+
if (!$databaseServer['publicly_accessible']) {
90+
$output->info(sprintf('Opening SSH tunnel to "<comment>%s</comment>" database server', $databaseServer['name']));
91+
92+
$tunnel = $this->startSshTunnel($databaseServer);
93+
$host = '127.0.0.1';
94+
$port = '3305';
95+
}
96+
97+
$output->infoWithDelayWarning(sprintf('Importing "<comment>%s</comment>" to the "<comment>%s</comment>" database', $file, $name));
98+
99+
$command = sprintf('%s %s | mysql --host=%s --port=%s --user=%s --password=%s %s', str_ends_with($file, '.sql.gz') ? 'gunzip <' : 'cat', $file, $host, $port, $user, $password, $name);
100+
101+
Process::runShellCommandline($command);
102+
103+
if ($tunnel instanceof Process) {
104+
$tunnel->stop();
105+
}
106+
107+
$output->info('Database imported successfully');
108+
}
109+
110+
/**
111+
* Determine the name of the database to export.
112+
*/
113+
private function determineDatabaseName(array $databaseServer, InputInterface $input, OutputInterface $output): string
114+
{
115+
$name = $this->getStringArgument($input, 'name');
116+
117+
if (!empty($name)) {
118+
return $name;
119+
} elseif (empty($name) && !$databaseServer['publicly_accessible']) {
120+
throw new RuntimeException('You must specify the name of the database to import the SQL file to for a private database server');
121+
}
122+
123+
return $output->choice('Which database would you like to import the SQL file to?', $this->apiClient->getDatabases($databaseServer['id']));
124+
}
125+
}

0 commit comments

Comments
 (0)