From 2760221767cdbc0ececa359f47a368e793fd7f97 Mon Sep 17 00:00:00 2001 From: chr0n1x Date: Sat, 21 Dec 2013 03:06:10 -0500 Subject: [PATCH 1/4] SelfUpdateCommand: initial groundwork for --rollback --- src/Composer/Command/SelfUpdateCommand.php | 152 ++++++++++++++++----- 1 file changed, 121 insertions(+), 31 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 4074d8bad..598be7431 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -17,6 +17,7 @@ use Composer\Factory; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -24,12 +25,32 @@ use Symfony\Component\Console\Output\OutputInterface; */ class SelfUpdateCommand extends Command { + const ROLLBACK = 'rollback'; + const HOMEPAGE = 'getcomposer.org'; + + protected $remoteFS; + protected $latestVersion; + protected $homepageURL; + protected $localFilename; + + public function __construct($name = null) + { + parent::__construct($name); + $protocol = (extension_loaded('openssl') ? 'https' : 'http') . '://'; + $this->homepageURL = $protocol . self::HOMEPAGE; + $this->remoteFS = new RemoteFilesystem($this->getIO()); + $this->localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + } + protected function configure() { $this ->setName('self-update') ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') + ->setDefinition(array( + new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_OPTIONAL, 'Revert to an older installation of composer'), + )) ->setHelp(<<self-update command checks getcomposer.org for newer versions of composer and if found, installs the latest. @@ -44,57 +65,126 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $config = Factory::createConfig(); - $cacheDir = $config->get('cache-dir'); - - $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + $cacheDir = rtrim($config->get('cache-dir'), '/'); + $saveDir = rtrim($config->get('home'), '/'); // Check if current dir is writable and if not try the cache dir from settings - $tmpDir = is_writable(dirname($localFilename))? dirname($localFilename) : $cacheDir; - $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp.phar'; + $tmpDir = is_writable(dirname($this->localFilename))? dirname($this->localFilename) : $cacheDir; // check for permissions in local filesystem before start connection process if (!is_writable($tmpDir)) { throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); } - if (!is_writable($localFilename)) { - throw new FilesystemException('Composer update failed: the "'.$localFilename. '" file could not be written'); + if (!is_writable($this->localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$this->localFilename.'" file could not be written'); } - $protocol = extension_loaded('openssl') ? 'https' : 'http'; - $rfs = new RemoteFilesystem($this->getIO()); - $latest = trim($rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/version', false)); + $rollback = $this->getOption(self::ROLLBACK); - if (Composer::VERSION !== $latest) { - $output->writeln(sprintf("Updating to version %s.", $latest)); + if (is_null($rollback)) { + $rollback = $this->getLastVersion(); + if (!$rollback) { + throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$saveDir.'"'); - $remoteFilename = $protocol . '://getcomposer.org/composer.phar'; + return 1; + } + } - $rfs->copy('getcomposer.org', $remoteFilename, $tempFilename); + // if a rollback version is specified, check for permissions and rollback installation + if ($rollback) { + if (!is_writable($saveDir)) { + throw new FilesystemException('Composer rollback failed: the "'.$saveDir.'" dir could not be written to'); + } + $old = $saveDir . "/{$rollback}.phar"; + + if (!is_file($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); + } + if (!is_readable($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be read'); + } + } + + $updateVersion = ($rollback)? $rollback : $this->getLatestVersion(); + + if (Composer::VERSION === $updateVersion) { + $output->writeln("You are already using composer v%s.", $updateVersion); + + return 0; + } + + $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; + + if ($rollback) { + copy($saveDir . "/{$rollback}.phar", $tempFilename); + } else { + $endpoint = ($updateVersion === $this->getLatestVersion()) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"; + $remoteFilename = $this->homepageURL . $endpoint; + + $output->writeln(sprintf("Updating to version %s.", $updateVersion)); + + $this->remoteFS->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + + // @todo: handle snapshot versions not being found! if (!file_exists($tempFilename)) { $output->writeln('The download of the new composer version failed for an unexpected reason'); return 1; } + } - try { - @chmod($tempFilename, 0777 & ~umask()); - // test the phar validity - $phar = new \Phar($tempFilename); - // free the variable to unlock the file - unset($phar); - rename($tempFilename, $localFilename); - } catch (\Exception $e) { - @unlink($tempFilename); - if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { - throw $e; - } - $output->writeln('The download is corrupted ('.$e->getMessage().').'); - $output->writeln('Please re-run the self-update command to try again.'); - } - } else { - $output->writeln("You are using the latest composer version."); + if ($err = $this->setLocalPhar($tempFilename, $saveDir)) { + $output->writeln('The file is corrupted ('.$err->getMessage().').'); + $output->writeln('Please re-run the self-update command to try again.'); + + return 1; } } + + protected function setLocalPhar($filename, $saveDir) + { + try { + @chmod($filename, 0777 & ~umask()); + // test the phar validity + $phar = new \Phar($filename); + // copy current file into installations dir + copy($this->localFilename, $saveDir . Composer::VERSION . '.phar'); + // free the variable to unlock the file + unset($phar); + rename($filename, $this->localFilename); + } catch (\Exception $e) { + @unlink($filename); + if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { + throw $e; + } + + return $e; + } + } + + protected function getLastVersion($saveDir) + { + $config = Factory::createConfig(); + $files = glob($saveDir . '/*.phar'); + + if (empty($files)) { + return false; + } + + $fileTimes = array_map('filemtime', $files); + $map = array_combine($fileTimes, $files); + $latest = max($fileTimes); + return basename($map[$latest], '.phar'); + } + + protected function getLatestVersion() + { + if (!$this->latestVersion) { + $this->latestVersion = trim($this->remoteFS->getContents(self::HOMEPAGE, $this->homepageURL. '/version', false)); + } + + return $this->latestVersion; + } } From bc5ce1ce04f61e4557f4c08823b77a55831d7c3d Mon Sep 17 00:00:00 2001 From: chr0n1x Date: Sun, 22 Dec 2013 00:22:27 -0500 Subject: [PATCH 2/4] SelfUpdateCommand: only use 1 rollback snapshot at a time --- src/Composer/Command/SelfUpdateCommand.php | 67 ++++++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 598be7431..9f9dce7e0 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -14,6 +14,7 @@ namespace Composer\Command; use Composer\Composer; use Composer\Factory; +use Composer\Util\Filesystem; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; @@ -27,6 +28,7 @@ class SelfUpdateCommand extends Command { const ROLLBACK = 'rollback'; const HOMEPAGE = 'getcomposer.org'; + const OLD_INSTALL_EXT = '-old.phar'; protected $remoteFS; protected $latestVersion; @@ -49,7 +51,7 @@ class SelfUpdateCommand extends Command ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') ->setDefinition(array( - new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_OPTIONAL, 'Revert to an older installation of composer'), + new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer') )) ->setHelp(<<self-update command checks getcomposer.org for newer @@ -66,7 +68,6 @@ EOT { $config = Factory::createConfig(); $cacheDir = rtrim($config->get('cache-dir'), '/'); - $saveDir = rtrim($config->get('home'), '/'); // Check if current dir is writable and if not try the cache dir from settings $tmpDir = is_writable(dirname($this->localFilename))? dirname($this->localFilename) : $cacheDir; @@ -80,24 +81,27 @@ EOT throw new FilesystemException('Composer update failed: the "'.$this->localFilename.'" file could not be written'); } - $rollback = $this->getOption(self::ROLLBACK); + $rollbackVersion = false; - if (is_null($rollback)) { - $rollback = $this->getLastVersion(); - if (!$rollback) { + // rollback specified, get last phar + if ($input->getOption(self::ROLLBACK)) { + $rollbackVersion = $this->getLastVersion($saveDir); + if (!$rollbackVersion) { throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$saveDir.'"'); return 1; } } + $saveDir = rtrim($config->get('home'), '/'); + // if a rollback version is specified, check for permissions and rollback installation - if ($rollback) { + if ($rollbackVersion) { if (!is_writable($saveDir)) { throw new FilesystemException('Composer rollback failed: the "'.$saveDir.'" dir could not be written to'); } - $old = $saveDir . "/{$rollback}.phar"; + $old = $saveDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; if (!is_file($old)) { throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); @@ -107,18 +111,20 @@ EOT } } - $updateVersion = ($rollback)? $rollback : $this->getLatestVersion(); + $updateVersion = ($rollbackVersion)? $rollbackVersion : $this->getLatestVersion(); if (Composer::VERSION === $updateVersion) { - $output->writeln("You are already using composer v%s.", $updateVersion); + $output->writeln('You are already using composer version '.$updateVersion.'.'); return 0; } $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; + $backupFile = ($rollbackVersion)? false : $saveDir . '/' . Composer::VERSION . self::OLD_INSTALL_EXT; - if ($rollback) { - copy($saveDir . "/{$rollback}.phar", $tempFilename); + if ($rollbackVersion) { + rename($saveDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT, $tempFilename); + $output->writeln(sprintf("Rolling back to cached version %s.", $rollbackVersion)); } else { $endpoint = ($updateVersion === $this->getLatestVersion()) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"; $remoteFilename = $this->homepageURL . $endpoint; @@ -133,25 +139,44 @@ EOT return 1; } + + // remove saved installations of composer + $files = $this->getOldInstallationFiles($saveDir); + if (!empty($files)) { + $fs = new Filesystem; + foreach ($files as $file) { + $output->writeln('Removing: '.$file); + $fs->remove($file); + } + } } - if ($err = $this->setLocalPhar($tempFilename, $saveDir)) { + if ($err = $this->setLocalPhar($tempFilename, $backupFile)) { $output->writeln('The file is corrupted ('.$err->getMessage().').'); $output->writeln('Please re-run the self-update command to try again.'); return 1; } + + if ($backupFile) { + $output->writeln('Saved rollback snapshot '.$backupFile); + } } - protected function setLocalPhar($filename, $saveDir) + protected function setLocalPhar($filename, $backupFile) { try { @chmod($filename, 0777 & ~umask()); // test the phar validity $phar = new \Phar($filename); - // copy current file into installations dir - copy($this->localFilename, $saveDir . Composer::VERSION . '.phar'); // free the variable to unlock the file + unset($phar); + + // copy current file into installations dir + if ($backupFile) { + copy($this->localFilename, $backupFile); + } + unset($phar); rename($filename, $this->localFilename); } catch (\Exception $e) { @@ -166,8 +191,7 @@ EOT protected function getLastVersion($saveDir) { - $config = Factory::createConfig(); - $files = glob($saveDir . '/*.phar'); + $files = $this->getOldInstallationFiles($saveDir); if (empty($files)) { return false; @@ -176,7 +200,12 @@ EOT $fileTimes = array_map('filemtime', $files); $map = array_combine($fileTimes, $files); $latest = max($fileTimes); - return basename($map[$latest], '.phar'); + return basename($map[$latest], self::OLD_INSTALL_EXT); + } + + protected function getOldInstallationFiles($saveDir) + { + return glob($saveDir . '/*' . self::OLD_INSTALL_EXT); } protected function getLatestVersion() From 0c76bba8bb001891c8022225262450b7f6d543b3 Mon Sep 17 00:00:00 2001 From: chr0n1x Date: Sun, 22 Dec 2013 00:36:24 -0500 Subject: [PATCH 3/4] SelfUpdateCommand: do not delete old snapshots, allow user to clean them --- src/Composer/Command/SelfUpdateCommand.php | 45 ++++++++++++---------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 9f9dce7e0..40f71dac7 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SelfUpdateCommand extends Command { const ROLLBACK = 'rollback'; + const CLEAN_ROLLBACKS = 'clean-rollbacks'; const HOMEPAGE = 'getcomposer.org'; const OLD_INSTALL_EXT = '-old.phar'; @@ -51,7 +52,8 @@ class SelfUpdateCommand extends Command ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') ->setDefinition(array( - new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer') + new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer'), + new InputOption(self::CLEAN_ROLLBACKS, null, InputOption::VALUE_NONE, 'Delete old snapshots during an update. This makes the current version of composer the only rollback snapshot after the update') )) ->setHelp(<<self-update command checks getcomposer.org for newer @@ -82,26 +84,25 @@ EOT } $rollbackVersion = false; + $rollbackDir = rtrim($config->get('home'), '/'); // rollback specified, get last phar if ($input->getOption(self::ROLLBACK)) { - $rollbackVersion = $this->getLastVersion($saveDir); + $rollbackVersion = $this->getLastVersion($rollbackDir); if (!$rollbackVersion) { - throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$saveDir.'"'); + throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); return 1; } } - $saveDir = rtrim($config->get('home'), '/'); - // if a rollback version is specified, check for permissions and rollback installation if ($rollbackVersion) { - if (!is_writable($saveDir)) { - throw new FilesystemException('Composer rollback failed: the "'.$saveDir.'" dir could not be written to'); + if (!is_writable($rollbackDir)) { + throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to'); } - $old = $saveDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; + $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; if (!is_file($old)) { throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); @@ -120,10 +121,10 @@ EOT } $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; - $backupFile = ($rollbackVersion)? false : $saveDir . '/' . Composer::VERSION . self::OLD_INSTALL_EXT; + $backupFile = ($rollbackVersion)? false : $rollbackDir . '/' . Composer::VERSION . self::OLD_INSTALL_EXT; if ($rollbackVersion) { - rename($saveDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT, $tempFilename); + rename($rollbackDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT, $tempFilename); $output->writeln(sprintf("Rolling back to cached version %s.", $rollbackVersion)); } else { $endpoint = ($updateVersion === $this->getLatestVersion()) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"; @@ -141,12 +142,16 @@ EOT } // remove saved installations of composer - $files = $this->getOldInstallationFiles($saveDir); - if (!empty($files)) { - $fs = new Filesystem; - foreach ($files as $file) { - $output->writeln('Removing: '.$file); - $fs->remove($file); + if ($input->getOption(self::CLEAN_ROLLBACKS)) { + $files = $this->getOldInstallationFiles($rollbackDir); + + if (!empty($files)) { + $fs = new Filesystem; + + foreach ($files as $file) { + $output->writeln('Removing: '.$file); + $fs->remove($file); + } } } } @@ -189,9 +194,9 @@ EOT } } - protected function getLastVersion($saveDir) + protected function getLastVersion($rollbackDir) { - $files = $this->getOldInstallationFiles($saveDir); + $files = $this->getOldInstallationFiles($rollbackDir); if (empty($files)) { return false; @@ -203,9 +208,9 @@ EOT return basename($map[$latest], self::OLD_INSTALL_EXT); } - protected function getOldInstallationFiles($saveDir) + protected function getOldInstallationFiles($rollbackDir) { - return glob($saveDir . '/*' . self::OLD_INSTALL_EXT); + return glob($rollbackDir . '/*' . self::OLD_INSTALL_EXT); } protected function getLatestVersion() From d26355ef65fb271aebe39cc9b50f840304c64b3f Mon Sep 17 00:00:00 2001 From: chr0n1x Date: Sun, 22 Dec 2013 00:59:02 -0500 Subject: [PATCH 4/4] SelfUpdateCommand: removed unneeded return --- src/Composer/Command/SelfUpdateCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 40f71dac7..d29d50b36 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -91,8 +91,6 @@ EOT $rollbackVersion = $this->getLastVersion($rollbackDir); if (!$rollbackVersion) { throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); - - return 1; } }