]> BookStack Code Mirror - system-cli/blob - src/Services/MySqlRunner.php
Bumped version
[system-cli] / src / Services / MySqlRunner.php
1 <?php declare(strict_types=1);
2
3 namespace Cli\Services;
4
5 use Exception;
6 use Symfony\Component\Process\Exception\ProcessTimedOutException;
7
8 class MySqlRunner
9 {
10     public function __construct(
11         protected string $host,
12         protected string $user,
13         protected string $password,
14         protected string $database,
15         protected int $port = 3306
16     ) {
17     }
18
19     /**
20      * @throws Exception
21      */
22     public function ensureOptionsSet(): void
23     {
24         $options = ['host', 'user', 'password', 'database'];
25         foreach ($options as $option) {
26             if (!$this->$option) {
27                 throw new Exception("Could not find a valid value for the \"{$option}\" database option.");
28             }
29         }
30     }
31
32     protected function createOptionsFile(): string
33     {
34         $path = tempnam(sys_get_temp_dir(), 'bs-cli-mysql-opts');
35         $password = str_replace('\\', '\\\\', $this->password);
36         $contents = "[client]\nuser='{$this->user}'\nhost='{$this->host}'\nport={$this->port}\npassword='{$password}'\nprotocol=TCP";
37         file_put_contents($path, $contents);
38         chmod($path, 0600);
39         return $path;
40     }
41
42     protected function getProgramRunnerInstance(): ProgramRunner
43     {
44         return (new ProgramRunner(['mariadb', 'mysql'], '/usr/bin/mysql'));
45     }
46
47     public function testConnection(): bool
48     {
49         $optionsFile = $this->createOptionsFile();
50
51         try {
52             $stdErr = $this->getProgramRunnerInstance()
53                 ->withTimeout(300)
54                 ->withIdleTimeout(300)
55                 ->runCapturingStdErr([
56                     "--defaults-extra-file={$optionsFile}",
57                     $this->database,
58                     '-e', "show tables;"
59                 ]);
60             unlink($optionsFile);
61         } catch (Exception $exception) {
62             unlink($optionsFile);
63             throw $exception;
64         }
65
66         return !$stdErr;
67     }
68
69     public function importSqlFile(string $sqlFilePath): void
70     {
71         $optionsFile = $this->createOptionsFile();
72
73         try {
74             $output = $this->getProgramRunnerInstance()
75                 ->withTimeout(300)
76                 ->withIdleTimeout(300)
77                 ->runCapturingStdErr([
78                     "--defaults-extra-file={$optionsFile}",
79                     $this->database,
80                     '-e', "source {$sqlFilePath}"
81                 ]);
82             unlink($optionsFile);
83         } catch (Exception $exception) {
84             unlink($optionsFile);
85             throw $exception;
86         }
87
88         if ($output) {
89             throw new Exception("Failed mysql file import with errors:\n{$output}");
90         }
91     }
92
93     public function dropTablesSql(): string
94     {
95         return <<<'HEREDOC'
96 SET FOREIGN_KEY_CHECKS = 0;
97 SET GROUP_CONCAT_MAX_LEN=32768;
98 SET @tables = NULL;
99 SELECT GROUP_CONCAT('`', table_name, '`') INTO @tables
100   FROM information_schema.tables
101   WHERE table_schema = (SELECT DATABASE());
102 SELECT IFNULL(@tables,'dummy') INTO @tables;
103
104 SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables);
105 PREPARE stmt FROM @tables;
106 EXECUTE stmt;
107 DEALLOCATE PREPARE stmt;
108 SET FOREIGN_KEY_CHECKS = 1;
109 HEREDOC;
110     }
111
112     public function runDumpToFile(string $filePath): string
113     {
114         $file = fopen($filePath, 'w');
115         $errors = "";
116         $warnings = "";
117         $hasOutput = false;
118         $optionsFile = $this->createOptionsFile();
119
120         try {
121             (new ProgramRunner(['mariadb-dump', 'mysqldump'], '/usr/bin/mysqldump'))
122                 ->withTimeout(300)
123                 ->withIdleTimeout(300)
124                 ->withAdditionalPathLocation('C:\xampp\mysql\bin')
125                 ->runWithoutOutputCallbacks([
126                     "--defaults-extra-file={$optionsFile}",
127                     '--single-transaction',
128                     '--no-tablespaces',
129                     $this->database,
130                 ], function ($data) use (&$file, &$hasOutput) {
131                     fwrite($file, $data);
132                     $hasOutput = true;
133                 }, function ($error) use (&$errors, &$warnings) {
134                     $lines = explode("\n", $error);
135                     foreach ($lines as $line) {
136                         if (str_starts_with(strtolower($line), 'warning: ')) {
137                             $warnings .= $line;
138                         } else if (!empty(trim($line))) {
139                             $errors .= $line . "\n";
140                         }
141                     }
142                 });
143             unlink($optionsFile);
144         } catch (\Exception $exception) {
145             fclose($file);
146             unlink($optionsFile);
147             if ($exception instanceof ProcessTimedOutException) {
148                 if (!$hasOutput) {
149                     throw new Exception("mysqldump operation timed-out.\nNo data has been received so the connection to your database may have failed.");
150                 } else {
151                     throw new Exception("mysqldump operation timed-out after data was received.");
152                 }
153             }
154             throw new Exception($exception->getMessage());
155         }
156
157         fclose($file);
158
159         if ($errors) {
160             throw new Exception("Failed mysqldump with errors:\n" . $errors);
161         }
162
163         return $warnings;
164     }
165
166     public static function fromEnvOptions(array $env): static
167     {
168         $host = ($env['DB_HOST'] ?? '');
169         $username = ($env['DB_USERNAME'] ?? '');
170         $password = ($env['DB_PASSWORD'] ?? '');
171         $database = ($env['DB_DATABASE'] ?? '');
172         $port = intval($env['DB_PORT'] ?? 3306);
173
174         return new static($host, $username, $password, $database, $port);
175     }
176 }