]> BookStack Code Mirror - system-cli/blob - scripts/Commands/InitCommand.php
1c943a21c0928809db341f926795895a38b9a7bc
[system-cli] / scripts / Commands / InitCommand.php
1 <?php
2
3 namespace Cli\Commands;
4
5 use Minicli\Command\CommandCall;
6 use Symfony\Component\Process\ExecutableFinder;
7 use Symfony\Component\Process\Process;
8
9 class InitCommand
10 {
11     /**
12      * @throws CommandError
13      */
14     public function handle(CommandCall $input)
15     {
16         $this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
17
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?
25
26         $suggestedOutPath = $input->subcommand;
27         if ($suggestedOutPath === 'default') {
28             $suggestedOutPath = '';
29         }
30
31         echo "Locating and checking install directory...\n";
32         $installDir = $this->getInstallDir($suggestedOutPath);
33         $this->ensureInstallDirEmpty($installDir);
34
35         echo "Cloning down BookStack project to install directory...\n";
36         $this->cloneBookStackViaGit($installDir);
37
38         echo "Installing application dependencies using composer...\n";
39         $this->installComposerDependencies($installDir);
40
41         echo "Creating .env file from .env.example...\n";
42         copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
43         sleep(1);
44
45         echo "Generating app key...\n";
46         $this->generateAppKey($installDir);
47
48         // Announce end
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";
55     }
56
57     /**
58      * Ensure the required PHP extensions are installed for this command.
59      * @throws CommandError
60      */
61     protected function ensureRequiredExtensionInstalled(): void
62     {
63 //        if (!extension_loaded('zip')) {
64 //            throw new CommandError('The "zip" PHP extension is required to run this command');
65 //        }
66     }
67
68     protected function generateAppKey(string $installDir): void
69     {
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.');
75         }
76
77         $process = new Process([
78             $phpPath,
79             $installDir . DIRECTORY_SEPARATOR . 'artisan',
80             'key:generate', '--force', '-n', '-q'
81         ], null, ['APP_KEY' => 'SomeRandomString']);
82         $process->setTimeout(240);
83         $process->setIdleTimeout(5);
84         $process->start();
85
86         $errors = '';
87         foreach ($process as $type => $data) {
88             // Errors are on stdout for artisan
89             $errors .= $data . "\n";
90         }
91
92         if ($errors) {
93             throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
94         }
95     }
96
97     /**
98      * Run composer install to download PHP dependencies.
99      * @throws CommandError
100      */
101     protected function installComposerDependencies(string $installDir): void
102     {
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.');
108         }
109
110         $process = new Process([
111             $composerPath, 'install',
112             '--no-dev', '-n', '-q', '--no-progress',
113             '-d', $installDir
114         ]);
115         $process->setTimeout(240);
116         $process->setIdleTimeout(15);
117         $process->start();
118
119         $errors = '';
120         foreach ($process as $type => $data) {
121             if ($process::ERR === $type) {
122                 $errors .= $data . "\n";
123             }
124         }
125
126         if ($errors) {
127             throw new CommandError("Failed composer install with errors:\n" . $errors);
128         }
129     }
130
131     /**
132      * Clone a new instance of BookStack to the given install folder.
133      * @throws CommandError
134      */
135     protected function cloneBookStackViaGit(string $installDir): void
136     {
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.');
142         }
143
144         $process = new Process([
145             $gitPath, 'clone', '-q',
146             '--branch', 'release',
147             '--single-branch',
148             'https://github.com/BookStackApp/BookStack.git',
149             $installDir
150         ]);
151         $process->setTimeout(240);
152         $process->setIdleTimeout(15);
153         $process->start();
154
155         $errors = '';
156         foreach ($process as $type => $data) {
157             if ($process::ERR === $type) {
158                 $errors .= $data . "\n";
159             }
160         }
161
162         if ($errors) {
163             throw new CommandError("Failed git clone with errors:\n" . $errors);
164         }
165     }
166
167     /**
168      * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
169      * @throws CommandError
170      */
171     protected function ensureInstallDirEmpty(string $installDir): void
172     {
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.");
176         }
177     }
178
179     /**
180      * Build a full path to the intended location for the BookStack install.
181      * @throws CommandError
182      */
183     protected function getInstallDir(string $suggestedDir): string
184     {
185         $dir = getcwd();
186
187         if ($suggestedDir) {
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);
194                 if (!$created) {
195                     throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
196                 }
197                 $dir = $suggestedDir;
198             } else {
199                 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
200             }
201         }
202
203         return $dir;
204     }
205 }