/** * Get/Create a transaction. * * @param string|Pyrus\Installer\Role\Common $path The directory path. * @return Transaction A Pyrus\AtomicFileTransaction\Transaction instance */ public function getTransaction($path) { if ($path instanceof \Pyrus\Installer\Role\Common) { $path = \Pyrus\Config::current()->{$path->getLocationConfig()}; } $path = FS::path((string) $path); if (isset($this->transactions[$path])) { return $this->transactions[$path]; } // Create new transaction object $class = $this->getTransactionClass(); try { $this->transactions[$path] = new $class($path, $this); return $this->transactions[$path]; } catch (\Exception $e) { $errs = new MultiErrors(); $errs->E_ERROR[] = $e; $errs->merge($this->rollbackTransactions()); throw new MultiException('Unable to begin transaction', $errs); } }
/** * Commit the transaction. * Replaces the journal path with the original path. * * @throws RuntimeException * @return void */ public function commit() { $this->checkActive(); if (file_exists($this->path)) { FS::rmrf($this->path); } // here is the only critical moment - a failure in between these two renames // leaves us with no source if (!@rename($this->journalPath, $this->path)) { throw new IOException('CRITICAL - unable to complete transaction, rename of journal to actual path failed'); } $this->inTransaction = false; }
/** * Completely remove all traces of an xml registry */ public static function removeRegistry($path) { if (!file_exists($path . '/.xmlregistry')) { return; } try { \Pyrus\Filesystem::rmrf(realpath($path . DIRECTORY_SEPARATOR . '.xmlregistry')); } catch (AtomicFileTransaction\Exception $e) { throw new Exception('Cannot remove XML registry: ' . $e->getMessage(), $e); } }
/** * Get the full journal path based on a relative path. * * @param string $relativePath * @return string */ protected function getPath($relativePath) { return \Pyrus\Filesystem::path($this->journalPath . '/' . (string) $relativePath); }
/** * Render packages from the creators passed into the constructor. * * This will take any package source and an array mapping internal * path => file name and create new packages in the formats requested. * * All files in package.xml will have the string @PACKAGE_VERSION@ * automatically replaced with the current package's version * @param \Pyrus\Package $package * @param array $extrafiles */ function render(\Pyrus\Package $package, array $extrafiles = array()) { foreach ($this->_creators as $creator) { $creator->init(); } $this->prepend = $prepend = $package->name . '-' . $package->version['release']; if ($package->isNewPackage()) { $packagexml = $prepend . '/.xmlregistry/packages/' . str_replace('/', '!', $package->channel) . '/' . $package->name . '/' . $package->version['release'] . '-info.xml'; } else { if ($package->isOldAndCrustyCompatible()) { $packagexml = 'package2.xml'; $old = file_get_contents('package.xml'); } else { $packagexml = 'package.xml'; } } if (self::VERSION === '@' . 'PACKAGE_VERSION@') { // we're running straight from SVN, so pretend to be 2.0.0 $package->packagerversion = '2.0.0'; } else { $package->packagerversion = self::VERSION; } // get packaging package.xml $packageingstr = (string) new \Pyrus\XMLWriter($package->toArray(true)); foreach ($this->_creators as $creator) { $creator->addFile($packagexml, $packageingstr); } if ($package->isOldAndCrustyCompatible()) { foreach ($this->_creators as $creator) { $creator->addFile('package.xml', $old); } } if ($package->getInternalPackage() instanceof Xml) { // check for package_compatible.xml if ($package->isNewPackage() && file_exists($package->getFilePath('package_compatible.xml'))) { foreach ($this->_creators as $creator) { $creator->addFile('package.xml', file_get_contents($package->getFilePath('package_compatible.xml'))); } } } $packagingloc = \Pyrus\Config::current()->temp_dir . DIRECTORY_SEPARATOR . 'pyrpackage'; if (file_exists($packagingloc)) { \Pyrus\Filesystem::rmrf($packagingloc, false, false); } mkdir($packagingloc, 0777, true); // $packageat is the relative path within the archive // $info is an array of format: // array('attribs' => array('name' => ...)[, 'tasks:blah' ...]) $alreadyPackaged = array(); $globalreplace = array('attribs' => array('from' => '@' . 'PACKAGE_VERSION@', 'to' => 'version', 'type' => 'package-info')); foreach ($package->packagingcontents as $packageat => $info) { $role = \Pyrus\Installer\Role::factory($package->getPackageType(), $info['attribs']['role']); try { $role->packageTimeValidate($package, $info); } catch (\Exception $e) { throw new Creator\Exception('Invalid file ' . $packageat . ': ' . $e->getMessage(), $e); } $packageat = str_replace('\\', '/', $packageat); $packageat = str_replace('//', '/', $packageat); if ($packageat[0] === '/' || strlen($packageat) > 2 && ($packageat[1] === ':' && $packageat[2] == '/')) { throw new Creator\Exception('Invalid path, cannot save a root path ' . $packageat); } if (preg_match('@^\\.\\.?/|/\\.\\.?\\z|/\\.\\./@', $packageat)) { throw new Creator\Exception('Invalid path, cannot use directory ' . 'reference . or .. ' . $packageat); } $alreadyPackaged[$packageat] = true; $packageat = $prepend . '/' . $packageat; $contents = $package->getFileContents($info['attribs']['name'], true); if (!file_exists(dirname($packagingloc . DIRECTORY_SEPARATOR . $packageat))) { mkdir(dirname($packagingloc . DIRECTORY_SEPARATOR . $packageat), 0777, true); } $fp = fopen($packagingloc . DIRECTORY_SEPARATOR . $packageat, 'wb+'); ftruncate($fp, 0); stream_copy_to_stream($contents, $fp); fclose($contents); rewind($fp); if ($package->isNewPackage() && $info['attribs']['role'] == 'php') { if (isset($info['tasks:replace'])) { if (isset($info['tasks:replace'][0])) { $info['tasks:replace'][] = $globalreplace; } else { $info['tasks:replace'] = array($info['tasks:replace'], $globalreplace); } } else { $info['tasks:replace'] = $globalreplace; } } if (isset(\Pyrus\Config::current()->registry->package[$package->channel . '/' . $package->name])) { $version = \Pyrus\Config::current()->registry->info($package->name, $package->channel, 'version'); } else { $version = null; } foreach (new Creator\TaskIterator($info, $package, \Pyrus\Task\Common::PACKAGE, $version) as $task) { // do pre-processing of file contents try { $task->startSession($fp, $packageat); } catch (\Exception $e) { // TODO: handle exceptions } } fclose($fp); } foreach ($this->_creators as $creator) { $creator->addDir($packagingloc); } if ($package->isNewPackage()) { $this->addPEAR2Stuff($alreadyPackaged); } $this->addExtraFiles($extrafiles); foreach ($this->_creators as $creator) { $creator->close(); } \Pyrus\Filesystem::rmrf($packagingloc, false, false); }
/** * Repair from a previously failed transaction cut off mid-transaction */ public static function repair() { if (static::inTransaction()) { throw new AtomicFileTransaction\RuntimeException('Cannot repair while in a transaction'); } static::$instance = null; $config = Config::current(); $remove = array(); foreach ($config->systemvars as $var) { if (!strpos($var, '_dir')) { continue; } $path = FS::path($config->{$var}); $backuppath = dirname($path) . DIRECTORY_SEPARATOR . '.old-' . basename($path); if (file_exists($backuppath) && is_dir($backuppath)) { if (file_exists($path)) { if (!is_dir($path)) { throw new AtomicFileTransaction\RuntimeException('Repair failed - ' . $var . ' path ' . $path . ' is not a directory. Move this file out of the way and ' . 'try the repair again'); } // this is the new stuff from journal path, so move it out of the way $journalpath = dirname($path) . DIRECTORY_SEPARATOR . '.journal-' . basename($path); $remove[] = $journalpath; rename($path, $journalpath); } // restore backup rename($backuppath, $path); } } foreach ($remove as $path) { FS::rmrf($path); } }
/** * Rollback the entire transaction. * This should restore the backup and remove any other new files. * * @return void */ public function revert() { if ($this->inTransaction) { throw new RuntimeException('Cannot revert - still in a transaction'); } if (!$this->hasBackup()) { return; } if (!file_exists($this->backupPath)) { throw new IOException('Cannot restore backup, no backup directory available.'); } if (file_exists($this->journalPath)) { throw new IOException('Cannot restore backup, a journal directory/file still exists.'); } // Rename current path to journal path if (file_exists($this->path)) { if (!rename($this->path, $this->journalPath)) { throw new IOException('Cannot restore backup, unable to rename directory.'); } } // Rename backup path to path if (!rename($this->backupPath, $this->path)) { throw new IOException('Cannot restore backup, unable to rename backup directory.'); } // Remove journal directory if (file_exists($this->journalPath)) { FS::rmrf($this->journalPath); } }
/** * Completely remove all traces of a PEAR 1.x registry */ public static function removeRegistry($path) { $path = $path . DIRECTORY_SEPARATOR . 'php'; if (!file_exists($path . '/.registry')) { return; } try { \Pyrus\Filesystem::rmrf(realpath($path . DIRECTORY_SEPARATOR . '.registry')); } catch (AtomicFileTransaction\Exception $e) { throw new Exception('Cannot remove Pear1 registry: ' . $e->getMessage(), $e); } $errs = new \PEAR2\MultiErrors(); try { if (file_exists($path . '/.channels')) { \Pyrus\Filesystem::rmrf(realpath($path . DIRECTORY_SEPARATOR . '.channels')); } } catch (AtomicFileTransaction\Exception $e) { $errs->E_ERROR[] = new Exception('Cannot remove Pear1 registry: ' . $e->getMessage(), $e); } foreach (array('.filemap', '.lock', '.depdb', '.depdblock') as $file) { if (file_exists($path . DIRECTORY_SEPARATOR . $file) && !@unlink(realpath($path . DIRECTORY_SEPARATOR . $file))) { $errs->E_ERROR[] = new Exception('Cannot remove Pear1 registry: Unable to remove ' . $file); } } if (count($errs->E_ERROR)) { throw new Exception('Unable to remove Pear1 registry', $errs); } }