]> BookStack Code Mirror - system-cli/blob - scripts/Commands/InitCommand.php
Added update command
[system-cli] / scripts / Commands / InitCommand.php
1 <?php
2
3 namespace Cli\Commands;
4
5 use Cli\Services\ComposerLocator;
6 use Cli\Services\EnvironmentLoader;
7 use Cli\Services\ProgramRunner;
8 use Cli\Services\RequirementsValidator;
9 use Symfony\Component\Console\Command\Command;
10 use Symfony\Component\Console\Input\InputArgument;
11 use Symfony\Component\Console\Input\InputInterface;
12 use Symfony\Component\Console\Output\OutputInterface;
13
14 class InitCommand extends Command
15 {
16     protected function configure(): void
17     {
18         $this->setName('init');
19         $this->setDescription('Initialise a new BookStack install. Does not configure the webserver or database.');
20         $this->addArgument('target-directory', InputArgument::OPTIONAL, 'The directory to create the BookStack install within. Must be empty.', '');
21     }
22
23     /**
24      * @throws CommandError
25      */
26     protected function execute(InputInterface $input, OutputInterface $output): int
27     {
28         $output->writeln("<info>Checking system requirements...</info>");
29         RequirementsValidator::validate();
30
31         $suggestedOutPath = $input->getArgument('target-directory');
32
33         $output->writeln("<info>Locating and checking install directory...</info>");
34         $installDir = $this->getInstallDir($suggestedOutPath);
35         $this->ensureInstallDirEmptyAndWritable($installDir);
36
37         $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
38         $this->cloneBookStackViaGit($installDir);
39
40         $output->writeln("<info>Checking composer exists...</info>");
41         $composerLocator = new ComposerLocator($installDir);
42         $composer = $composerLocator->getProgram();
43         if (!$composer->isFound()) {
44             $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
45             $composerLocator->download();
46         }
47
48         $output->writeln("<info>Installing application dependencies using composer...</info>");
49         $this->installComposerDependencies($composer, $installDir);
50
51         $output->writeln("<info>Creating .env file from .env.example...</info>");
52         copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
53         sleep(1);
54
55         $output->writeln("<info>Generating app key...</info>");
56         $this->generateAppKey($installDir);
57
58         // Announce end
59         $output->writeln("<info>A BookStack install has been initialized at: {$installDir}\n</info>");
60         $output->writeln("<info>You will still need to:</info>");
61         $output->writeln("<info>- Update the .env file in the install with correct URL, database and email details.</info>");
62         $output->writeln("<info>- Run 'php artisan migrate' to set-up the database.</info>");
63         $output->writeln("<info>- Configure your webserver for use with BookStack.</info>");
64         $output->writeln("<info>- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.</info>");
65
66         return Command::SUCCESS;
67     }
68
69     protected function generateAppKey(string $installDir): void
70     {
71         $errors = (new ProgramRunner('php', '/usr/bin/php'))
72             ->withTimeout(60)
73             ->withIdleTimeout(5)
74             ->withEnvironment(EnvironmentLoader::load($installDir))
75             ->runCapturingAllOutput([
76                 $installDir . DIRECTORY_SEPARATOR . 'artisan',
77                 'key:generate', '--force', '-n', '-q'
78             ]);
79
80         if ($errors) {
81             throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
82         }
83     }
84
85     /**
86      * Run composer install to download PHP dependencies.
87      * @throws CommandError
88      */
89     protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
90     {
91         $errors = $composer->runCapturingStdErr([
92                 'install',
93                 '--no-dev', '-n', '-q', '--no-progress',
94                 '-d', $installDir
95             ]);
96
97         if ($errors) {
98             throw new CommandError("Failed composer install with errors:\n" . $errors);
99         }
100     }
101
102     /**
103      * Clone a new instance of BookStack to the given install folder.
104      * @throws CommandError
105      */
106     protected function cloneBookStackViaGit(string $installDir): void
107     {
108         $errors = (new ProgramRunner('git', '/usr/bin/git'))
109             ->withTimeout(240)
110             ->withIdleTimeout(15)
111             ->runCapturingStdErr([
112                 'clone', '-q',
113                 '--branch', 'release',
114                 '--single-branch',
115                 'https://github.com/BookStackApp/BookStack.git',
116                 $installDir
117             ]);
118
119         if ($errors) {
120             throw new CommandError("Failed git clone with errors:\n" . $errors);
121         }
122     }
123
124     /**
125      * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
126      * @throws CommandError
127      */
128     protected function ensureInstallDirEmptyAndWritable(string $installDir): void
129     {
130         $contents = array_diff(scandir($installDir), ['..', '.']);
131         if (count($contents) > 0) {
132             throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
133         }
134
135         if (!is_writable($installDir)) {
136             throw new CommandError("Target install directory [{$installDir}] is not writable.");
137         }
138     }
139
140     /**
141      * Build a full path to the intended location for the BookStack install.
142      * @throws CommandError
143      */
144     protected function getInstallDir(string $suggestedDir): string
145     {
146         $dir = getcwd();
147
148         if ($suggestedDir) {
149             if (is_file($suggestedDir)) {
150                 throw new CommandError("Was provided [{$suggestedDir}] as an install path but existing file provided.");
151             } else if (is_dir($suggestedDir)) {
152                 $dir = realpath($suggestedDir);
153             } else if (is_dir(dirname($suggestedDir))) {
154                 $created = mkdir($suggestedDir);
155                 if (!$created) {
156                     throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
157                 }
158                 $dir = realpath($suggestedDir);
159             } else {
160                 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
161             }
162         }
163
164         return $dir;
165     }
166 }