]> BookStack Code Mirror - system-cli/blob - src/Commands/InitCommand.php
Added general warning on all app usage about alpha status
[system-cli] / src / Commands / InitCommand.php
1 <?php declare(strict_types=1);
2
3 namespace Cli\Commands;
4
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;
14
15 class InitCommand extends Command
16 {
17     protected function configure(): void
18     {
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.', '');
22     }
23
24     /**
25      * @throws CommandError
26      */
27     protected function execute(InputInterface $input, OutputInterface $output): int
28     {
29         $output->writeln("<info>Checking system requirements...</info>");
30         RequirementsValidator::validate();
31
32         $suggestedOutPath = $input->getArgument('target-directory');
33
34         $output->writeln("<info>Locating and checking install directory...</info>");
35         $installDir = $this->getInstallDir($suggestedOutPath);
36         $this->ensureInstallDirEmptyAndWritable($installDir);
37
38         $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
39         $this->cloneBookStackViaGit($installDir);
40
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();
47         }
48
49         $output->writeln("<info>Installing application dependencies using composer...</info>");
50         $this->installComposerDependencies($composer, $installDir);
51
52         $output->writeln("<info>Creating .env file from .env.example...</info>");
53         copy(Paths::join($installDir, '.env.example'), Paths::join($installDir, '.env'));
54         sleep(1);
55
56         $output->writeln("<info>Generating app key...</info>");
57         $this->generateAppKey($installDir);
58
59         // Announce end
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>");
66
67         return Command::SUCCESS;
68     }
69
70     protected function generateAppKey(string $installDir): void
71     {
72         $errors = (new ProgramRunner('php', '/usr/bin/php'))
73             ->withTimeout(60)
74             ->withIdleTimeout(15)
75             ->withEnvironment(EnvironmentLoader::load($installDir))
76             ->runCapturingAllOutput([
77                 Paths::join($installDir, 'artisan'),
78                 'key:generate', '--force', '-n', '-q'
79             ]);
80
81         if ($errors) {
82             throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
83         }
84     }
85
86     /**
87      * Run composer install to download PHP dependencies.
88      * @throws CommandError
89      */
90     protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
91     {
92         $errors = $composer->runCapturingStdErr([
93                 'install',
94                 '--no-dev', '-n', '-q', '--no-progress',
95                 '-d', $installDir
96             ]);
97
98         if ($errors) {
99             throw new CommandError("Failed composer install with errors:\n" . $errors);
100         }
101     }
102
103     /**
104      * Clone a new instance of BookStack to the given install folder.
105      * @throws CommandError
106      */
107     protected function cloneBookStackViaGit(string $installDir): void
108     {
109         $git = (new ProgramRunner('git', '/usr/bin/git'))
110             ->withTimeout(300)
111             ->withIdleTimeout(300);
112
113         $errors = $git->runCapturingStdErr([
114                 'clone', '-q',
115                 '--branch', 'release',
116                 '--single-branch',
117                 'https://github.com/BookStackApp/BookStack.git',
118                 $installDir
119             ]);
120
121         if ($errors) {
122             throw new CommandError("Failed git clone with errors:\n" . $errors);
123         }
124
125         // Disable file permission tracking for git repo
126         $git->runCapturingStdErr([
127             '-C', $installDir,
128             'config', 'core.fileMode', 'false'
129         ]);
130     }
131
132     /**
133      * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
134      * @throws CommandError
135      */
136     protected function ensureInstallDirEmptyAndWritable(string $installDir): void
137     {
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.");
141         }
142
143         if (!is_writable($installDir)) {
144             throw new CommandError("Target install directory [{$installDir}] is not writable.");
145         }
146     }
147
148     /**
149      * Build a full path to the intended location for the BookStack install.
150      * @throws CommandError
151      */
152     protected function getInstallDir(string $suggestedDir): string
153     {
154         $dir = Paths::resolve($suggestedDir);
155
156         if (is_file($dir)) {
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);
162             if (!$created) {
163                 throw new CommandError("Could not create directory [{$dir}] for install.");
164             }
165             $dir = realpath($dir);
166         } else {
167             throw new CommandError("Could not resolve provided [{$dir}] path to an existing folder.");
168         }
169
170         return $dir;
171     }
172 }