1 <?php declare(strict_types=1);
3 namespace Cli\Commands;
5 use Cli\Services\ComposerLocator;
6 use Cli\Services\EnvironmentLoader;
7 use Cli\Services\Paths;
8 use Cli\Services\ProgramRunner;
9 use Cli\Services\RequirementsValidator;
10 use Symfony\Component\Console\Command\Command;
11 use Symfony\Component\Console\Input\InputArgument;
12 use Symfony\Component\Console\Input\InputInterface;
13 use Symfony\Component\Console\Output\OutputInterface;
15 class InitCommand extends Command
17 protected function configure(): void
19 $this->setName('init');
20 $this->setDescription('Initialise a new BookStack install. Does not configure the webserver or database.');
21 $this->addArgument('target-directory', InputArgument::OPTIONAL, 'The directory to create the BookStack install within. Must be empty.', '');
25 * @throws CommandError
27 protected function execute(InputInterface $input, OutputInterface $output): int
29 $output->writeln("<info>Checking system requirements...</info>");
30 RequirementsValidator::validate();
32 $suggestedOutPath = $input->getArgument('target-directory');
34 $output->writeln("<info>Locating and checking install directory...</info>");
35 $installDir = $this->getInstallDir($suggestedOutPath);
36 $this->ensureInstallDirEmptyAndWritable($installDir);
38 $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
39 $this->cloneBookStackViaGit($installDir);
41 $output->writeln("<info>Checking composer exists...</info>");
42 $composerLocator = new ComposerLocator($installDir);
43 $composer = $composerLocator->getProgram();
44 if (!$composer->isFound()) {
45 $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
46 $composerLocator->download();
49 $output->writeln("<info>Installing application dependencies using composer...</info>");
50 $this->installComposerDependencies($composer, $installDir);
52 $output->writeln("<info>Creating .env file from .env.example...</info>");
53 copy(Paths::join($installDir, '.env.example'), Paths::join($installDir, '.env'));
56 $output->writeln("<info>Generating app key...</info>");
57 $this->generateAppKey($installDir);
60 $output->writeln("<success>A BookStack install has been initialized at: {$installDir}\n</success>");
61 $output->writeln("<info>You will still need to:</info>");
62 $output->writeln("<info>- Update the .env file in the install with correct URL, database and email details.</info>");
63 $output->writeln("<info>- Run 'php artisan migrate' to set-up the database.</info>");
64 $output->writeln("<info>- Configure your webserver for use with BookStack.</info>");
65 $output->writeln("<info>- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.</info>");
67 return Command::SUCCESS;
70 protected function generateAppKey(string $installDir): void
72 $errors = (new ProgramRunner('php', '/usr/bin/php'))
75 ->withEnvironment(EnvironmentLoader::load($installDir))
76 ->runCapturingAllOutput([
77 Paths::join($installDir, 'artisan'),
78 'key:generate', '--force', '-n', '-q'
82 throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
87 * Run composer install to download PHP dependencies.
88 * @throws CommandError
90 protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
92 $errors = $composer->runCapturingStdErr([
94 '--no-dev', '-n', '-q', '--no-progress',
99 throw new CommandError("Failed composer install with errors:\n" . $errors);
104 * Clone a new instance of BookStack to the given install folder.
105 * @throws CommandError
107 protected function cloneBookStackViaGit(string $installDir): void
109 $git = (new ProgramRunner('git', '/usr/bin/git'))
111 ->withIdleTimeout(300);
113 $errors = $git->runCapturingStdErr([
115 '--branch', 'release',
117 'https://github.com/BookStackApp/BookStack.git',
122 throw new CommandError("Failed git clone with errors:\n" . $errors);
125 // Disable file permission tracking for git repo
126 $git->runCapturingStdErr([
128 'config', 'core.fileMode', 'false'
133 * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
134 * @throws CommandError
136 protected function ensureInstallDirEmptyAndWritable(string $installDir): void
138 $contents = array_diff(scandir($installDir), ['..', '.']);
139 if (count($contents) > 0) {
140 throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
143 if (!is_writable($installDir)) {
144 throw new CommandError("Target install directory [{$installDir}] is not writable.");
149 * Build a full path to the intended location for the BookStack install.
150 * @throws CommandError
152 protected function getInstallDir(string $suggestedDir): string
154 $dir = Paths::resolve($suggestedDir);
157 throw new CommandError("Was provided [{$dir}] as an install path but existing file provided.");
158 } else if (is_dir($dir) && realpath($dir)) {
159 $dir = realpath($dir);
160 } else if (is_dir(dirname($dir))) {
161 $created = mkdir($dir);
163 throw new CommandError("Could not create directory [{$dir}] for install.");
165 $dir = realpath($dir);
167 throw new CommandError("Could not resolve provided [{$dir}] path to an existing folder.");