namespace Cli\Commands;
+use Cli\Services\ProgramRunner;
use Minicli\Command\CommandCall;
use RecursiveDirectoryIterator;
use SplFileInfo;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
use ZipArchive;
final class BackupCommand
// Create a new ZIP file
$zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+ $dumpTempFile = '';
$zip = new ZipArchive();
$zip->open($zipTempFile, ZipArchive::CREATE);
$dumpTempFile = $this->createDatabaseDump();
echo "Adding database dump to backup archive...\n";
$zip->addFile($dumpTempFile, 'db.sql');
- // Delete our temporary DB dump file
- unlink($dumpTempFile);
}
if ($handleUploads) {
// Close off our zip and move it to the required location
$zip->close();
+ // Delete our temporary DB dump file if exists. Must be done after zip close.
+ if ($dumpTempFile) {
+ unlink($dumpTempFile);
+ }
+ // Move the zip into the target location
rename($zipTempFile, $zipOutFile);
// Announce end
}
}
- // Create a mysqldump for the BookStack database
- $executableFinder = new ExecutableFinder();
- $mysqldumpPath = $executableFinder->find('mysqldump', '/usr/bin/mysqldump');
-
- if (!is_file($mysqldumpPath)) {
- throw new CommandError('Could not locate "mysqldump" program');
- }
-
- $process = new Process([
- $mysqldumpPath,
- '-h', $dbOptions['host'],
- '-u', $dbOptions['username'],
- '-p' . $dbOptions['password'],
- '--single-transaction',
- '--no-tablespaces',
- $dbOptions['database'],
- ]);
- $process->setTimeout(240);
- $process->setIdleTimeout(5);
- $process->start();
-
$errors = "";
$hasOutput = false;
- $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+ $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsdbdump');
$dumpTempFileResource = fopen($dumpTempFile, 'w');
+
try {
- foreach ($process as $type => $data) {
- if ($process::OUT === $type) {
+ (new ProgramRunner('mysqldump', '/usr/bin/mysqldump'))
+ ->withTimeout(240)
+ ->withIdleTimeout(15)
+ ->runWithoutOutputCallbacks([
+ '-h', $dbOptions['host'],
+ '-u', $dbOptions['username'],
+ '-p' . $dbOptions['password'],
+ '--single-transaction',
+ '--no-tablespaces',
+ $dbOptions['database'],
+ ], function ($data) use (&$dumpTempFileResource, &$hasOutput) {
fwrite($dumpTempFileResource, $data);
$hasOutput = true;
- } else { // $process::ERR === $type
- $errors .= $data . "\n";
- }
- }
- } catch (ProcessTimedOutException $timedOutException) {
+ }, function ($error) use (&$errors) {
+ $errors .= $error . "\n";
+ });
+ } catch (\Exception $exception) {
fclose($dumpTempFileResource);
unlink($dumpTempFile);
- if (!$hasOutput) {
- throw new CommandError("mysqldump operation timed-out.\nNo data has been received so the connection to your database may have failed.");
- } else {
- throw new CommandError("mysqldump operation timed-out after data was received.");
+ if ($exception instanceof ProcessTimedOutException) {
+ if (!$hasOutput) {
+ throw new CommandError("mysqldump operation timed-out.\nNo data has been received so the connection to your database may have failed.");
+ } else {
+ throw new CommandError("mysqldump operation timed-out after data was received.");
+ }
}
+ throw new CommandError($exception->getMessage());
}
fclose($dumpTempFileResource);
namespace Cli\Commands;
+use Cli\Services\ProgramRunner;
use Minicli\Command\CommandCall;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
class InitCommand
{
{
$this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
- // TODO - Dedupe the command stuff going on.
// TODO - Check composer and git exists before running
// TODO - Look at better way of handling env usage, on demand maybe where needed?
// Env loading in main `run` script if confilicting with certain bits here (app key generate, hence APP_KEY overload)
protected function generateAppKey(string $installDir): void
{
- // Find reference to php
- $executableFinder = new ExecutableFinder();
- $phpPath = $executableFinder->find('php', '/usr/bin/php');
- if (!is_file($phpPath)) {
- throw new CommandError('Could not locate "php" program.');
- }
-
- $process = new Process([
- $phpPath,
- $installDir . DIRECTORY_SEPARATOR . 'artisan',
- 'key:generate', '--force', '-n', '-q'
- ], null, ['APP_KEY' => 'SomeRandomString']);
- $process->setTimeout(240);
- $process->setIdleTimeout(5);
- $process->start();
-
- $errors = '';
- foreach ($process as $type => $data) {
- // Errors are on stdout for artisan
- $errors .= $data . "\n";
- }
+ $errors = (new ProgramRunner('php', '/usr/bin/php'))
+ ->withTimeout(60)
+ ->withIdleTimeout(5)
+ ->withEnvironment(['APP_KEY' => 'SomeRandomString'])
+ ->runCapturingAllOutput([
+ $installDir . DIRECTORY_SEPARATOR . 'artisan',
+ 'key:generate', '--force', '-n', '-q'
+ ]);
if ($errors) {
throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
*/
protected function installComposerDependencies(string $installDir): void
{
- // Find reference to composer
- $executableFinder = new ExecutableFinder();
- $composerPath = $executableFinder->find('composer', '/usr/local/bin/composer');
- if (!is_file($composerPath)) {
- throw new CommandError('Could not locate "composer" program.');
- }
-
- $process = new Process([
- $composerPath, 'install',
- '--no-dev', '-n', '-q', '--no-progress',
- '-d', $installDir
- ]);
- $process->setTimeout(240);
- $process->setIdleTimeout(15);
- $process->start();
-
- $errors = '';
- foreach ($process as $type => $data) {
- if ($process::ERR === $type) {
- $errors .= $data . "\n";
- }
- }
+ $errors = (new ProgramRunner('composer', '/usr/local/bin/composer'))
+ ->withTimeout(300)
+ ->withIdleTimeout(15)
+ ->runCapturingStdErr([
+ 'install',
+ '--no-dev', '-n', '-q', '--no-progress',
+ '-d', $installDir
+ ]);
if ($errors) {
throw new CommandError("Failed composer install with errors:\n" . $errors);
*/
protected function cloneBookStackViaGit(string $installDir): void
{
- // Find reference to git
- $executableFinder = new ExecutableFinder();
- $gitPath = $executableFinder->find('git', '/usr/bin/bit');
- if (!is_file($gitPath)) {
- throw new CommandError('Could not locate "git" program.');
- }
-
- $process = new Process([
- $gitPath, 'clone', '-q',
- '--branch', 'release',
- '--single-branch',
- 'https://github.com/BookStackApp/BookStack.git',
- $installDir
- ]);
- $process->setTimeout(240);
- $process->setIdleTimeout(15);
- $process->start();
-
- $errors = '';
- foreach ($process as $type => $data) {
- if ($process::ERR === $type) {
- $errors .= $data . "\n";
- }
- }
+ $errors = (new ProgramRunner('git', '/usr/bin/git'))
+ ->withTimeout(240)
+ ->withIdleTimeout(15)
+ ->runCapturingStdErr([
+ 'clone', '-q',
+ '--branch', 'release',
+ '--single-branch',
+ 'https://github.com/BookStackApp/BookStack.git',
+ $installDir
+ ]);
if ($errors) {
throw new CommandError("Failed git clone with errors:\n" . $errors);
--- /dev/null
+<?php
+
+namespace Cli\Services;
+
+use Symfony\Component\Process\ExecutableFinder;
+use Symfony\Component\Process\Process;
+
+class ProgramRunner
+{
+ protected int $timeout = 240;
+ protected int $idleTimeout = 15;
+ protected array $environment = [];
+
+ public function __construct(
+ protected string $program,
+ protected string $defaultPath
+ ) {
+ }
+
+ public function withTimeout(int $timeoutSeconds): static
+ {
+ $this->timeout = $timeoutSeconds;
+ return $this;
+ }
+
+ public function withIdleTimeout(int $idleTimeoutSeconds): static
+ {
+ $this->idleTimeout = $idleTimeoutSeconds;
+ return $this;
+ }
+
+ public function withEnvironment(array $environment): static
+ {
+ $this->environment = $environment;
+ return $this;
+ }
+
+ public function runCapturingAllOutput(array $args): string
+ {
+ $output = '';
+ $callable = function ($data) use (&$output) {
+ $output .= $data . "\n";
+ };
+
+ $this->runWithoutOutputCallbacks($args, $callable, $callable);
+ return $output;
+ }
+
+ public function runCapturingStdErr(array $args): string
+ {
+ $err = '';
+ $this->runWithoutOutputCallbacks($args, fn() => '', function ($data) use (&$err) {
+ $err .= $data . "\n";
+ });
+ return $err;
+ }
+
+ public function runWithoutOutputCallbacks(array $args, callable $stdOutCallback, callable $stdErrCallback): void
+ {
+ $process = $this->startProcess($args);
+ foreach ($process as $type => $data) {
+ if ($type === $process::ERR) {
+ $stdErrCallback($data);
+ } else {
+ $stdOutCallback($data);
+ }
+ }
+ }
+
+ protected function startProcess(array $args): Process
+ {
+ $programPath = $this->resolveProgramPath();
+ $process = new Process([$programPath, ...$args], null, $this->environment);
+ $process->setTimeout($this->timeout);
+ $process->setIdleTimeout($this->idleTimeout);
+ $process->start();
+ return $process;
+ }
+
+ protected function resolveProgramPath(): string
+ {
+ $executableFinder = new ExecutableFinder();
+ $path = $executableFinder->find($this->program, $this->defaultPath);
+
+ if (is_null($path) || !is_file($path)) {
+ throw new \Exception("Could not locate \"{$this->program}\" program.");
+ }
+
+ return $path;
+ }
+}
},
"autoload": {
"psr-4": {
- "Cli\\Commands\\": "Commands/"
+ "Cli\\Commands\\": "Commands/",
+ "Cli\\Services\\": "Services/"
}
},
"config": {
require __DIR__ . '/vendor/autoload.php';
use Cli\Commands\BackupCommand;
-use Cli\Commands\CommandError;
use Cli\Commands\InitCommand;
use Minicli\App;
try {
$app->runCommand($argv);
-} catch (CommandError $error) {
+} catch (Exception $error) {
fwrite(STDERR, "An error occurred when attempting to run a command:\n");
fwrite(STDERR, $error->getMessage() . "\n");
exit(1);