From: Dan Brown Date: Sat, 4 Mar 2023 02:40:29 +0000 (+0000) Subject: Added "init" command to admin-cli X-Git-Url: http://source.bookstackapp.com/system-cli/commitdiff_plain/e799f2c2136f4f81c6d5c9ce95fce6990ef10260 Added "init" command to admin-cli Got to basic working state, some todos in there. --- diff --git a/scripts/Commands/BackupCommand.php b/scripts/Commands/BackupCommand.php index f2c47a2..a607f34 100644 --- a/scripts/Commands/BackupCommand.php +++ b/scripts/Commands/BackupCommand.php @@ -66,7 +66,7 @@ final class BackupCommand $zip->close(); rename($zipTempFile, $zipOutFile); - // Announce end and display errors + // Announce end echo "Backup finished.\nOutput ZIP saved to: {$zipOutFile}\n"; } diff --git a/scripts/Commands/InitCommand.php b/scripts/Commands/InitCommand.php new file mode 100644 index 0000000..1c943a2 --- /dev/null +++ b/scripts/Commands/InitCommand.php @@ -0,0 +1,205 @@ +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) + // See dotenv's Dotenv::createArrayBacked as way to go this. + // (More of a change for 'backup' command). + // TODO - Potentially download composer? + + $suggestedOutPath = $input->subcommand; + if ($suggestedOutPath === 'default') { + $suggestedOutPath = ''; + } + + echo "Locating and checking install directory...\n"; + $installDir = $this->getInstallDir($suggestedOutPath); + $this->ensureInstallDirEmpty($installDir); + + echo "Cloning down BookStack project to install directory...\n"; + $this->cloneBookStackViaGit($installDir); + + echo "Installing application dependencies using composer...\n"; + $this->installComposerDependencies($installDir); + + echo "Creating .env file from .env.example...\n"; + copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env'); + sleep(1); + + echo "Generating app key...\n"; + $this->generateAppKey($installDir); + + // Announce end + echo "A BookStack install has been initialized at: {$installDir}\n\n"; + echo "You will still need to:\n"; + echo "- Update the .env file in the install with correct URL, database and email details.\n"; + echo "- Run 'php artisan migrate' to set-up the database.\n"; + echo "- Configure your webserver for use with BookStack.\n"; + echo "- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.\n"; + } + + /** + * Ensure the required PHP extensions are installed for this command. + * @throws CommandError + */ + protected function ensureRequiredExtensionInstalled(): void + { +// if (!extension_loaded('zip')) { +// throw new CommandError('The "zip" PHP extension is required to run this command'); +// } + } + + 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"; + } + + if ($errors) { + throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors); + } + } + + /** + * Run composer install to download PHP dependencies. + * @throws CommandError + */ + 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"; + } + } + + if ($errors) { + throw new CommandError("Failed composer install with errors:\n" . $errors); + } + } + + /** + * Clone a new instance of BookStack to the given install folder. + * @throws CommandError + */ + 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"; + } + } + + if ($errors) { + throw new CommandError("Failed git clone with errors:\n" . $errors); + } + } + + /** + * Ensure that the installation directory is completely empty to avoid potential conflicts or issues. + * @throws CommandError + */ + protected function ensureInstallDirEmpty(string $installDir): void + { + $contents = array_diff(scandir($installDir), ['..', '.']); + if (count($contents) > 0) { + throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location."); + } + } + + /** + * Build a full path to the intended location for the BookStack install. + * @throws CommandError + */ + protected function getInstallDir(string $suggestedDir): string + { + $dir = getcwd(); + + if ($suggestedDir) { + if (is_file($suggestedDir)) { + throw new CommandError("Was provided [{$suggestedDir}] as an install path but existing file provided."); + } else if (is_dir($suggestedDir)) { + $dir = realpath($suggestedDir); + } else if (is_dir(dirname($suggestedDir))) { + $created = mkdir($suggestedDir); + if (!$created) { + throw new CommandError("Could not create directory [{$suggestedDir}] for install."); + } + $dir = $suggestedDir; + } else { + throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder."); + } + } + + return $dir; + } +} diff --git a/scripts/run b/scripts/run index ff38c81..6f1d205 100644 --- a/scripts/run +++ b/scripts/run @@ -9,6 +9,7 @@ require __DIR__ . '/vendor/autoload.php'; use Cli\Commands\BackupCommand; use Cli\Commands\CommandError; +use Cli\Commands\InitCommand; use Minicli\App; // Get the directory of the CLI "entrypoint", adjusted to be the real @@ -28,6 +29,7 @@ $app = new App(); $app->setSignature('./run'); $app->registerCommand('backup', [new BackupCommand($bsDir), 'handle']); +$app->registerCommand('init', [new InitCommand(), 'handle']); try { $app->runCommand($argv);