]> BookStack Code Mirror - system-cli/blobdiff - scripts/Commands/InitCommand.php
Got restore command to a working state
[system-cli] / scripts / Commands / InitCommand.php
index 1c943a21c0928809db341f926795895a38b9a7bc..26ab54c9e54abbfa4634fe94bc0a274252fe5596 100644 (file)
@@ -2,92 +2,80 @@
 
 namespace Cli\Commands;
 
-use Minicli\Command\CommandCall;
-use Symfony\Component\Process\ExecutableFinder;
-use Symfony\Component\Process\Process;
-
-class InitCommand
+use Cli\Services\ComposerLocator;
+use Cli\Services\EnvironmentLoader;
+use Cli\Services\ProgramRunner;
+use Cli\Services\RequirementsValidator;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class InitCommand extends Command
 {
+    protected function configure(): void
+    {
+        $this->setName('init');
+        $this->setDescription('Initialise a new BookStack install. Does not configure the webserver or database.');
+        $this->addArgument('target-directory', InputArgument::OPTIONAL, 'The directory to create the BookStack install within. Must be empty.', '');
+    }
+
     /**
      * @throws CommandError
      */
-    public function handle(CommandCall $input)
+    protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        $this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
-
-        // TODO - Dedupe the command stuff going on.
-        // TODO - Check composer and git exists before running
-        // TODO - Look at better way of handling env usage, on demand maybe where needed?
-        //   Env loading in main `run` script if confilicting with certain bits here (app key generate, hence APP_KEY overload)
-        //   See dotenv's Dotenv::createArrayBacked as way to go this.
-        //   (More of a change for 'backup' command).
-        // TODO - Potentially download composer?
-
-        $suggestedOutPath = $input->subcommand;
-        if ($suggestedOutPath === 'default') {
-            $suggestedOutPath = '';
-        }
+        $output->writeln("<info>Checking system requirements...</info>");
+        RequirementsValidator::validate();
+
+        $suggestedOutPath = $input->getArgument('target-directory');
 
-        echo "Locating and checking install directory...\n";
+        $output->writeln("<info>Locating and checking install directory...</info>");
         $installDir = $this->getInstallDir($suggestedOutPath);
-        $this->ensureInstallDirEmpty($installDir);
+        $this->ensureInstallDirEmptyAndWritable($installDir);
 
-        echo "Cloning down BookStack project to install directory...\n";
+        $output->writeln("<info>Cloning down BookStack project to install directory...</info>");
         $this->cloneBookStackViaGit($installDir);
 
-        echo "Installing application dependencies using composer...\n";
-        $this->installComposerDependencies($installDir);
+        $output->writeln("<info>Checking composer exists...</info>");
+        $composerLocator = new ComposerLocator($installDir);
+        $composer = $composerLocator->getProgram();
+        if (!$composer->isFound()) {
+            $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
+            $composerLocator->download();
+        }
+
+        $output->writeln("<info>Installing application dependencies using composer...</info>");
+        $this->installComposerDependencies($composer, $installDir);
 
-        echo "Creating .env file from .env.example...\n";
+        $output->writeln("<info>Creating .env file from .env.example...</info>");
         copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
         sleep(1);
 
-        echo "Generating app key...\n";
+        $output->writeln("<info>Generating app key...</info>");
         $this->generateAppKey($installDir);
 
         // Announce end
-        echo "A BookStack install has been initialized at: {$installDir}\n\n";
-        echo "You will still need to:\n";
-        echo "- Update the .env file in the install with correct URL, database and email details.\n";
-        echo "- Run 'php artisan migrate' to set-up the database.\n";
-        echo "- Configure your webserver for use with BookStack.\n";
-        echo "- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.\n";
-    }
-
-    /**
-     * Ensure the required PHP extensions are installed for this command.
-     * @throws CommandError
-     */
-    protected function ensureRequiredExtensionInstalled(): void
-    {
-//        if (!extension_loaded('zip')) {
-//            throw new CommandError('The "zip" PHP extension is required to run this command');
-//        }
+        $output->writeln("<info>A BookStack install has been initialized at: {$installDir}\n</info>");
+        $output->writeln("<info>You will still need to:</info>");
+        $output->writeln("<info>- Update the .env file in the install with correct URL, database and email details.</info>");
+        $output->writeln("<info>- Run 'php artisan migrate' to set-up the database.</info>");
+        $output->writeln("<info>- Configure your webserver for use with BookStack.</info>");
+        $output->writeln("<info>- Ensure the required directories (storage/ bootstrap/cache public/uploads) are web-server writable.</info>");
+
+        return Command::SUCCESS;
     }
 
     protected function generateAppKey(string $installDir): void
     {
-        // Find reference to php
-        $executableFinder = new ExecutableFinder();
-        $phpPath = $executableFinder->find('php', '/usr/bin/php');
-        if (!is_file($phpPath)) {
-            throw new CommandError('Could not locate "php" program.');
-        }
-
-        $process = new Process([
-            $phpPath,
-            $installDir . DIRECTORY_SEPARATOR . 'artisan',
-            'key:generate', '--force', '-n', '-q'
-        ], null, ['APP_KEY' => 'SomeRandomString']);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(5);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            // Errors are on stdout for artisan
-            $errors .= $data . "\n";
-        }
+        $errors = (new ProgramRunner('php', '/usr/bin/php'))
+            ->withTimeout(60)
+            ->withIdleTimeout(5)
+            ->withEnvironment(EnvironmentLoader::load($installDir))
+            ->runCapturingAllOutput([
+                $installDir . DIRECTORY_SEPARATOR . 'artisan',
+                'key:generate', '--force', '-n', '-q'
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed 'php artisan key:generate' with errors:\n" . $errors);
@@ -98,30 +86,13 @@ class InitCommand
      * Run composer install to download PHP dependencies.
      * @throws CommandError
      */
-    protected function installComposerDependencies(string $installDir): void
+    protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
     {
-        // Find reference to composer
-        $executableFinder = new ExecutableFinder();
-        $composerPath = $executableFinder->find('composer', '/usr/local/bin/composer');
-        if (!is_file($composerPath)) {
-            throw new CommandError('Could not locate "composer" program.');
-        }
-
-        $process = new Process([
-            $composerPath, 'install',
-            '--no-dev', '-n', '-q', '--no-progress',
-            '-d', $installDir
-        ]);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(15);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            if ($process::ERR === $type) {
-                $errors .= $data . "\n";
-            }
-        }
+        $errors = $composer->runCapturingStdErr([
+                'install',
+                '--no-dev', '-n', '-q', '--no-progress',
+                '-d', $installDir
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed composer install with errors:\n" . $errors);
@@ -134,46 +105,43 @@ class InitCommand
      */
     protected function cloneBookStackViaGit(string $installDir): void
     {
-        // Find reference to git
-        $executableFinder = new ExecutableFinder();
-        $gitPath = $executableFinder->find('git', '/usr/bin/bit');
-        if (!is_file($gitPath)) {
-            throw new CommandError('Could not locate "git" program.');
-        }
-
-        $process = new Process([
-            $gitPath, 'clone', '-q',
-            '--branch', 'release',
-            '--single-branch',
-            'https://github.com/BookStackApp/BookStack.git',
-            $installDir
-        ]);
-        $process->setTimeout(240);
-        $process->setIdleTimeout(15);
-        $process->start();
-
-        $errors = '';
-        foreach ($process as $type => $data) {
-            if ($process::ERR === $type) {
-                $errors .= $data . "\n";
-            }
-        }
+        $git = (new ProgramRunner('git', '/usr/bin/git'))
+            ->withTimeout(240)
+            ->withIdleTimeout(15);
+
+        $errors = $git->runCapturingStdErr([
+                'clone', '-q',
+                '--branch', 'release',
+                '--single-branch',
+                'https://github.com/BookStackApp/BookStack.git',
+                $installDir
+            ]);
 
         if ($errors) {
             throw new CommandError("Failed git clone with errors:\n" . $errors);
         }
+
+        // Disable file permission tracking for git repo
+        $git->runCapturingStdErr([
+            '-C', $installDir,
+            'config', 'core.fileMode', 'false'
+        ]);
     }
 
     /**
      * Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
      * @throws CommandError
      */
-    protected function ensureInstallDirEmpty(string $installDir): void
+    protected function ensureInstallDirEmptyAndWritable(string $installDir): void
     {
         $contents = array_diff(scandir($installDir), ['..', '.']);
         if (count($contents) > 0) {
             throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
         }
+
+        if (!is_writable($installDir)) {
+            throw new CommandError("Target install directory [{$installDir}] is not writable.");
+        }
     }
 
     /**
@@ -194,7 +162,7 @@ class InitCommand
                 if (!$created) {
                     throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
                 }
-                $dir = $suggestedDir;
+                $dir = realpath($suggestedDir);
             } else {
                 throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
             }