- Added better generally output formatting with warnings and blue info.
- Addressed deprecation warning when opening empty zip from temp file.
- Updated mysqldump so warnings did not stop dump, but are shown to
users.
- Massively increased timeouts where needed.
- Fixed a few typos.
- Logged extra mysql known issues.
- Added protocol to mysql commands since that's what's expected in
BookStack's usage.
- Updated required extensions list to be more comprehensive, based upon
actual need on FreeBSD (Where php defaults were minimal) and extracted
requirements to static class vars for easier editing.
> mysqldump: Couldn't execute 'FLUSH TABLES': Access denied; you need (at least one of) the RELOAD or FLUSH_TABLES privilege(s) for this operation (1227)
This was due to 8.0.32 mysqldump, changing the required permissions, and this should be largely [fixed as per 8.0.33](https://bugs.mysql.com/bug.php?id=109685).
-Temporary workaround is to provide the database user RELOAD permissions: `GRANT RELOAD ON *.* TO 'bookstack'@'%';`
\ No newline at end of file
+Temporary workaround is to provide the database user RELOAD permissions. For example:
+
+```mysql
+GRANT RELOAD ON *.* TO 'bookstack'@'localhost';
+```
+#### mysql - Restore throws permission error
+
+MySQL during restore can throw the following:
+
+> ERROR 1227 (42000) at line 18 in file: '/root/bookstack/restore-temp-1682771620/db.sql': Access denied; you need (at least one of) the SUPER, SYSTEM_VARIABLES_ADMIN or SESSION_VARIABLES_ADMIN privilege(s) for this operation
+
+This is due to mysql adding replication data to the output.
+We could set `--set-gtid-purged=value` during dump but does not exist for mariadb.
+Alternatively, we could maybe filter these SET lines? But need to be targeted and efficient since files dump files may be large.
+
+#### mysql - Only TCP connections
+
+This assumes a database connection via a TCP connection is being used by BookStack.
+Socket/other type of connections could technically be used with BookStack but is not something we advise
+or document within our docs or env options listing, so we make this assumption for simplicity.
\ No newline at end of file
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Output\ConsoleOutput;
+// Get the app with commands loaded
$app = require __DIR__ . '/src/app.php';
+// Configure output formatting
+$output = new ConsoleOutput();
+$formatter = $output->getFormatter();
+$formatter->setStyle('warn', new OutputFormatterStyle('yellow'));
+$formatter->setStyle('info', new OutputFormatterStyle('blue'));
+$formatter->setStyle('success', new OutputFormatterStyle('green'));
+$formatter->setStyle('error', new OutputFormatterStyle('red'));
+
+// Run the command and handle errors
try {
- $app->run();
+ $app->run(null, $output);
} catch (Exception $error) {
$output = (new ConsoleOutput())->getErrorOutput();
$output->getFormatter()->setStyle('error', new OutputFormatterStyle('red'));
$zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
$dumpTempFile = '';
$zip = new ZipArchive();
- $zip->open($zipTempFile, ZipArchive::CREATE);
+ $zip->open($zipTempFile, ZipArchive::OVERWRITE);
// Add default files (.env config file and this CLI if existing)
$zip->addFile(Paths::join($appDir, '.env'), '.env');
if ($handleDatabase) {
$output->writeln("<info>Dumping the database via mysqldump...</info>");
- $dumpTempFile = $this->createDatabaseDump($appDir);
+ $dumpTempFile = $this->createDatabaseDump($appDir, $output);
$output->writeln("<info>Adding database dump to backup archive...</info>");
$zip->addFile($dumpTempFile, 'db.sql');
}
rename($zipTempFile, $zipOutFile);
// Announce end
- $output->writeln("<info>Backup finished.</info>");
+ $output->writeln("<success>Backup finished.</success>");
$output->writeln("Output ZIP saved to: {$zipOutFile}");
return Command::SUCCESS;
* Create a database dump and return the path to the dumped SQL output.
* @throws CommandError
*/
- protected function createDatabaseDump(string $appDir): string
+ protected function createDatabaseDump(string $appDir, OutputInterface $output): string
{
$envOptions = EnvironmentLoader::loadMergedWithCurrentEnv($appDir);
$mysql = MySqlRunner::fromEnvOptions($envOptions);
$dumpTempFile = tempnam(sys_get_temp_dir(), 'bsdbdump');
try {
- $mysql->runDumpToFile($dumpTempFile);
+ $warnings = $mysql->runDumpToFile($dumpTempFile);
+ if ($warnings) {
+ $output->writeln("<warn>Received warnings during mysqldump:\n{$warnings}</warn>");
+ }
} catch (\Exception $exception) {
unlink($dumpTempFile);
throw new CommandError($exception->getMessage());
$this->generateAppKey($installDir);
// Announce end
- $output->writeln("<info>A BookStack install has been initialized at: {$installDir}\n</info>");
+ $output->writeln("<success>A BookStack install has been initialized at: {$installDir}\n</success>");
$output->writeln("<info>You will still need to:</info>");
$output->writeln("<info>- Update the .env file in the install with correct URL, database and email details.</info>");
$output->writeln("<info>- Run 'php artisan migrate' to set-up the database.</info>");
protected function cloneBookStackViaGit(string $installDir): void
{
$git = (new ProgramRunner('git', '/usr/bin/git'))
- ->withTimeout(240)
- ->withIdleTimeout(15);
+ ->withTimeout(300)
+ ->withIdleTimeout(300);
$errors = $git->runCapturingStdErr([
'clone', '-q',
{
$interactions = new InteractiveConsole($this->getHelper('question'), $input, $output);
- $output->writeln("<info>Warning!</info>");
- $output->writeln("<info>- A restore operation will overwrite and remove files & content from an existing instance.</info>");
- $output->writeln("<info>- Any existing tables within the configured database will be dropped.</info>");
- $output->writeln("<info>- You should only restore into an instance of the same or newer BookStack version.</info>");
- $output->writeln("<info>- This command won't handle, restore or address any server configuration.</info>");
+ $output->writeln("<warn>Warning!</warn>");
+ $output->writeln("<warn>- A restore operation will overwrite and remove files & content from an existing instance.</warn>");
+ $output->writeln("<warn>- Any existing tables within the configured database will be dropped.</warn>");
+ $output->writeln("<warn>- You should only restore into an instance of the same or newer BookStack version.</warn>");
+ $output->writeln("<warn>- This command won't handle, restore or address any server configuration.</warn>");
$appDir = AppLocator::require($input->getOption('app-directory'));
$output->writeln("<info>Checking system requirements...</info>");
}
$output->writeln("<info>The checked elements will be restored into [{$appDir}].</info>");
- $output->writeln("<info>Existing content will be overwritten.</info>");
+ $output->writeln("<warn>Existing content will be overwritten.</warn>");
if (!$interactions->confirm("Do you want to continue?")) {
$output->writeln("<info>Stopping restore operation.</info>");
$output->writeln("<info>Cleaning up extract directory...</info>");
$this->deleteDirectoryAndContents($extractDir);
- $output->writeln("<info>\nRestore operation complete!</info>");
+ $output->writeln("<success>\nRestore operation complete!</success>");
return Command::SUCCESS;
}
$artisan->run(['config:clear']);
$artisan->run(['view:clear']);
- $output->writeln("<info>Your BookStack instance at [{$appDir}] has been updated!</info>");
+ $output->writeln("<success>Your BookStack instance at [{$appDir}] has been updated!</success>");
return Command::SUCCESS;
}
public function run(array $commandArgs)
{
$errors = (new ProgramRunner('php', '/usr/bin/php'))
- ->withTimeout(60)
- ->withIdleTimeout(5)
+ ->withTimeout(600)
+ ->withIdleTimeout(600)
->withEnvironment(EnvironmentLoader::load($this->appDir))
->runCapturingAllOutput([
$this->appDir . DIRECTORY_SEPARATOR . 'artisan',
{
return (new ProgramRunner('composer', '/usr/local/bin/composer'))
->withTimeout(300)
- ->withIdleTimeout(60)
+ ->withIdleTimeout(300)
->withAdditionalPathLocation($this->appDir);
}
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
+ '--protocol=TCP',
$this->database,
'-e', "show tables;"
]);
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
+ '--protocol=TCP',
$this->database,
'-e', "source {$sqlFilePath}"
]);
HEREDOC;
}
- public function runDumpToFile(string $filePath): void
+ public function runDumpToFile(string $filePath): string
{
$file = fopen($filePath, 'w');
$errors = "";
+ $warnings = "";
$hasOutput = false;
try {
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
+ '--protocol=TCP',
'--single-transaction',
'--no-tablespaces',
$this->database,
], function ($data) use (&$file, &$hasOutput) {
fwrite($file, $data);
$hasOutput = true;
- }, function ($error) use (&$errors) {
- $errors .= $error . "\n";
+ }, function ($error) use (&$errors, &$warnings) {
+ $lines = explode("\n", $error);
+ foreach ($lines as $line) {
+ if (str_starts_with(strtolower($line), 'warning: ')) {
+ $warnings .= $line;
+ } else {
+ $errors .= $line . "\n";
+ }
+ }
});
} catch (\Exception $exception) {
fclose($file);
if ($errors) {
throw new Exception("Failed mysqldump with errors:\n" . $errors);
}
+
+ return $warnings;
}
public static function fromEnvOptions(array $env): static
class RequirementsValidator
{
+ protected static string $phpVersion = '8.0.2';
+ protected static array $extensions = [
+ 'curl',
+ 'dom',
+ 'fileinfo',
+ 'gd',
+ 'iconv',
+ 'libxml',
+ 'mbstring',
+ 'mysqlnd',
+ 'pdo_mysql',
+ 'session',
+ 'simplexml',
+ 'tokenizer',
+ 'xml',
+ ];
+
/**
* Ensure the required PHP extensions are installed for this command.
* @throws Exception
{
$errors = [];
- if (version_compare(PHP_VERSION, '8.0.2') < 0) {
- $errors[] = "PHP >= 8.0.2 is required to install BookStack.";
+ if (version_compare(PHP_VERSION, static::$phpVersion) < 0) {
+ $errors[] = sprintf("PHP >= %s is required to install BookStack.", static::$phpVersion);
}
- $requiredExtensions = ['curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml'];
- foreach ($requiredExtensions as $extension) {
+ foreach (static::$extensions as $extension) {
if (!extension_loaded($extension)) {
- $errors[] = "The \"{$extension}\" PHP extension is required by not active.";
+ $errors[] = "The \"{$extension}\" PHP extension is required but not active.";
}
}