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