GreenCape/joomla-cli

View on GitHub
src/Command/Core/DownloadCommand.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
/**
 * GreenCape Joomla Command Line Interface
 *
 * MIT License
 *
 * Copyright (c) 2012-2019, Niels Braczek <nbraczek@bsds.de>. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions
 * of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @package         GreenCape\JoomlaCLI
 * @subpackage      Command
 * @author          Niels Braczek <nbraczek@bsds.de>
 * @copyright   (C) 2012-2019 GreenCape, Niels Braczek <nbraczek@bsds.de>
 * @license         http://opensource.org/licenses/MIT The MIT license (MIT)
 * @link            http://greencape.github.io
 * @since           File available since Release 0.1.1
 */

namespace GreenCape\JoomlaCLI\Command\Core;

use GreenCape\JoomlaCLI\Command;
use GreenCape\JoomlaCLI\Repository\VersionList;
use League\Flysystem\Adapter\Local;
use League\Flysystem\FileExistsException;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;

/**
 * The Download command allows the installation of Joomla! from the command line.
 *
 * @package     GreenCape\JoomlaCLI
 * @subpackage  Command
 * @since       Class available since Release 0.1.1
 */
class DownloadCommand extends Command
{
    /**
     * @var string
     */
    private $versionFile;

    /**
     * @var string
     */
    private $version;

    /**
     * @var string
     */
    private $cachePath;

    /**
     * @var OutputInterface
     */
    private $output;

    /**
     * Configure the options for the install command
     *
     * @return  void
     */
    protected function configure(): void
    {
        $this
            ->setName('core:download')
            ->setDescription('Downloads a Joomla! version and unpacks it to the base path')
            ->addArgument(
                'version',
                InputArgument::OPTIONAL,
                'The Joomla! version to install.',
                'latest'
            )
            ->addOption(
                'file',
                'f',
                InputArgument::OPTIONAL,
                'Location of the version cache file',
                '/tmp/versions.json'
            )
            ->addOption(
                'cache',
                'c',
                InputArgument::OPTIONAL,
                'Location of the cache for Joomla! packages',
                '.cache'
            )
        ;
    }

    /**
     * Execute the setup command
     *
     * @param  InputInterface   $input   An InputInterface instance
     * @param  OutputInterface  $output  An OutputInterface instance
     *
     * @return  integer  0 if everything went fine, 1 on error
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->output      = $output;
        $this->version     = $input->getArgument('version');
        $this->versionFile = $input->getOption('file');
        $this->cachePath   = $input->getOption('cache');

        try {
            $basePath = $input->getOption('basepath');

            $versionList = $this->getAvailableVersions();
            $this->createPath($this->cachePath);

            $tarball = $this->getTarball($versionList);
            $this->output->writeln("Archive is {$tarball}", OutputInterface::VERBOSITY_VERY_VERBOSE);

            $this->unpack($basePath, $tarball);

            $this->output->writeln("Installed Joomla! files to  {$basePath}", OutputInterface::VERBOSITY_VERY_VERBOSE);

            return 0;
        } catch (Throwable $e) {
            $this->output->writeln($e->getMessage());

            return 1;
        }
    }

    /**
     * @return VersionList
     * @throws FileNotFoundException
     * @throws FileExistsException
     */
    private function getAvailableVersions(): VersionList
    {
        $filesystem = new Filesystem(new Local(dirname($this->versionFile)));
        $cacheFile  = basename($this->versionFile);

        return new VersionList($filesystem, $cacheFile);
    }

    private function createPath(string $path): void
    {
        if (!@mkdir($path, 0777, true) && !is_dir($path)) {
            throw new RuntimeException(sprintf('Directory "%s" could not be created', $path));  // @codeCoverageIgnore
        }
    }

    /**
     * @param  VersionList  $versions
     *
     * @return string
     */
    private function getTarball(VersionList $versions): string
    {
        $version   = $this->version;
        $cachePath = $this->cachePath;

        $requested = $version;
        $version   = $versions->resolve($version);
        $tarball   = $cachePath . '/' . $version . '.tar.gz';

        if (!$versions->isBranch($version) && file_exists($tarball)) {
            $this->output->writeln("$requested: Joomla $version is already in cache",
                OutputInterface::VERBOSITY_VERBOSE);

            return $tarball;
        }

        if ($versions->isBranch($version)) {
            $this->output->writeln("$requested: Downloading Joomla $version branch",
                OutputInterface::VERBOSITY_VERBOSE);
            $url = 'http://github.com/joomla/joomla-cms/tarball/' . $version;

            return $this->download($tarball, $url);
        }

        if ($versions->isTag($version)) {
            $this->output->writeln("$requested: Downloading Joomla $version", OutputInterface::VERBOSITY_VERBOSE);

            try // to get the official release for that version
            {
                $this->output->writeln('Trying release channel', OutputInterface::VERBOSITY_VERY_VERBOSE);
                $url = "https://github.com/joomla/joomla-cms/releases/download/{$version}/Joomla_{$version}-Stable-Full_Package.tar.gz";

                return $this->download($tarball, $url);
            } catch (Throwable $exception) // else get it from the archive
            {
                $repository = $versions->getRepository($version);
                $this->output->writeln("Trying {$repository} archive", OutputInterface::VERBOSITY_VERY_VERBOSE);
                $url = 'https://github.com/' . $repository . '/archive/' . $version . '.tar.gz';

                return $this->download($tarball, $url);
            }
        }
        throw new RuntimeException("$requested: Version is unknown");
    }

    /**
     * @param          $basePath
     * @param  string  $tarball
     */
    private function unpack($basePath, string $tarball): void
    {
        $this->createPath($basePath);
        shell_exec("tar -zxvf {$tarball} -C {$basePath} --exclude-vcs");

        // If $basePath contains only a single directory, we need to lift everything up one level.
        $dirList = glob("{$basePath}/*", GLOB_ONLYDIR);

        if (count($dirList) === 1) {
            $subDir  = $dirList[0];
            $dirList = array_filter(
                glob("{$subDir}/{*,.*}", GLOB_NOSORT | GLOB_BRACE),
                static function ($filename) {
                    $basename = basename($filename);

                    return ($basename !== '.' && $basename !== '..');
                }
            );
            foreach ($dirList as $item) {
                shell_exec("mv {$item} {$basePath}");
            }
            shell_exec("rm -d {$subDir}");
        }
    }

    /**
     * @param  string  $filename
     * @param  string  $url
     *
     * @return string
     */
    private function download(string $filename, string $url): string
    {
        $bytes = file_put_contents($filename, @fopen($url, 'rb'));

        if ($bytes === false || $bytes === 0) {
            throw new RuntimeException("Failed to download $url");
        }

        return $filename;
    }
}