3 namespace Cli\Commands;
5 use Cli\Services\EnvironmentLoader;
6 use Cli\Services\ProgramRunner;
7 use Symfony\Component\Console\Command\Command;
8 use Symfony\Component\Console\Input\InputArgument;
9 use Symfony\Component\Console\Input\InputInterface;
10 use Symfony\Component\Console\Output\OutputInterface;
12 class InitCommand extends Command
14 protected function configure(): void
16 $this->setName('init');
17 $this->setDescription('Initialise a new BookStack install. Does not configure the webserver or database.');
18 $this->addArgument('target-directory', InputArgument::OPTIONAL, 'The directory to create the BookStack install within. Must be empty.', '');
22 * @throws CommandError
24 protected function execute(InputInterface $input, OutputInterface $output): int
26 $output->writeln("<info>Checking system requirements...</info>");
27 $this->ensureRequirementsMet();
29 $suggestedOutPath = $input->getArgument('target-directory');
31 $output->writeln("<info>Locating and checking install directory...</info>");
32 $installDir = $this->getInstallDir($suggestedOutPath);
33 $this->ensureInstallDirEmptyAndWritable($installDir);
35 $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
36 $this->cloneBookStackViaGit($installDir);
38 $output->writeln("<info>Checking composer exists...</info>");
39 $composer = $this->getComposerProgram($installDir);
41 $composer->ensureFound();
42 } catch (\Exception $exception) {
43 $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
44 $this->downloadComposerToInstall($installDir);
47 $output->writeln("<info>Installing application dependencies using composer...</info>");
48 $this->installComposerDependencies($composer, $installDir);
50 $output->writeln("<info>Creating .env file from .env.example...</info>");
51 copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
54 $output->writeln("<info>Generating app key...</info>");
55 $this->generateAppKey($installDir);
58 $output->writeln("<info>A BookStack install has been initialized at: {$installDir}\n</info>");
59 $output->writeln("<info>You will still need to:</info>");
60 $output->writeln("<info>- Update the .env file in the install with correct URL, database and email details.</info>");
61 $output->writeln("<info>- Run 'php artisan migrate' to set-up the database.</info>");
62 $output->writeln("<info>- Configure your webserver for use with BookStack.</info>");
63 $output->writeln("<info>- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.</info>");
65 return Command::SUCCESS;
69 * Ensure the required PHP extensions are installed for this command.
70 * @throws CommandError
72 protected function ensureRequirementsMet(): void
76 if (version_compare(PHP_VERSION, '8.0.2') < 0) {
77 $errors[] = "PHP >= 8.0.2 is required to install BookStack.";
80 $requiredExtensions = ['bcmath', 'curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml'];
81 foreach ($requiredExtensions as $extension) {
82 if (!extension_loaded($extension)) {
83 $errors[] = "The \"{$extension}\" PHP extension is required by not active.";
88 (new ProgramRunner('git', '/usr/bin/git'))->ensureFound();
89 (new ProgramRunner('php', '/usr/bin/php'))->ensureFound();
90 } catch (\Exception $exception) {
91 $errors[] = $exception->getMessage();
94 if (count($errors) > 0) {
95 throw new CommandError("Requirements failed with following errors:\n" . implode("\n", $errors));
99 protected function downloadComposerToInstall(string $installDir): void
101 $setupPath = $installDir . DIRECTORY_SEPARATOR . 'composer-setup.php';
102 $signature = file_get_contents('https://composer.github.io/installer.sig');
103 copy('https://getcomposer.org/installer', $setupPath);
104 $checksum = hash_file('sha384', $setupPath);
106 if ($signature !== $checksum) {
108 throw new CommandError("Could not install composer, checksum validation failed.");
111 $status = (new ProgramRunner('php', '/usr/bin/php'))
112 ->runWithoutOutputCallbacks([
113 $setupPath, '--quiet',
114 "--install-dir={$installDir}",
115 "--filename=composer",
121 throw new CommandError("Could not install composer, composer-setup script run failed.");
126 * Get the composer program.
128 protected function getComposerProgram(string $installDir): ProgramRunner
130 return (new ProgramRunner('composer', '/usr/local/bin/composer'))
132 ->withIdleTimeout(15)
133 ->withAdditionalPathLocation($installDir);
136 protected function generateAppKey(string $installDir): void
138 $errors = (new ProgramRunner('php', '/usr/bin/php'))
141 ->withEnvironment(EnvironmentLoader::load($installDir))
142 ->runCapturingAllOutput([
143 $installDir . DIRECTORY_SEPARATOR . 'artisan',
144 'key:generate', '--force', '-n', '-q'
148 throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
153 * Run composer install to download PHP dependencies.
154 * @throws CommandError
156 protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
158 $errors = $composer->runCapturingStdErr([
160 '--no-dev', '-n', '-q', '--no-progress',
165 throw new CommandError("Failed composer install with errors:\n" . $errors);
170 * Clone a new instance of BookStack to the given install folder.
171 * @throws CommandError
173 protected function cloneBookStackViaGit(string $installDir): void
175 $errors = (new ProgramRunner('git', '/usr/bin/git'))
177 ->withIdleTimeout(15)
178 ->runCapturingStdErr([
180 '--branch', 'release',
182 'https://github.com/BookStackApp/BookStack.git',
187 throw new CommandError("Failed git clone with errors:\n" . $errors);
192 * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
193 * @throws CommandError
195 protected function ensureInstallDirEmptyAndWritable(string $installDir): void
197 $contents = array_diff(scandir($installDir), ['..', '.']);
198 if (count($contents) > 0) {
199 throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
202 if (!is_writable($installDir)) {
203 throw new CommandError("Target install directory [{$installDir}] is not writable.");
208 * Build a full path to the intended location for the BookStack install.
209 * @throws CommandError
211 protected function getInstallDir(string $suggestedDir): string
216 if (is_file($suggestedDir)) {
217 throw new CommandError("Was provided [{$suggestedDir}] as an install path but existing file provided.");
218 } else if (is_dir($suggestedDir)) {
219 $dir = realpath($suggestedDir);
220 } else if (is_dir(dirname($suggestedDir))) {
221 $created = mkdir($suggestedDir);
223 throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
225 $dir = realpath($suggestedDir);
227 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");