namespace Cli\Commands;
-use Minicli\Command\CommandCall;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
-
-class InitCommand
+use Cli\Services\ComposerLocator;
+use Cli\Services\EnvironmentLoader;
+use Cli\Services\ProgramRunner;
+use Cli\Services\RequirementsValidator;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class InitCommand extends Command
{
+ protected function configure(): void
+ {
+ $this->setName('init');
+ $this->setDescription('Initialise a new BookStack install. Does not configure the webserver or database.');
+ $this->addArgument('target-directory', InputArgument::OPTIONAL, 'The directory to create the BookStack install within. Must be empty.', '');
+ }
+
/**
* @throws CommandError
*/
- public function handle(CommandCall $input)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
- $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)
- // 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 = '';
- }
+ $output->writeln("<info>Checking system requirements...</info>");
+ RequirementsValidator::validate();
+
+ $suggestedOutPath = $input->getArgument('target-directory');
- echo "Locating and checking install directory...\n";
+ $output->writeln("<info>Locating and checking install directory...</info>");
$installDir = $this->getInstallDir($suggestedOutPath);
- $this->ensureInstallDirEmpty($installDir);
+ $this->ensureInstallDirEmptyAndWritable($installDir);
- echo "Cloning down BookStack project to install directory...\n";
+ $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
$this->cloneBookStackViaGit($installDir);
- echo "Installing application dependencies using composer...\n";
- $this->installComposerDependencies($installDir);
+ $output->writeln("<info>Checking composer exists...</info>");
+ $composerLocator = new ComposerLocator($installDir);
+ $composer = $composerLocator->getProgram();
+ if (!$composer->isFound()) {
+ $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
+ $composerLocator->download();
+ }
+
+ $output->writeln("<info>Installing application dependencies using composer...</info>");
+ $this->installComposerDependencies($composer, $installDir);
- echo "Creating .env file from .env.example...\n";
+ $output->writeln("<info>Creating .env file from .env.example...</info>");
copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
sleep(1);
- echo "Generating app key...\n";
+ $output->writeln("<info>Generating app key...</info>");
$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');
-// }
+ $output->writeln("<info>A BookStack install has been initialized at: {$installDir}\n</info>");
+ $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>");
+ $output->writeln("<info>- Configure your webserver for use with BookStack.</info>");
+ $output->writeln("<info>- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.</info>");
+
+ return Command::SUCCESS;
}
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(EnvironmentLoader::load($installDir))
+ ->runCapturingAllOutput([
+ $installDir . DIRECTORY_SEPARATOR . 'artisan',
+ 'key:generate', '--force', '-n', '-q'
+ ]);
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
+ protected function installComposerDependencies(ProgramRunner $composer, 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 = $composer->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";
- }
- }
+ $git = (new ProgramRunner('git', '/usr/bin/git'))
+ ->withTimeout(240)
+ ->withIdleTimeout(15);
+
+ $errors = $git->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);
}
+
+ // Disable file permission tracking for git repo
+ $git->runCapturingStdErr([
+ '-C', $installDir,
+ 'config', 'core.fileMode', 'false'
+ ]);
}
/**
* Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
* @throws CommandError
*/
- protected function ensureInstallDirEmpty(string $installDir): void
+ protected function ensureInstallDirEmptyAndWritable(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.");
}
+
+ if (!is_writable($installDir)) {
+ throw new CommandError("Target install directory [{$installDir}] is not writable.");
+ }
}
/**
if (!$created) {
throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
}
- $dir = $suggestedDir;
+ $dir = realpath($suggestedDir);
} else {
throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
}