New bin/gpm direct-install command (#1038)

* initial push of DirectInstall command

* Refactored to support direct-install

* added info about dependencies, and continue question

* Cleanup per @w00fz comments

* put Grav destination check back.
This commit is contained in:
Andy Miller 2016-09-18 10:23:55 -06:00 committed by GitHub
parent 7710cba7ad
commit c57e43ea1d
4 changed files with 447 additions and 68 deletions

View File

@ -67,6 +67,7 @@ $app->addCommands(array(
new \Grav\Console\Gpm\UninstallCommand(),
new \Grav\Console\Gpm\UpdateCommand(),
new \Grav\Console\Gpm\SelfupgradeCommand(),
new \Grav\Console\Gpm\DirectInstallCommand(),
));
$app->run();

View File

@ -179,7 +179,7 @@ abstract class Folder
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($recursive) {
$flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
$flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
+ \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
@ -403,10 +403,7 @@ abstract class Folder
// If the destination directory does not exist create it
if (!is_dir($dest)) {
if (!mkdir($dest)) {
// If the destination directory could not be created stop processing
return false;
}
Folder::mkdir($dest);
}
// Open the source directory to read in files

View File

@ -29,6 +29,8 @@ class Installer
const ZIP_OPEN_ERROR = 32;
/** @const Error while trying to extract the ZIP package */
const ZIP_EXTRACT_ERROR = 64;
/** @const Invalid source file */
const INVALID_SOURCE = 128;
/**
* Destination folder on which validation checks are applied
@ -62,13 +64,13 @@ class Installer
/**
* Installs a given package to a given destination.
*
* @param string $package The local path to the ZIP package
* @param string $zip the local path to ZIP package
* @param string $destination The local path to the Grav Instance
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
*
* @return boolean True if everything went fine, False otherwise.
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
* @param string $extracted The local path to the extacted ZIP package
* @return bool True if everything went fine, False otherwise.
*/
public static function install($package, $destination, $options = [])
public static function install($zip, $destination, $options = [], $extracted = null)
{
$destination = rtrim($destination, DS);
$options = array_merge(self::$options, $options);
@ -86,34 +88,26 @@ class Installer
return false;
}
$zip = new \ZipArchive();
$archive = $zip->open($package);
// Create a tmp location
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp = $tmp_dir . '/Grav-' . uniqid();
if ($archive !== true) {
self::$error = self::ZIP_OPEN_ERROR;
if (!$extracted) {
$extracted = self::unZip($zip, $tmp);
if (!$extracted) {
Folder::delete($tmp);
return false;
}
}
if (!file_exists($extracted)) {
self::$error = self::INVALID_SOURCE;
return false;
}
Folder::mkdir($tmp);
$unzip = $zip->extractTo($tmp);
if (!$unzip) {
self::$error = self::ZIP_EXTRACT_ERROR;
$zip->close();
Folder::delete($tmp);
return false;
}
$package_folder_name = $zip->getNameIndex(0);
$installer_file_folder = $tmp . '/' . $package_folder_name;
$is_install = true;
$installer = self::loadInstaller($installer_file_folder, $is_install);
$installer = self::loadInstaller($extracted, $is_install);
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'preUpdate';
@ -135,16 +129,15 @@ class Installer
if (!$options['sophisticated']) {
if ($options['theme']) {
self::copyInstall($zip, $install_path, $tmp);
self::copyInstall($extracted, $install_path);
} else {
self::moveInstall($zip, $install_path, $tmp);
self::moveInstall($extracted, $install_path);
}
} else {
self::sophisticatedInstall($zip, $install_path, $tmp);
self::sophisticatedInstall($extracted, $install_path);
}
Folder::delete($tmp);
$zip->close();
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'postUpdate';
@ -163,6 +156,43 @@ class Installer
}
/**
* Unzip a file to somewhere
*
* @param $zip_file
* @param $destination
* @return bool|string
*/
public static function unZip($zip_file, $destination)
{
$zip = new \ZipArchive();
$archive = $zip->open($zip_file);
if ($archive === true) {
Folder::mkdir($destination);
$unzip = $zip->extractTo($destination);
if (!$unzip) {
self::$error = self::ZIP_EXTRACT_ERROR;
Folder::delete($destination);
$zip->close();
return false;
}
$package_folder_name = $zip->getNameIndex(0);
$zip->close();
$extracted_folder = $destination . '/' . $package_folder_name;
return $extracted_folder;
}
self::$error = self::ZIP_EXTRACT_ERROR;
return false;
}
/**
* Instantiates and returns the package installer class
*
@ -211,80 +241,67 @@ class Installer
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function moveInstall(\ZipArchive $zip, $install_path, $tmp)
public static function moveInstall($source_path, $install_path)
{
$container = $zip->getNameIndex(0);
if (file_exists($install_path)) {
Folder::delete($install_path);
}
Folder::move($tmp . DS . $container, $install_path);
Folder::move($source_path, $install_path);
return true;
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function copyInstall(\ZipArchive $zip, $install_path, $tmp)
public static function copyInstall($source_path, $install_path)
{
$firstDir = $zip->getNameIndex(0);
if (empty($firstDir)) {
throw new \RuntimeException("Directory $firstDir is missing");
if (empty($source_path)) {
throw new \RuntimeException("Directory $source_path is missing");
} else {
$tmp = realpath($tmp . DS . $firstDir);
Folder::rcopy($tmp, $install_path);
Folder::rcopy($source_path, $install_path);
}
return true;
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp)
public static function sophisticatedInstall($source_path, $install_path)
{
for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) {
$filename = $zip->getNameIndex($i);
$fileinfo = pathinfo($filename);
$depth = count(explode(DS, rtrim($filename, '/')));
foreach (new \DirectoryIterator($source_path) as $file) {
if ($depth > 2) {
if ($file->isLink() || $file->isDot()) {
continue;
}
$path = $install_path . DS . $fileinfo['basename'];
$path = $install_path . DS . $file->getBasename();
if (is_link($path)) {
continue;
} else {
if (is_dir($path)) {
Folder::delete($path);
Folder::move($tmp . DS . $filename, $path);
if ($file->isDir()) {
Folder::delete($path);
Folder::move($file->getPathname(), $path);
if ($fileinfo['basename'] == 'bin') {
foreach (glob($path . DS . '*') as $file) {
@chmod($file, 0755);
}
if ($file->getBasename() == 'bin') {
foreach (glob($path . DS . '*') as $bin_file) {
@chmod($bin_file, 0755);
}
} else {
@unlink($path);
@copy($tmp . DS . $filename, $path);
}
} else {
@unlink($path);
@copy($file->getPathname(), $path);
}
}

View File

@ -0,0 +1,364 @@
<?php
/**
* @package Grav.Console
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
class DirectInstallCommand extends ConsoleCommand
{
/**
*
*/
protected function configure()
{
$this
->setName("direct-install")
->setAliases(['directinstall'])
->addArgument(
'package-file',
InputArgument::REQUIRED,
'The local location or remote URL to an installable package file'
)
->setDescription("Installs Grav, plugin, or theme directly from a file or a URL")
->setHelp('The <info>direct-install</info> command installs Grav, plugin, or theme directly from a file or a URL');
}
/**
* @return int|null|void
*/
protected function serve()
{
$package_file = $this->input->getArgument('package-file');
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Are you sure you want to direct-install <cyan>'.$package_file.'</cyan> [y|N] ', false);
$answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("exiting...");
$this->output->writeln('');
exit;
}
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp_zip = $tmp_dir . '/Grav-' . uniqid();
$this->output->writeln("");
$this->output->writeln("Preparing to install <cyan>" . $package_file . "</cyan>");
if ($this->isRemote($package_file)) {
$zip = $this->downloadPackage($package_file, $tmp_zip);
} else {
$zip = $this->copyPackage($package_file, $tmp_zip);
}
if (file_exists($zip)) {
$tmp_source = $tmp_dir . '/Grav-' . uniqid();
$this->output->write(" |- Extracting package... ");
$extracted = Installer::unZip($zip, $tmp_source);
if (!$extracted) {
$this->output->write("\x0D");
$this->output->writeln(" |- Extracting package... <red>failed</red>");
exit;
}
$this->output->write("\x0D");
$this->output->writeln(" |- Extracting package... <green>ok</green>");
$type = $this->getPackageType($extracted);
if (!$type) {
$this->output->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
$this->output->writeln('');
exit;
}
$blueprint = $this->getBlueprints($extracted);
if ($blueprint) {
if (isset($blueprint['dependencies'])) {
$depencencies = [];
foreach ($blueprint['dependencies'] as $dependency) {
if (is_array($dependency) && isset($dependency['name'])) {
$depencencies[] = $dependency['name'];
} else {
$depencencies[] = $dependency;
}
}
$this->output->writeln(" |- Dependencies found... <cyan>[" . implode(',', $depencencies) . "]</cyan>");
$question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false);
$answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("exiting...");
$this->output->writeln('');
exit;
}
}
}
if ($type == 'grav') {
$this->output->write(" |- Checking destination... ");
Installer::isValidDestination(GRAV_ROOT . '/system');
if (Installer::IS_LINK === Installer::lastErrorCode()) {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <yellow>symbolic link</yellow>");
$this->output->writeln(" '- <red>ERROR: symlinks found...</red> <yellow>" . GRAV_ROOT."</yellow>");
$this->output->writeln('');
exit;
}
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <green>ok</green>");
$this->output->write(" |- Installing package... ");
Installer::install($zip, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true], $extracted);
} else {
$name = $this->getPackageName($extracted);
if (!$name) {
$this->output->writeln("<red>ERROR: Name could not be determined.</red> Please specify with --name|-n");
$this->output->writeln('');
exit;
}
$install_path = $this->getInstallPath($type, $name);
$is_update = file_exists($install_path);
$this->output->write(" |- Checking destination... ");
Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <yellow>symbolic link</yellow>");
$this->output->writeln(" '- <red>ERROR: symlink found...</red> <yellow>" . GRAV_ROOT . DS . $install_path . '</yellow>');
$this->output->writeln('');
exit;
} else {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <green>ok</green>");
}
$this->output->write(" |- Installing package... ");
Installer::install($zip, GRAV_ROOT, ['install_path' => $install_path, 'theme' => (($type == 'theme')), 'is_update' => $is_update], $extracted);
}
Folder::delete($tmp_source);
$this->output->write("\x0D");
if(Installer::lastErrorCode()) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
} else {
$this->output->writeln(" |- Installing package... <green>ok</green>");
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
}
} else {
$this->output->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
}
Folder::delete($tmp_zip);
// clear cache after successful upgrade
$this->clearCache();
return true;
}
/**
* Get the install path for a name and a particular type of package
*
* @param $type
* @param $name
* @return string
*/
protected function getInstallPath($type, $name)
{
$locator = Grav::instance()['locator'];
if ($type == 'theme') {
$install_path = $locator->findResource('themes://', false) . DS . $name;
} else {
$install_path = $locator->findResource('plugins://', false) . DS . $name;
}
return $install_path;
}
/**
* Try to guess the package name from the source files
*
* @param $source
* @return bool|string
*/
protected function getPackageName($source)
{
foreach (glob($source . "*.yaml") as $filename) {
$name = strtolower(basename($filename, '.yaml'));
if ($name == 'blueprints') {
continue;
}
return $name;
}
return false;
}
/**
* Try to guess the package type from the source files
*
* @param $source
* @return bool|string
*/
protected function getPackageType($source)
{
if (
file_exists($source . 'system/defines.php') &&
file_exists($source . 'system/config/system.yaml')
) {
return 'grav';
} else {
// must have a blueprint
if (!file_exists($source . 'blueprints.yaml')) {
return false;
}
// either theme or plugin
$name = basename($source);
if (Utils::contains($name, 'theme')) {
return 'theme';
} elseif (Utils::contains($name, 'plugin')) {
return 'plugin';
}
foreach (glob($source . "*.php") as $filename) {
$contents = file_get_contents($filename);
if (Utils::contains($contents, 'Grav\Common\Theme')) {
return 'theme';
} elseif (Utils::contains($contents, 'Grav\Common\Plugin')) {
return 'plugin';
}
}
// Assume it's a theme
return 'theme';
}
}
/**
* Determine if this is a local or a remote file
*
* @param $file
* @return bool
*/
protected function isRemote($file)
{
return (bool) filter_var($file, FILTER_VALIDATE_URL);
}
/**
* Find/Parse the blueprint file
*
* @param $source
* @return array|bool
*/
protected function getBlueprints($source)
{
$blueprint_file = $source . 'blueprints.yaml';
if (!file_exists($blueprint_file)) {
return false;
}
$blueprint = (array)Yaml::parse(file_get_contents($blueprint_file));
return $blueprint;
}
/**
* Download the zip package via the URL
*
* @param $package_file
* @param $tmp
* @return null|string
*/
private function downloadPackage($package_file, $tmp)
{
$this->output->write(" |- Downloading package... 0%");
$package = parse_url($package_file);
$filename = basename($package['path']);
$output = Response::get($package_file, [], [$this, 'progress']);
if ($output) {
Folder::mkdir($tmp);
$this->output->write("\x0D");
$this->output->write(" |- Downloading package... 100%");
$this->output->writeln('');
file_put_contents($tmp . DS . $filename, $output);
return $tmp . DS . $filename;
}
return null;
}
/**
* Copy the local zip package to tmp
*
* @param $package_file
* @param $tmp
* @return null|string
*/
private function copyPackage($package_file, $tmp)
{
$this->output->write(" |- Copying package... 0%");
$package_file = realpath($package_file);
if (file_exists($package_file)) {
$filename = basename($package_file);
Folder::mkdir($tmp);
$this->output->write("\x0D");
$this->output->write(" |- Copying package... 100%");
$this->output->writeln('');
copy(realpath($package_file), $tmp . DS . $filename);
return $tmp . DS . $filename;
}
return null;
}
}