Skip to content

Commit 97da6ca

Browse files
committed
Use ShellCommand
1 parent 5d5b6b2 commit 97da6ca

4 files changed

Lines changed: 175 additions & 97 deletions

File tree

src/config/GeneralConfig.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,28 @@ class GeneralConfig extends BaseConfig
450450
*/
451451
public string|null|false|Closure $backupCommand = null;
452452

453+
/**
454+
* @var string|null The output format to pass to `pg_dump` when backing up the database.
455+
*
456+
* This setting has no effect with MySQL databases.
457+
*
458+
* Valid options are `custom`, `directory`, `tar`, or `plain`.
459+
* When set to `null` (default), `pg_restore` will default to `plain`
460+
* @see https://www.postgresql.org/docs/current/app-pgdump.html
461+
*
462+
* ::: code
463+
* ```php Static Config
464+
* ->backupCommandFormat('custom')
465+
* ```
466+
* ```shell Environment Override
467+
* CRAFT_BACKUP_COMMAND_FORMAT=custom
468+
* ```
469+
* :::
470+
*
471+
* @group Environment
472+
*/
473+
public ?string $backupCommandFormat = null;
474+
453475
/**
454476
* @var string|null The base URL Craft should use when generating control panel URLs.
455477
*
@@ -3543,6 +3565,27 @@ public function backupCommand(string|null|false|Closure $value): self
35433565
return $this;
35443566
}
35453567

3568+
/**
3569+
* The output format to pass to `pg_dump` when backing up the database.
3570+
*
3571+
* This setting has no effect with MySQL databases.
3572+
*
3573+
* Valid options are `custom`, `directory`, `tar`, or `plain`.
3574+
* When set to `null` (default), `pg_restore` will default to `plain`
3575+
* @see https://www.postgresql.org/docs/current/app-pgdump.html
3576+
*
3577+
* @group Environment
3578+
* @param string $value
3579+
* @return self
3580+
* @see $backupCommandFormat
3581+
* @since 4.9.0
3582+
*/
3583+
public function backupCommandFormat(string $value): self
3584+
{
3585+
$this->backupCommandFormat = $value;
3586+
return $this;
3587+
}
3588+
35463589
/**
35473590
* The base URL Craft should use when generating control panel URLs.
35483591
*

src/db/Connection.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,11 @@ public function getBackupFilePath(): string
209209
$version = Craft::$app->getInfo()->version ?? Craft::$app->getVersion();
210210
$filename = ($systemName ? "$systemName--" : '') . gmdate('Y-m-d-His') . "--v$version";
211211
$backupPath = Craft::$app->getPath()->getDbBackupPath();
212-
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '.sql';
212+
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . $this->_getDumpExtension();
213+
213214
$i = 0;
214215
while (file_exists($path)) {
215-
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '--' . ++$i . '.sql';
216+
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '--' . ++$i . $this->_getDumpExtension();
216217
}
217218
return $path;
218219
}
@@ -273,10 +274,8 @@ public function backupTo(string $filePath): void
273274

274275
if ($backupCommand === false) {
275276
throw new Exception('Database not backed up because the backup command is false.');
276-
} elseif ($backupCommand === null) {
277+
} elseif ($backupCommand === null || $backupCommand instanceof \Closure) {
277278
$backupCommand = $this->getSchema()->getDefaultBackupCommand($event->ignoreTables);
278-
} elseif ($backupCommand instanceof \Closure) {
279-
$backupCommand = $backupCommand($this->getSchema()->getDefaultBackupCommand($event->ignoreTables));
280279
}
281280

282281
// Create the shell command
@@ -297,10 +296,10 @@ public function backupTo(string $filePath): void
297296
if ($generalConfig->maxBackups) {
298297
$backupPath = Craft::$app->getPath()->getDbBackupPath();
299298

300-
// Grab all .sql files in the backup folder.
299+
// Grab all .sql/.dump files in the backup folder.
301300
$files = array_merge(
302-
glob($backupPath . DIRECTORY_SEPARATOR . '*.sql'),
303-
glob($backupPath . DIRECTORY_SEPARATOR . '*.sql.zip'),
301+
glob($backupPath . DIRECTORY_SEPARATOR . "*.{$this->_getDumpExtension()}"),
302+
glob($backupPath . DIRECTORY_SEPARATOR . "*.{$this->_getDumpExtension()}.zip"),
304303
);
305304

306305
// Sort them by file modified time descending (newest first).
@@ -470,6 +469,11 @@ public function trigger($name, Event $event = null)
470469
parent::trigger($name, $event);
471470
}
472471

472+
private function _getDumpExtension(): string
473+
{
474+
return $this->getIsPgsql() && $this->getSchema()->usePgRestore() ? '.dump' : '.sql';
475+
}
476+
473477
/**
474478
* Generates a FK, index, or PK name.
475479
*

src/db/mysql/Schema.php

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -152,79 +152,57 @@ public function createColumnSchemaBuilder($type, $length = null): ColumnSchemaBu
152152
*/
153153
public function getDefaultBackupCommand(?array $ignoreTables = null): string
154154
{
155-
$useSingleTransaction = true;
156-
$serverVersion = App::normalizeVersion($this->getServerVersion());
157-
155+
$baseCommand = (new ShellCommand('mysqldump'))
156+
->addArg('--defaults-file=', $this->_createDumpConfigFile())
157+
->addArg('--add-drop-table')
158+
->addArg('--comments')
159+
->addArg('--create-options')
160+
->addArg('--dump-date')
161+
->addArg('--no-autocommit')
162+
->addArg('--routines')
163+
->addArg('--default-character-set=', Craft::$app->getConfig()->getDb()->charset)
164+
->addArg('--set-charset')
165+
->addArg('--triggers')
166+
->addArg('--no-tablespaces');
167+
168+
$serverVersion = App::normalizeVersion(Craft::$app->getDb()->getServerVersion());
158169
$isMySQL5 = version_compare($serverVersion, '8', '<');
159170
$isMySQL8 = version_compare($serverVersion, '8', '>=');
171+
$ignoreTables = $ignoreTables ?? Craft::$app->getDb()->getIgnoredBackupTables();
172+
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->backupCommand;
160173

161174
// https://bugs.mysql.com/bug.php?id=109685
162-
if (($isMySQL5 && version_compare($serverVersion, '5.7.41', '>=')) ||
163-
($isMySQL8 && version_compare($serverVersion, '8.0.32', '>='))) {
164-
$useSingleTransaction = false;
165-
}
166-
167-
$defaultArgs =
168-
' --defaults-file="' . $this->_createDumpConfigFile() . '"' .
169-
' --add-drop-table' .
170-
' --comments' .
171-
' --create-options' .
172-
' --dump-date' .
173-
' --no-autocommit' .
174-
' --routines' .
175-
' --default-character-set=' . Craft::$app->getConfig()->getDb()->charset .
176-
' --set-charset' .
177-
' --triggers' .
178-
' --no-tablespaces';
175+
$useSingleTransaction =
176+
($isMySQL5 && version_compare($serverVersion, '5.7.41', '>=')) ||
177+
($isMySQL8 && version_compare($serverVersion, '8.0.32', '>='));
179178

180179
if ($useSingleTransaction) {
181-
$defaultArgs .= ' --single-transaction';
180+
$baseCommand->addArg('--single-transaction');
182181
}
183182

184-
// Find out if the db/dump client supports column-statistics
185-
$shellCommand = new ShellCommand();
186-
187-
if (Platform::isWindows()) {
188-
$shellCommand->setCommand('mysqldump --help | findstr "column-statistics"');
189-
} else {
190-
$shellCommand->setCommand('mysqldump --help | grep "column-statistics"');
183+
if ($this->supportsColumnStatistics()) {
184+
$baseCommand->addArg('--column-statistics=', '0');
191185
}
192186

193-
// If we don't have proc_open, maybe we've got exec
194-
if (!function_exists('proc_open') && function_exists('exec')) {
195-
$shellCommand->useExec = true;
196-
}
197-
198-
$success = $shellCommand->execute();
187+
$schemaDump = (clone $baseCommand)
188+
->addArg('--no-data')
189+
->addArg('--result-file=', '{file}')
190+
->addArg('{database}');
199191

200-
// if there was output, then column-statistics is supported and we should disable it
201-
if ($success && $shellCommand->getOutput()) {
202-
$defaultArgs .= ' --column-statistics=0';
203-
}
192+
$dataDump = (clone $baseCommand)
193+
->addArg('--no-create-info');
204194

205-
if ($ignoreTables === null) {
206-
$ignoreTables = $this->db->getIgnoredBackupTables();
207-
}
208-
$ignoreTableArgs = [];
209195
foreach ($ignoreTables as $table) {
210-
$table = $this->getRawTableName($table);
211-
$ignoreTableArgs[] = "--ignore-table={database}.$table";
196+
$table = Craft::$app->getDb()->getSchema()->getRawTableName($table);
197+
$dataDump->addArg('--ignore-table', "{schema}.$table");
212198
}
213199

214-
$schemaDump = 'mysqldump' .
215-
$defaultArgs .
216-
' --no-data' .
217-
' --result-file="{file}"' .
218-
' {database}';
219-
220-
$dataDump = 'mysqldump' .
221-
$defaultArgs .
222-
' --no-create-info' .
223-
' ' . implode(' ', $ignoreTableArgs) .
224-
' {database}' .
225-
' >> "{file}"';
200+
if ($commandFromConfig instanceof \Closure) {
201+
$schemaDump = $commandFromConfig($schemaDump);
202+
$dataDump = $commandFromConfig($dataDump);
203+
}
226204

227-
return $schemaDump . ' && ' . $dataDump;
205+
return "{$schemaDump->getExecCommand()} && {$dataDump->getExecCommand()} >> {file}";
228206
}
229207

230208
/**
@@ -235,10 +213,16 @@ public function getDefaultBackupCommand(?array $ignoreTables = null): string
235213
*/
236214
public function getDefaultRestoreCommand(): string
237215
{
238-
return 'mysql' .
239-
' --defaults-file="' . $this->_createDumpConfigFile() . '"' .
240-
' {database}' .
241-
' < "{file}"';
216+
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->backupCommand;
217+
$command = (new ShellCommand('mysql'))
218+
->addArg('--defaults-file=', $this->_createDumpConfigFile())
219+
->addArg('{database}');
220+
221+
if ($commandFromConfig instanceof \Closure) {
222+
$command = $commandFromConfig($command);
223+
}
224+
225+
return $command->getExecCommand() . ' < "{file}"';
242226
}
243227

244228
/**
@@ -383,6 +367,28 @@ protected function findConstraints($table): void
383367
}
384368
}
385369

370+
protected function supportsColumnStatistics(): bool
371+
{
372+
// Find out if the db/dump client supports column-statistics
373+
$shellCommand = new ShellCommand();
374+
375+
if (Platform::isWindows()) {
376+
$shellCommand->setCommand('mysqldump --help | findstr "column-statistics"');
377+
} else {
378+
$shellCommand->setCommand('mysqldump --help | grep "column-statistics"');
379+
}
380+
381+
// If we don't have proc_open, maybe we've got exec
382+
if (!function_exists('proc_open') && function_exists('exec')) {
383+
$shellCommand->useExec = true;
384+
}
385+
386+
$success = $shellCommand->execute();
387+
388+
// if there was output, then column-statistics is supported
389+
return $success && $shellCommand->getOutput();
390+
}
391+
386392
/**
387393
* Creates a temporary my.cnf file based on the DB config settings.
388394
*

src/db/pgsql/Schema.php

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Craft;
1212
use craft\db\Connection;
1313
use craft\db\TableSchema;
14+
use mikehaertl\shellcommand\Command as ShellCommand;
1415
use yii\db\Exception;
1516

1617
/**
@@ -116,29 +117,37 @@ public function getLastInsertID($sequenceName = ''): string
116117
*/
117118
public function getDefaultBackupCommand(?array $ignoreTables = null): string
118119
{
119-
if ($ignoreTables === null) {
120-
$ignoreTables = $this->db->getIgnoredBackupTables();
121-
}
122-
$ignoredTableArgs = [];
120+
$command = (new ShellCommand('pg_dump'))
121+
->addArg('--dbname=', '{database}')
122+
->addArg('--host=', '{server}')
123+
->addArg('--port=', '{port}')
124+
->addArg('--username=', '{user}')
125+
->addArg('--if-exists')
126+
->addArg('--clean')
127+
->addArg('--no-owner')
128+
->addArg('--no-privileges')
129+
->addArg('--no-acl')
130+
->addArg('--file=', '{file}')
131+
->addArg('--schema=', '{schema}');
132+
133+
$ignoreTables = $ignoreTables ?? Craft::$app->getDb()->getIgnoredBackupTables();
134+
$format = Craft::$app->getConfig()->getGeneral()->backupCommandFormat;
135+
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->backupCommand;
136+
123137
foreach ($ignoreTables as $table) {
124-
$table = $this->getRawTableName($table);
125-
$ignoredTableArgs[] = "--exclude-table-data '{schema}.$table'";
138+
$table = Craft::$app->getDb()->getSchema()->getRawTableName($table);
139+
$command->addArg('--exclude-table-data', "{schema}.$table");
140+
}
141+
142+
if ($format) {
143+
$command->addArg('--format=', $format);
144+
}
145+
146+
if ($commandFromConfig instanceof \Closure) {
147+
$command = $commandFromConfig($command);
126148
}
127149

128-
return $this->_pgpasswordCommand() .
129-
'pg_dump' .
130-
' --dbname={database}' .
131-
' --host={server}' .
132-
' --port={port}' .
133-
' --username={user}' .
134-
' --if-exists' .
135-
' --clean' .
136-
' --no-owner' .
137-
' --no-privileges' .
138-
' --no-acl' .
139-
' --file="{file}"' .
140-
' --schema={schema}' .
141-
' ' . implode(' ', $ignoredTableArgs);
150+
return $command->getExecCommand();
142151
}
143152

144153
/**
@@ -148,14 +157,22 @@ public function getDefaultBackupCommand(?array $ignoreTables = null): string
148157
*/
149158
public function getDefaultRestoreCommand(): string
150159
{
151-
return $this->_pgpasswordCommand() .
152-
'psql' .
153-
' --dbname={database}' .
154-
' --host={server}' .
155-
' --port={port}' .
156-
' --username={user}' .
157-
' --no-password' .
158-
' < "{file}"';
160+
$command = (new ShellCommand($this->usePgRestore() ? 'pg_restore' : 'psql'))
161+
->addArg('--dbname=', '{database}')
162+
->addArg('--host=', '{server}')
163+
->addArg('--port=', '{port}')
164+
->addArg('--username=', '{user}')
165+
->addArg('--no-password');
166+
167+
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->restoreCommand;
168+
169+
if ($commandFromConfig instanceof \Closure) {
170+
$command = $commandFromConfig($command);
171+
}
172+
173+
return $this->_pgpasswordCommand()
174+
. $command->getExecCommand()
175+
. '< "{file}"';
159176
}
160177

161178
/**
@@ -216,6 +233,14 @@ public function loadTableSchema($name): ?TableSchema
216233
return null;
217234
}
218235

236+
public function usePgRestore(): bool
237+
{
238+
return in_array(Craft::$app->getConfig()->getGeneral()->backupCommandFormat, [
239+
'custom',
240+
'directory',
241+
], true);
242+
}
243+
219244
/**
220245
* Collects extra foreign key information details for the given table.
221246
*

0 commit comments

Comments
 (0)