tapestry-cloud/tapestry

View on GitHub
src/Console/Commands/SelfUpdateCommand.php

Summary

Maintainability
A
2 hrs
Test Coverage
F
19%
<?php

namespace Tapestry\Console\Commands;

use ZipArchive;
use Tapestry\Tapestry;
use Composer\Semver\Comparator;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Console\Input\InputOption;

class SelfUpdateCommand extends Command
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var Finder
     */
    private $finder;

    /**
     * @var string
     */
    private $releaseApiUrl = 'https://api.github.com/repos/carbontwelve/tapestry/releases/latest';

    private $currentPharFileName;

    private $scratchDirectoryPath;

    private $pharExists = false;

    private $canExecute = true;

    /**
     * Command only enabled if running within a Phar.
     *
     * @return bool
     */
    public function isEnabled()
    {
        return strlen(\Phar::running() > 0);
    }

    /**
     * InitCommand constructor.
     *
     * @param Filesystem $filesystem
     * @param Finder     $finder
     */
    public function __construct(Filesystem $filesystem, Finder $finder)
    {
        parent::__construct();
        if (! isset($_SERVER['argv'])) {
            $this->canExecute = false;

            return;
        }
        $this->filesystem = $filesystem;
        $this->finder = $finder;
        $this->currentPharFileName = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];
        $this->scratchDirectoryPath = dirname($this->currentPharFileName).DIRECTORY_SEPARATOR.'tmp';
        $this->pharExists = (pathinfo($this->currentPharFileName, PATHINFO_EXTENSION) === 'phar');
    }

    /**
     * @return void
     */
    protected function configure()
    {
        $this->setName('self-update')
            ->setDescription('Update your installed version of Tapestry.')
            ->setDefinition([
                new InputOption('test', 't', InputOption::VALUE_NONE, 'Test functionality outside of phar'),
                new InputOption('force', 'f', InputOption::VALUE_NONE, 'Force update even if you have the latest version'),
                new InputOption('rollback', 'r', InputOption::VALUE_NONE, 'Revert to an older installation of tapestry'),
                new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of tapestry the only backup available after the update'),
            ]);
    }

    protected function fire()
    {
        if (! $this->canExecute) {
            return 0;
        }

        if (! $this->input->getOption('test') && $this->pharExists === false) {
            $this->output->writeln('[!] Self-Update only works on phar archives.');

            return 1;
        }

        if ($this->input->getOption('rollback')) {
            return $this->rollback();
        }

        if (! $this->filesystem->exists($this->scratchDirectoryPath)) {
            $this->filesystem->mkdir($this->scratchDirectoryPath);
        }

        $jsonPathName = $this->scratchDirectoryPath.DIRECTORY_SEPARATOR.'release.json';
        if (! $this->downloadFile($this->releaseApiUrl, $jsonPathName, ['Accept' => 'application/vnd.github.v3+json'])) {
            $this->panic('There was a problem in downloading the update, please try again.');
        }

        $releaseJson = json_decode(file_get_contents($jsonPathName));
        $latestVersion = $releaseJson->tag_name;

        if ($this->input->getOption('force') === false && Comparator::greaterThanOrEqualTo(Tapestry::VERSION, $latestVersion)) {
            $this->output->writeln('You already have the latest version of Tapestry ['.Tapestry::VERSION.']. Doing nothing and exiting.');

            return 0;
        }

        $this->backupPhar();
        $this->replacePhar($releaseJson->assets[0]->browser_download_url);

        return 0;
    }

    private function rollback()
    {
        $binPath = pathinfo($this->currentPharFileName, PATHINFO_DIRNAME);
        $pharPath = $binPath.DIRECTORY_SEPARATOR.'tapestry.phar';
        $pharBackupPath = $binPath.DIRECTORY_SEPARATOR.'tapestry-temp.phar';

        if (! $this->filesystem->exists($pharPath)) {
            $this->error('tapestry.phar could not be found at ['.$pharPath.']. Doing nothing and exiting.');

            return 0;
        }

        if (! $this->filesystem->exists($pharBackupPath)) {
            $this->error('No previous version could be found at ['.$pharBackupPath.']. Doing nothing and exiting.');

            return 0;
        }

        $this->filesystem->remove($pharPath);
        $this->filesystem->rename($pharBackupPath, $pharPath);

        return 0;
    }

    private function backupPhar()
    {
        if ($this->input->getOption('test') === true && $this->pharExists === false) {
            $this->output->writeln('[*] Pretending to Backup Phar');

            return;
        } elseif ($this->input->getOption('test') === false && $this->pharExists === false) {
            $this->panic('Phar Archive Not Found!');
        }

        $this->output->writeln('[*] Making Backup Phar');
        $tempFilename = dirname($this->currentPharFileName).DIRECTORY_SEPARATOR.basename($this->currentPharFileName, '.phar').'-temp.phar';
        $this->filesystem->copy($this->currentPharFileName, $tempFilename);
    }

    private function replacePhar($latestVersionDownloadUrl)
    {
        $this->output->writeln('[*] Downloading Update');
        $downloadToPath = $this->scratchDirectoryPath.DIRECTORY_SEPARATOR.pathinfo($latestVersionDownloadUrl, PATHINFO_BASENAME);
        if (! $this->downloadFile($latestVersionDownloadUrl, $downloadToPath)) {
            $this->panic('There was a problem in downloading the update, please try again.');
        }

        $this->output->writeln('[*] Unpacking Update');
        $this->unzip($downloadToPath, dirname($this->currentPharFileName));
    }

    private function unzip($from, $to)
    {
        $zip = new ZipArchive();
        $res = $zip->open($from);
        if ($res === true) {
            $zip->extractTo($to);
            $zip->close();

            return true;
        } else {
            return false;
        }
    }

    private function downloadFile($url, $filepath, $accept = null)
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Tapestry CLI Update');
        if (! is_null($accept)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $accept);
        }
        $raw_file_data = curl_exec($ch);
        $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

        if (curl_errno($ch)) {
            $this->error(curl_error($ch));
            curl_close($ch);
            exit(1);
        }

        if ($responseCode !== 200) {
            $this->error('Github responded with response code ['.$responseCode.']');
            curl_close($ch);
            exit(1);
        }

        curl_close($ch);

        file_put_contents($filepath, $raw_file_data);

        return (filesize($filepath) > 0) ? true : false;
    }
}