Skip to content

Commit

Permalink
Merge branch '4.9' into 5.1
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG-WIP.md
#	src/db/mysql/Schema.php
#	src/db/pgsql/Schema.php
  • Loading branch information
timkelty committed Apr 30, 2024
2 parents 23a2b1e + 271972b commit 81acb21
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 107 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- Element conditions within field layout designers’ component settings now only list custom fields present in the current field layout. ([#14787](https://github.com/craftcms/cms/issues/14787))
- Improved the behavior of the URI input within Edit Route modals. ([#14884](https://github.com/craftcms/cms/issues/14884))
- Added the `asyncCsrfInputs` config setting. ([#14625](https://github.com/craftcms/cms/pull/14625))
- Added the `backupCommandFormat` config setting. ([#14897](https://github.com/craftcms/cms/pull/14897))
- The `backupCommand` config setting can now be set to a closure, which will be passed a `mikehaertl\shellcommand\Command` object. ([#14897](https://github.com/craftcms/cms/pull/14897))
- Added the `safeMode` config setting. ([#14734](https://github.com/craftcms/cms/pull/14734))
- `resave` commands now support an `--if-invalid` option. ([#14731](https://github.com/craftcms/cms/issues/14731))
- Improved the styling of conditional tabs and UI elements within field layout designers.
Expand Down
61 changes: 53 additions & 8 deletions src/config/GeneralConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace craft\config;

use Closure;
use Craft;
use craft\helpers\ConfigHelper;
use craft\helpers\DateTimeHelper;
Expand Down Expand Up @@ -420,7 +421,7 @@ class GeneralConfig extends BaseConfig
public bool $backupOnUpdate = true;

/**
* @var string|null|false The shell command that Craft should execute to create a database backup.
* @var string|null|false|Closure The shell command that Craft should execute to create a database backup.
*
* When set to `null` (default), Craft will run `mysqldump` or `pg_dump`, provided that those libraries are in the `$PATH` variable
* for the system user running the web server.
Expand Down Expand Up @@ -448,7 +449,30 @@ class GeneralConfig extends BaseConfig
*
* @group Environment
*/
public string|null|false $backupCommand = null;
public string|null|false|Closure $backupCommand = null;

/**
* @var string|null The output format to pass to `pg_dump` when backing up the database.
*
* This setting has no effect with MySQL databases.
*
* Valid options are `custom`, `directory`, `tar`, or `plain`.
* When set to `null` (default), `pg_restore` will default to `plain`
* @see https://www.postgresql.org/docs/current/app-pgdump.html
*
* ::: code
* ```php Static Config
* ->backupCommandFormat('custom')
* ```
* ```shell Environment Override
* CRAFT_BACKUP_COMMAND_FORMAT=custom
* ```
* :::
*
* @group Environment
* @since 4.9.0
*/
public ?string $backupCommandFormat = null;

/**
* @var string|null The base URL Craft should use when generating control panel URLs.
Expand Down Expand Up @@ -2475,7 +2499,7 @@ class GeneralConfig extends BaseConfig
public string $resourceBaseUrl = '@web/cpresources';

/**
* @var string|null|false The shell command Craft should execute to restore a database backup.
* @var string|null|false|Closure The shell command Craft should execute to restore a database backup.
*
* By default Craft will run `mysql` or `psql`, provided those libraries are in the `$PATH` variable for the user the web server is running as.
*
Expand All @@ -2501,7 +2525,7 @@ class GeneralConfig extends BaseConfig
*
* @group Environment
*/
public string|null|false $restoreCommand = null;
public string|null|false|Closure $restoreCommand = null;

/**
* @var bool Whether asset URLs should be revved so browsers don’t load cached versions when they’re modified.
Expand Down Expand Up @@ -3632,17 +3656,38 @@ public function backupOnUpdate(bool $value = true): self
* ```
*
* @group Environment
* @param string|null|false $value
* @param string|null|false|Closure $value
* @return self
* @see $backupCommand
* @since 4.2.0
*/
public function backupCommand(string|null|false $value): self
public function backupCommand(string|null|false|Closure $value): self
{
$this->backupCommand = $value;
return $this;
}

/**
* The output format to pass to `pg_dump` when backing up the database.
*
* This setting has no effect with MySQL databases.
*
* Valid options are `custom`, `directory`, `tar`, or `plain`.
* When set to `null` (default), `pg_restore` will default to `plain`
* @see https://www.postgresql.org/docs/current/app-pgdump.html
*
* @group Environment
* @param string $value
* @return self
* @see $backupCommandFormat
* @since 4.9.0
*/
public function backupCommandFormat(string $value): self
{
$this->backupCommandFormat = $value;
return $this;
}

/**
* The base URL Craft should use when generating control panel URLs.
*
Expand Down Expand Up @@ -5996,12 +6041,12 @@ public function resourceBaseUrl(string $value): self
* ```
*
* @group Environment
* @param string|null|false $value
* @param string|null|false|Closure $value
* @return self
* @see $restoreCommand
* @since 4.2.0
*/
public function restoreCommand(string|null|false $value): self
public function restoreCommand(string|null|false|Closure $value): self
{
$this->restoreCommand = $value;
return $this;
Expand Down
2 changes: 1 addition & 1 deletion src/console/controllers/DbController.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public function actionBackup(?string $path = null): int
$db->backupTo($path);
if ($this->zip) {
$zipPath = FileHelper::zip($path);
unlink($path);
FileHelper::unlink($path);
$path = $zipPath;
}
} catch (Throwable $e) {
Expand Down
28 changes: 15 additions & 13 deletions src/db/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,11 @@ public function getBackupFilePath(): string
$version = Craft::$app->getInfo()->version ?? Craft::$app->getVersion();
$filename = ($systemName ? "$systemName--" : '') . gmdate('Y-m-d-His') . "--v$version";
$backupPath = Craft::$app->getPath()->getDbBackupPath();
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '.sql';
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . $this->_getDumpExtension();

$i = 0;
while (file_exists($path)) {
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '--' . ++$i . '.sql';
$path = $backupPath . DIRECTORY_SEPARATOR . $filename . '--' . ++$i . $this->_getDumpExtension();
}
return $path;
}
Expand Down Expand Up @@ -290,12 +291,10 @@ public function backupTo(string $filePath): void
// Determine the command that should be executed
$backupCommand = Craft::$app->getConfig()->getGeneral()->backupCommand;

if ($backupCommand === null) {
$backupCommand = $this->getSchema()->getDefaultBackupCommand($event->ignoreTables);
}

if ($backupCommand === false) {
throw new Exception('Database not backed up because the backup command is false.');
} elseif ($backupCommand === null || $backupCommand instanceof \Closure) {
$backupCommand = $this->getSchema()->getDefaultBackupCommand($event->ignoreTables);
}

// Create the shell command
Expand All @@ -316,10 +315,10 @@ public function backupTo(string $filePath): void
if ($generalConfig->maxBackups) {
$backupPath = Craft::$app->getPath()->getDbBackupPath();

// Grab all .sql files in the backup folder.
// Grab all .sql/.dump files in the backup folder.
$files = array_merge(
glob($backupPath . DIRECTORY_SEPARATOR . '*.sql'),
glob($backupPath . DIRECTORY_SEPARATOR . '*.sql.zip'),
glob($backupPath . DIRECTORY_SEPARATOR . "*.{$this->_getDumpExtension()}"),
glob($backupPath . DIRECTORY_SEPARATOR . "*.{$this->_getDumpExtension()}.zip"),
);

// Sort them by file modified time descending (newest first).
Expand Down Expand Up @@ -356,12 +355,10 @@ public function restore(string $filePath): void
// Determine the command that should be executed
$restoreCommand = Craft::$app->getConfig()->getGeneral()->restoreCommand;

if ($restoreCommand === null) {
$restoreCommand = $this->getSchema()->getDefaultRestoreCommand();
}

if ($restoreCommand === false) {
throw new Exception('Database not restored because the restore command is false.');
} elseif ($restoreCommand === null || $restoreCommand instanceof \Closure) {
$restoreCommand = $this->getSchema()->getDefaultRestoreCommand();
}

// Create the shell command
Expand Down Expand Up @@ -489,6 +486,11 @@ public function trigger($name, Event $event = null)
parent::trigger($name, $event);
}

private function _getDumpExtension(): string
{
return $this->getIsPgsql() && $this->getSchema()->usePgRestore() ? '.dump' : '.sql';
}

/**
* Generates a FK, index, or PK name.
*
Expand Down
123 changes: 65 additions & 58 deletions src/db/mysql/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace craft\db\mysql;

use Composer\Util\Platform;
use Craft;
use craft\db\Connection;
use craft\db\ExpressionBuilder;
Expand Down Expand Up @@ -182,76 +183,54 @@ public function createColumnSchemaBuilder($type, $length = null): ColumnSchemaBu
*/
public function getDefaultBackupCommand(?array $ignoreTables = null): string
{
$useSingleTransaction = true;
$serverVersion = App::normalizeVersion($this->getServerVersion());
$baseCommand = (new ShellCommand('mysqldump'))
->addArg('--defaults-file=', $this->_createDumpConfigFile())
->addArg('--add-drop-table')
->addArg('--comments')
->addArg('--create-options')
->addArg('--dump-date')
->addArg('--no-autocommit')
->addArg('--routines')
->addArg('--default-character-set=', Craft::$app->getConfig()->getDb()->charset)
->addArg('--set-charset')
->addArg('--triggers')
->addArg('--no-tablespaces');

$serverVersion = App::normalizeVersion(Craft::$app->getDb()->getServerVersion());
$isMySQL8 = version_compare($serverVersion, '8', '>=');
$ignoreTables = $ignoreTables ?? Craft::$app->getDb()->getIgnoredBackupTables();
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->backupCommand;

// https://bugs.mysql.com/bug.php?id=109685
if ($isMySQL8 && version_compare($serverVersion, '8.0.32', '>=')) {
$useSingleTransaction = false;
}

$defaultArgs =
' --defaults-file="' . $this->_createDumpConfigFile() . '"' .
' --add-drop-table' .
' --comments' .
' --create-options' .
' --dump-date' .
' --no-autocommit' .
' --routines' .
' --default-character-set=' . Craft::$app->getConfig()->getDb()->getCharset() .
' --set-charset' .
' --triggers' .
' --no-tablespaces';
$useSingleTransaction = $isMySQL8 && version_compare($serverVersion, '8.0.32', '>=');

if ($useSingleTransaction) {
$defaultArgs .= ' --single-transaction';
}

// Find out if the db/dump client supports column-statistics
$shellCommand = new ShellCommand();

if (App::isWindows()) {
$shellCommand->setCommand('mysqldump --help | findstr "column-statistics"');
} else {
$shellCommand->setCommand('mysqldump --help | grep "column-statistics"');
$baseCommand->addArg('--single-transaction');
}

// If we don't have proc_open, maybe we've got exec
if (!function_exists('proc_open') && function_exists('exec')) {
$shellCommand->useExec = true;
if ($this->supportsColumnStatistics()) {
$baseCommand->addArg('--column-statistics=', '0');
}

$success = $shellCommand->execute();
$schemaDump = (clone $baseCommand)
->addArg('--no-data')
->addArg('--result-file=', '{file}')
->addArg('{database}');

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

if ($ignoreTables === null) {
$ignoreTables = $this->db->getIgnoredBackupTables();
}
$ignoreTableArgs = [];
foreach ($ignoreTables as $table) {
$table = $this->getRawTableName($table);
$ignoreTableArgs[] = "--ignore-table={database}.$table";
$dataDump->addArg('--ignore-table', "{schema}.$table");
}

$schemaDump = 'mysqldump' .
$defaultArgs .
' --no-data' .
' --result-file="{file}"' .
' {database}';

$dataDump = 'mysqldump' .
$defaultArgs .
' --no-create-info' .
' ' . implode(' ', $ignoreTableArgs) .
' {database}' .
' >> "{file}"';
if ($commandFromConfig instanceof \Closure) {
$schemaDump = $commandFromConfig($schemaDump);
$dataDump = $commandFromConfig($dataDump);
}

return $schemaDump . ' && ' . $dataDump;
return "{$schemaDump->getExecCommand()} && {$dataDump->getExecCommand()} >> {file}";
}

/**
Expand All @@ -262,10 +241,16 @@ public function getDefaultBackupCommand(?array $ignoreTables = null): string
*/
public function getDefaultRestoreCommand(): string
{
return 'mysql' .
' --defaults-file="' . $this->_createDumpConfigFile() . '"' .
' {database}' .
' < "{file}"';
$commandFromConfig = Craft::$app->getConfig()->getGeneral()->restoreCommand;
$command = (new ShellCommand('mysql'))
->addArg('--defaults-file=', $this->_createDumpConfigFile())
->addArg('{database}');

if ($commandFromConfig instanceof \Closure) {
$command = $commandFromConfig($command);
}

return $command->getExecCommand() . ' < "{file}"';
}

/**
Expand Down Expand Up @@ -422,6 +407,28 @@ protected function findConstraints($table): void
}
}

protected function supportsColumnStatistics(): bool
{
// Find out if the db/dump client supports column-statistics
$shellCommand = new ShellCommand();

if (Platform::isWindows()) {
$shellCommand->setCommand('mysqldump --help | findstr "column-statistics"');
} else {
$shellCommand->setCommand('mysqldump --help | grep "column-statistics"');
}

// If we don't have proc_open, maybe we've got exec
if (!function_exists('proc_open') && function_exists('exec')) {
$shellCommand->useExec = true;
}

$success = $shellCommand->execute();

// if there was output, then column-statistics is supported
return $success && $shellCommand->getOutput();
}

/**
* Creates a temporary my.cnf file based on the DB config settings.
*
Expand Down
Loading

0 comments on commit 81acb21

Please sign in to comment.