3 namespace Cli\Commands;
5 use Minicli\Command\CommandCall;
6 use Symfony\Component\Process\ExecutableFinder;
7 use Symfony\Component\Process\Process;
12 * @throws CommandError
14 public function handle(CommandCall $input)
16 $this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
18 // TODO - Dedupe the command stuff going on.
19 // TODO - Check composer and git exists before running
20 // TODO - Look at better way of handling env usage, on demand maybe where needed?
21 // Env loading in main `run` script if confilicting with certain bits here (app key generate, hence APP_KEY overload)
22 // See dotenv's Dotenv::createArrayBacked as way to go this.
23 // (More of a change for 'backup' command).
24 // TODO - Potentially download composer?
26 $suggestedOutPath = $input->subcommand;
27 if ($suggestedOutPath === 'default') {
28 $suggestedOutPath = '';
31 echo "Locating and checking install directory...\n";
32 $installDir = $this->getInstallDir($suggestedOutPath);
33 $this->ensureInstallDirEmpty($installDir);
35 echo "Cloning down BookStack project to install directory...\n";
36 $this->cloneBookStackViaGit($installDir);
38 echo "Installing application dependencies using composer...\n";
39 $this->installComposerDependencies($installDir);
41 echo "Creating .env file from .env.example...\n";
42 copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
45 echo "Generating app key...\n";
46 $this->generateAppKey($installDir);
49 echo "A BookStack install has been initialized at: {$installDir}\n\n";
50 echo "You will still need to:\n";
51 echo "- Update the .env file in the install with correct URL, database and email details.\n";
52 echo "- Run 'php artisan migrate' to set-up the database.\n";
53 echo "- Configure your webserver for use with BookStack.\n";
54 echo "- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.\n";
58 * Ensure the required PHP extensions are installed for this command.
59 * @throws CommandError
61 protected function ensureRequiredExtensionInstalled(): void
63 // if (!extension_loaded('zip')) {
64 // throw new CommandError('The "zip" PHP extension is required to run this command');
68 protected function generateAppKey(string $installDir): void
70 // Find reference to php
71 $executableFinder = new ExecutableFinder();
72 $phpPath = $executableFinder->find('php', '/usr/bin/php');
73 if (!is_file($phpPath)) {
74 throw new CommandError('Could not locate "php" program.');
77 $process = new Process([
79 $installDir . DIRECTORY_SEPARATOR . 'artisan',
80 'key:generate', '--force', '-n', '-q'
81 ], null, ['APP_KEY' => 'SomeRandomString']);
82 $process->setTimeout(240);
83 $process->setIdleTimeout(5);
87 foreach ($process as $type => $data) {
88 // Errors are on stdout for artisan
89 $errors .= $data . "\n";
93 throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
98 * Run composer install to download PHP dependencies.
99 * @throws CommandError
101 protected function installComposerDependencies(string $installDir): void
103 // Find reference to composer
104 $executableFinder = new ExecutableFinder();
105 $composerPath = $executableFinder->find('composer', '/usr/local/bin/composer');
106 if (!is_file($composerPath)) {
107 throw new CommandError('Could not locate "composer" program.');
110 $process = new Process([
111 $composerPath, 'install',
112 '--no-dev', '-n', '-q', '--no-progress',
115 $process->setTimeout(240);
116 $process->setIdleTimeout(15);
120 foreach ($process as $type => $data) {
121 if ($process::ERR === $type) {
122 $errors .= $data . "\n";
127 throw new CommandError("Failed composer install with errors:\n" . $errors);
132 * Clone a new instance of BookStack to the given install folder.
133 * @throws CommandError
135 protected function cloneBookStackViaGit(string $installDir): void
137 // Find reference to git
138 $executableFinder = new ExecutableFinder();
139 $gitPath = $executableFinder->find('git', '/usr/bin/bit');
140 if (!is_file($gitPath)) {
141 throw new CommandError('Could not locate "git" program.');
144 $process = new Process([
145 $gitPath, 'clone', '-q',
146 '--branch', 'release',
148 'https://github.com/BookStackApp/BookStack.git',
151 $process->setTimeout(240);
152 $process->setIdleTimeout(15);
156 foreach ($process as $type => $data) {
157 if ($process::ERR === $type) {
158 $errors .= $data . "\n";
163 throw new CommandError("Failed git clone with errors:\n" . $errors);
168 * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
169 * @throws CommandError
171 protected function ensureInstallDirEmpty(string $installDir): void
173 $contents = array_diff(scandir($installDir), ['..', '.']);
174 if (count($contents) > 0) {
175 throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
180 * Build a full path to the intended location for the BookStack install.
181 * @throws CommandError
183 protected function getInstallDir(string $suggestedDir): string
188 if (is_file($suggestedDir)) {
189 throw new CommandError("Was provided [{$suggestedDir}] as an install path but existing file provided.");
190 } else if (is_dir($suggestedDir)) {
191 $dir = realpath($suggestedDir);
192 } else if (is_dir(dirname($suggestedDir))) {
193 $created = mkdir($suggestedDir);
195 throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
197 $dir = $suggestedDir;
199 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");