container = $container; if(!isset($container['dbrestore']['dbkey'])) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_NORESTOREDBKEYINCONTAINER'), 500); } $this->dbKey = $container['dbrestore']['dbkey']; $maxExecTime = isset($container['dbrestore']['maxexectime']) ? (int)$container['dbrestore']['maxexectime'] : 5; $runTimeBias = isset($container['dbrestore']['runtimebias']) ? (int)$container['dbrestore']['runtimebias'] : 75; $maxExecTime = ($maxExecTime < 1) ? 1 : $maxExecTime; $runTimeBias = ($runTimeBias < 10) ? 10 : $runTimeBias; $this->timer = new Timer($maxExecTime, $runTimeBias); $this->populatePartsMap(); } /** * Public destructor. Closes open handlers. * * @return void */ public function __destruct() { if (is_object($this->db)) { if ($this->db instanceof Driver) { try { $this->db->disconnect(); } catch (\Exception $exc) { // Nothing. We just never want to fail when closing the // database connection. } } } if (is_resource($this->file)) { @fclose($this->file); } } /** * Gets an instance of the database restoration class based on the container. * * @staticvar array $instances The array of \Awf\Database\Restore instances * * @param Container $container The container the class is attached to * * @return Restore * * @throws \Exception */ public static function getInstance(Container $container) { if (!isset($container['dbrestore'])) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_NORESTOREDATAINCONTAINER'), 500); } if(!isset($container['dbrestore']['dbkey'])) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_NORESTOREDBKEYINCONTAINER'), 500); } $dbkey = $container['dbrestore']['dbkey']; if (!array_key_exists($dbkey, self::$instances)) { if(!isset($container['dbrestore']['dbtype'])) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_NORESTOREDBTYPEINCONTAINER'), 500); } $class = '\\Awf\\Database\\Restore\\' . ucfirst($container['dbrestore']['dbtype']); if(!class_exists($class, true)) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_RESTORECLASSNOTEXISTS'), 500); } self::$instances[$dbkey] = new $class($container); } return self::$instances[$dbkey]; } /** * Remove all cached information from the session storage */ protected function removeInformationFromStorage() { $variables = array( 'start', 'foffset', 'totalqueries', 'curpart', 'partsmap', 'totalsize', 'runsize' ); $session = $this->container->segment; foreach ($variables as $var) { $key = 'restore_' . $this->dbKey . '_' . $var; $session->$key = null; } } /** * Return a value from the session storage * * @param string $var The name of the variable * @param mixed $default The default value (null if ommitted) * * @return mixed The variable's value */ protected function getFromStorage($var, $default = null) { $session = $this->container->segment; $key = 'restore_' . $this->dbKey . '_' . $var; if (!isset($session->$key)) { $session->$key = $default; } return $session->$key; } /** * Sets a value to the session storage * * @param string $var The name of the variable * @param mixed $value The value to store */ protected function setToStorage($var, $value) { $session = $this->container->segment; $key = 'restore_' . $this->dbKey . '_' . $var; $session->$key = $value; } /** * Gets a database configuration variable as cached in the container * * @param string $key The name of the variable to get * @param mixed $default Default value (null if skipped) * * @return mixed The configuration variable's value */ protected function getParam($key, $default = null) { if (array_key_exists($key, $this->container['dbrestore'])) { return $this->container['dbrestore'][$key]; } else { return $default; } } protected function populatePartsMap() { // Nothing to do if it's already populated, right? if (!empty($this->partsMap)) { return; } // First, try to fetch from the session storage $this->totalSize = $this->getFromStorage('totalsize', 0); $this->runSize = $this->getFromStorage('runsize', 0); $this->partsMap = $this->getFromStorage('partsmap', array()); $this->currentPart = $this->getFromStorage('curpart', 0); $this->fileOffset = $this->getFromStorage('foffset', 0); $this->start = $this->getFromStorage('start', 0); $this->totalQueries = $this->getFromStorage('totalqueries', 0); // If that didn't work try a full initalisation if (empty($this->partsMap)) { if(!isset($this->container['dbrestore']['sqlfile'])) { throw new \RuntimeException('AWF_RESTORE_ERROR_NORESTOREFILEINCONTAINER', 500); } $sqlfile = $this->container['dbrestore']['sqlfile']; $parts = $this->getParam('parts', 1); $this->partsMap = array(); $path = $this->container->sqlPath; $this->totalSize = 0; $this->runSize = 0; $this->currentPart = 0; $this->fileOffset = 0; for ($index = 0; $index <= $parts; $index++) { if ($index == 0) { $basename = $sqlfile; } else { $basename = substr($sqlfile, 0, -4) . '.s' . sprintf('%02u', $index); } $file = $path . '/' . $basename; if (!file_exists($file)) { $file = 'sql/' . $basename; } $filesize = @filesize($file); $this->totalSize += intval($filesize); $this->partsMap[] = $file; } $this->setToStorage('totalsize', $this->totalSize); $this->setToStorage('runsize', $this->runSize); $this->setToStorage('partsmap', $this->partsMap); $this->setToStorage('curpart', $this->currentPart); $this->setToStorage('foffset', $this->fileOffset); $this->setToStorage('start', $this->start); $this->setToStorage('totalqueries', $this->totalQueries); } } /** * Proceeds to opening the next SQL part file * * @return bool True on success */ protected function getNextFile() { $parts = $this->getParam('parts', 1); if ($this->currentPart >= ($parts - 1)) { return false; } $this->currentPart++; $this->fileOffset = 0; $this->setToStorage('curpart', $this->currentPart); $this->setToStorage('foffset', $this->fileOffset); return $this->openFile(); } /** * Opens the SQL part file whose ID is specified in the $curpart variable * and updates the $file, $start and $foffset variables. * * @return boolean True on success * * @throws \Exception When an error occurs */ protected function openFile() { if (!is_numeric($this->currentPart)) { $this->currentPart = 0; } $this->filename = $this->partsMap[$this->currentPart]; if (!$this->file = @fopen($this->filename, "r")) { throw new \Exception(Text::sprintf('AWF_RESTORE_ERROR_CANTOPENDUMPFILE', $this->filename)); } else { // Get the file size if (fseek($this->file, 0, SEEK_END) == 0) { $this->fileSize = ftell($this->file); } else { throw new \Exception(Text::_('AWF_RESTORE_ERROR_UNKNOWNFILESIZE')); } } // Check start and foffset are numeric values if (!is_numeric($this->start) || !is_numeric($this->fileOffset)) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_INVALIDPARAMETERS')); } $this->start = floor($this->start); $this->fileOffset = floor($this->fileOffset); // Check $foffset upon $filesize if ($this->fileOffset > $this->fileSize) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_AFTEREOF')); } // Set file pointer to $foffset if (fseek($this->file, $this->fileOffset) != 0) { throw new \Exception(Text::_('AWF_RESTORE_ERROR_CANTSETOFFSET')); } return true; } /** * Returns the instance of the database driver, creating it if it doesn't * exist. * * @return Driver * * @throws \RuntimeException */ protected function getDatabase() { if (!is_object($this->db)) { $options = array( 'driver' => $this->container['dbrestore']['dbtype'], 'database' => $this->container['dbrestore']['dbname'], 'select' => 0, 'host' => $this->container['dbrestore']['dbhost'], 'user' => $this->container['dbrestore']['dbuser'], 'password' => $this->container['dbrestore']['dbpass'], 'prefix' => $this->container['dbrestore']['prefix'], ); $class = '\\Awf\\Database\\Driver\\' . ucfirst(strtolower($options['driver'])); $this->db = new $class($options); $this->db->setUTF(); } return $this->db; } /** * Executes a SQL statement, ignoring errors in the $allowedErrorCodes list. * * @param string $sql The SQL statement to execute * * @return mixed A database cursor on success, false on failure * * @throws \Exception On error */ protected function execute($sql) { $db = $this->getDatabase(); try { $db->setQuery($sql); $result = $db->execute(); } catch (\Exception $exc) { $result = false; if (!in_array($exc->getCode(), $this->allowedErrorCodes)) { // Format the error message and throw it again $message = '
' . Text::_('AWF_RESTORE_ERROR_MYSQLERROR') . '
' . "\n"; $message .= 'ErrNo #' . htmlspecialchars($exc->getCode()) . '' . "\n";
$message .= '' . htmlspecialchars($exc->getMessage()) . '' . "\n"; $message .= '
' . Text::_('AWF_RESTORE_ERROR_RAWQUERY') . '
' . "\n"; $message .= '' . htmlspecialchars($sql) . '' . "\n"; // Rethrow the exception if we're not supposed to handle it throw new \Exception($message); } } return $result; } /** * Read the next line from the database dump * * @return string The query string * * @throws \Exception */ protected function readNextLine() { $parts = $this->getParam('parts', 1); $query = ""; while (!feof($this->file) && (strpos($query, "\n") === false)) { $query .= fgets($this->file, DATA_CHUNK_LENGTH); } // An empty query is EOF. Are we done or should I skip to the next file? if (empty($query) || ($query === false)) { if ($this->currentPart >= ($parts - 1)) { throw new \Exception('All done', 200); } else { // Register the bytes read $current_foffset = @ftell($this->file); if (is_null($this->fileOffset)) { $this->fileOffset = 0; } $this->runSize = (is_null($this->runSize) ? 0 : $this->runSize) + ($current_foffset - $this->fileOffset); // Get the next file $this->getNextFile(); // Rerun the fetcher throw new \Exception('Continue', 201); } } if (substr($query, -1) != "\n") { // We read more data than we should. Roll back the file... $newLinePos = strpos($query, "\n"); if ($newLinePos !== false) { $queryLength = strlen($query); $rollback = $queryLength - $newLinePos; fseek($this->file, -$rollback, SEEK_CUR); // ...and chop the line $query = substr($query, 0, $rollback); } } // Handle DOS linebreaks $query = str_replace("\r\n", "\n", $query); $query = str_replace("\r", "\n", $query); // Skip comments and blank lines only if NOT in parentheses $skipline = false; reset($this->comment); foreach ($this->comment as $comment_value) { if (trim($query) == "" || strpos($query, $comment_value) === 0) { $skipline = true; break; } } if ($skipline) { $this->lineNumber++; throw new \Exception('Continue', 201); } $query = trim($query, " \n"); $query = rtrim($query, ';'); return $query; } /** * Runs a restoration step and returns an array to be used in the response. * * @return array * * @throws \Exception */ public function stepRestoration() { $parts = $this->getParam('parts', 1); $this->openFile(); $this->lineNumber = $this->start; $this->totalSizeRead = 0; $this->queries = 0; while ($this->timer->getTimeLeft() > 0) { $query = ''; // Get the next query line try { $query = $this->readNextLine(); } catch (\Exception $exc) { if ($exc->getCode() == 200) { break; } elseif ($exc->getCode() == 201) { continue; } } if (empty($query)) { continue; } // Update variables $this->totalSizeRead += strlen($query); $this->totalQueries++; $this->queries++; $this->lineNumber++; // Process the query line, running drop/rename queries as necessary $this->processQueryLine($query); } // Get the current file position $current_foffset = ftell($this->file); if ($current_foffset === false) { if (is_resource($this->file)) { @fclose($this->file); } throw new \Exception(Text::_('AWF_RESTORE_ERROR_CANTREADPOINTER')); } else { if (is_null($this->fileOffset)) { $this->fileOffset = 0; } $bytes_in_step = $current_foffset - $this->fileOffset; $this->runSize = (is_null($this->runSize) ? 0 : $this->runSize) + $bytes_in_step; $this->fileOffset = $current_foffset; } // Return statistics $bytes_togo = $this->totalSize - $this->runSize; // Check for global EOF if (($this->currentPart >= ($parts - 1)) && feof($this->file)) { $bytes_togo = 0; } // Save variables in storage $this->setToStorage('start', $this->start); $this->setToStorage('foffset', $this->fileOffset); $this->setToStorage('totalqueries', $this->totalQueries); $this->setToStorage('runsize', $this->runSize); if ($bytes_togo == 0) { // Clear stored variables if we're finished $this->removeInformationFromStorage(); } // Calculate estimated time $bytesPerSecond = $bytes_in_step / $this->timer->getRunningTime(); if ($bytesPerSecond <= 0.01) { $remainingSeconds = 120; } else { $remainingSeconds = round($bytes_togo / $bytesPerSecond, 0); } // Return meaningful data return array( 'percent' => round(100 * ($this->runSize / $this->totalSize), 1), 'restored' => $this->sizeformat($this->runSize), 'total' => $this->sizeformat($this->totalSize), 'queries_restored' => $this->totalQueries, 'current_line' => $this->lineNumber, 'current_part' => $this->currentPart, 'total_parts' => $parts, 'eta' => $this->etaformat($remainingSeconds), 'error' => '', 'done' => ($bytes_togo == 0) ? '1' : '0' ); } /** * Processes the query line in the best way each restoration engine sees * fit. This method is supposed to take care of backing up and dropping * tables, changing table collation if requested and converting INSERT to * REPLACE if requested. It is also supposed to execute $query against the * database, replacing the metaprefix #__ with the real prefix. * * @param string $query * * @return string The processed query */ abstract protected function processQueryLine($query); /** * Format a raw time in seconds as a human readable string * * @param integer $Raw Time in seconds * @param string $measureby Unit of measurement, leave blank to auto-detect * * @return string Human readable time string */ private function etaformat($Raw, $measureby = '') { $Clean = abs($Raw); $calcNum = array( array('s', 60), array('m', 60 * 60), array('h', 60 * 60 * 60), array('d', 60 * 60 * 60 * 24), array('y', 60 * 60 * 60 * 24 * 365) ); $calc = array( 's' => array(1, 'second'), 'm' => array(60, 'minute'), 'h' => array(60 * 60, 'hour'), 'd' => array(60 * 60 * 24, 'day'), 'y' => array(60 * 60 * 24 * 365, 'year') ); if ($measureby == '') { $usemeasure = 's'; for ($i = 0; $i < count($calcNum); $i++) { if ($Clean <= $calcNum[$i][1]) { $usemeasure = $calcNum[$i][0]; $i = count($calcNum); } } } else { $usemeasure = $measureby; } $datedifference = floor($Clean / $calc[$usemeasure][0]); if ($datedifference == 1) { return $datedifference . ' ' . $calc[$usemeasure][1]; } else { return $datedifference . ' ' . $calc[$usemeasure][1] . 's'; } } /** * Returns the cached total size of the SQL dump. * * @param boolean $use_units Should I automatically figure out the unit of measurement * * @return string */ public function getTotalSize($use_units = false) { $size = $this->totalSize; if ($use_units) { $size = $this->sizeformat($size); } return $size; } /** * Format a size in bytes into a human readable format * * @param string $size The size in bytes * * @return string The human readable size string */ private function sizeformat($size) { if ($size < 0) { return 0; } $unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB'); $i = floor(log($size, 1024)); if (($i < 0) || ($i > 5)) { $i = 0; } return @round($size / pow(1024, ($i)), 2) . ' ' . $unit[$i]; } }