3 namespace Cli\Commands;
5 use Cli\Services\EnvironmentLoader;
6 use Cli\Services\ProgramRunner;
7 use Minicli\Command\CommandCall;
12 * @throws CommandError
14 public function handle(CommandCall $input)
16 echo "Checking system requirements...\n";
17 $this->ensureRequirementsMet();
19 $suggestedOutPath = $input->subcommand;
20 if ($suggestedOutPath === 'default') {
21 $suggestedOutPath = '';
24 echo "Locating and checking install directory...\n";
25 $installDir = $this->getInstallDir($suggestedOutPath);
26 $this->ensureInstallDirEmptyAndWritable($installDir);
28 echo "Cloning down BookStack project to install directory...\n";
29 $this->cloneBookStackViaGit($installDir);
31 echo "Checking composer exists...\n";
32 $composer = $this->getComposerProgram($installDir);
34 $composer->ensureFound();
35 } catch (\Exception $exception) {
36 echo "Composer does not exist, downloading a local copy...\n";
37 $this->downloadComposerToInstall($installDir);
40 echo "Installing application dependencies using composer...\n";
41 $this->installComposerDependencies($composer, $installDir);
43 echo "Creating .env file from .env.example...\n";
44 copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
47 echo "Generating app key...\n";
48 $this->generateAppKey($installDir);
51 echo "A BookStack install has been initialized at: {$installDir}\n\n";
52 echo "You will still need to:\n";
53 echo "- Update the .env file in the install with correct URL, database and email details.\n";
54 echo "- Run 'php artisan migrate' to set-up the database.\n";
55 echo "- Configure your webserver for use with BookStack.\n";
56 echo "- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.\n";
60 * Ensure the required PHP extensions are installed for this command.
61 * @throws CommandError
63 protected function ensureRequirementsMet(): void
67 if (version_compare(PHP_VERSION, '8.0.2') < 0) {
68 $errors[] = "PHP >= 8.0.2 is required to install BookStack.";
71 $requiredExtensions = ['bcmath', 'curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml'];
72 foreach ($requiredExtensions as $extension) {
73 if (!extension_loaded($extension)) {
74 $errors[] = "The \"{$extension}\" PHP extension is required by not active.";
79 (new ProgramRunner('git', '/usr/bin/git'))->ensureFound();
80 (new ProgramRunner('php', '/usr/bin/php'))->ensureFound();
81 } catch (\Exception $exception) {
82 $errors[] = $exception->getMessage();
85 if (count($errors) > 0) {
86 throw new CommandError("Requirements failed with following errors:\n" . implode("\n", $errors));
90 protected function downloadComposerToInstall(string $installDir): void
92 $setupPath = $installDir . DIRECTORY_SEPARATOR . 'composer-setup.php';
93 $signature = file_get_contents('https://composer.github.io/installer.sig');
94 copy('https://getcomposer.org/installer', $setupPath);
95 $checksum = hash_file('sha384', $setupPath);
97 if ($signature !== $checksum) {
99 throw new CommandError("Could not install composer, checksum validation failed.");
102 $status = (new ProgramRunner('php', '/usr/bin/php'))
103 ->runWithoutOutputCallbacks([
104 $setupPath, '--quiet',
105 "--install-dir={$installDir}",
106 "--filename=composer",
112 throw new CommandError("Could not install composer, composer-setup script run failed.");
117 * Get the composer program.
119 protected function getComposerProgram(string $installDir): ProgramRunner
121 return (new ProgramRunner('composer', '/usr/local/bin/composer'))
123 ->withIdleTimeout(15)
124 ->withAdditionalPathLocation($installDir);
127 protected function generateAppKey(string $installDir): void
129 $errors = (new ProgramRunner('php', '/usr/bin/php'))
132 ->withEnvironment(EnvironmentLoader::load($installDir))
133 ->runCapturingAllOutput([
134 $installDir . DIRECTORY_SEPARATOR . 'artisan',
135 'key:generate', '--force', '-n', '-q'
139 throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
144 * Run composer install to download PHP dependencies.
145 * @throws CommandError
147 protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
149 $errors = $composer->runCapturingStdErr([
151 '--no-dev', '-n', '-q', '--no-progress',
156 throw new CommandError("Failed composer install with errors:\n" . $errors);
161 * Clone a new instance of BookStack to the given install folder.
162 * @throws CommandError
164 protected function cloneBookStackViaGit(string $installDir): void
166 $errors = (new ProgramRunner('git', '/usr/bin/git'))
168 ->withIdleTimeout(15)
169 ->runCapturingStdErr([
171 '--branch', 'release',
173 'https://github.com/BookStackApp/BookStack.git',
178 throw new CommandError("Failed git clone with errors:\n" . $errors);
183 * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
184 * @throws CommandError
186 protected function ensureInstallDirEmptyAndWritable(string $installDir): void
188 $contents = array_diff(scandir($installDir), ['..', '.']);
189 if (count($contents) > 0) {
190 throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
193 if (!is_writable($installDir)) {
194 throw new CommandError("Target install directory [{$installDir}] is not writable.");
199 * Build a full path to the intended location for the BookStack install.
200 * @throws CommandError
202 protected function getInstallDir(string $suggestedDir): string
207 if (is_file($suggestedDir)) {
208 throw new CommandError("Was provided [{$suggestedDir}] as an install path but existing file provided.");
209 } else if (is_dir($suggestedDir)) {
210 $dir = realpath($suggestedDir);
211 } else if (is_dir(dirname($suggestedDir))) {
212 $created = mkdir($suggestedDir);
214 throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
216 $dir = realpath($suggestedDir);
218 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");