%PDF- %PDF-
| Direktori : /home1/lightco1/www/administrator/components/com_akeeba/BackupEngine/Dump/ |
| Current File : //home1/lightco1/www/administrator/components/com_akeeba/BackupEngine/Dump/Base.php |
<?php
/**
* Akeeba Engine
* The modular PHP5 site backup engine
* @copyright Copyright (c)2006-2017 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or, at your option, any later version
* @package akeebaengine
*
*/
namespace Akeeba\Engine\Dump;
// Protection against direct access
defined('AKEEBAENGINE') or die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Domain\Pack;
use Akeeba\Engine\Driver\Base as DriverBase;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Psr\Log\LogLevel;
abstract class Base extends Part
{
// **********************************************************************
// Configuration parameters
// **********************************************************************
/** @var string Prefix to this database */
protected $prefix = '';
/** @var string MySQL database server host name or IP address */
protected $host = '';
/** @var string MySQL database server port (optional) */
protected $port = '';
/** @var string MySQL user name, for authentication */
protected $username = '';
/** @var string MySQL password, for authentication */
protected $password = '';
/** @var string MySQL database */
protected $database = '';
/** @var string The database driver to use */
protected $driver = '';
/** @var boolean Should I post process quoted values */
protected $postProcessValues = false;
// **********************************************************************
// File handling fields
// **********************************************************************
/** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */
protected $dumpFile = '';
/** @var string Data cache, used to cache data before being written to disk */
protected $data_cache = '';
/** @var int */
protected $largest_query = 0;
/** @var int Size of the data cache, default 128Kb */
protected $cache_size = 131072;
/** @var bool Should I process empty prefixes when creating abstracted names? */
protected $processEmptyPrefix = true;
/** @var int Current dump file part number */
public $partNumber = 0;
/** @var resource Filepointer to the current dump part */
private $fp = null;
/** @var string Absolute path to the temp file */
protected $tempFile = '';
/** @var string Relative path of how the file should be saved in the archive */
protected $saveAsName = '';
// **********************************************************************
// Protected fields (data handling)
// **********************************************************************
/** @var array Contains the sorted (by dependencies) list of tables/views to backup */
protected $tables = array();
/** @var array Contains the configuration data of the tables */
protected $tables_data = array();
/** @var array Maps database table names to their abstracted format */
protected $table_name_map = array();
/** @var array Contains the dependencies of tables and views (temporary) */
protected $dependencies = array();
/** @var string The next table to backup */
protected $nextTable;
/** @var integer The next row of the table to start backing up from */
protected $nextRange;
/** @var integer Current table's row count */
protected $maxRange;
/** @var bool Use extended INSERTs */
protected $extendedInserts = false;
/** @var integer Maximum packet size for extended INSERTs, in bytes */
protected $packetSize = 0;
/** @var string Extended INSERT query, while it's being constructed */
protected $query = '';
/** @var int Dump part's maximum size */
protected $partSize = 0;
/**
* Find where to store the backup files
*
* @param $partNumber int The SQL part number, default is 0 (.sql)
*/
protected function getBackupFilePaths($partNumber = 0)
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Getting temporary file");
$this->tempFile = Factory::getTempFiles()->registerTempFile(dechex(crc32(microtime())) . '.sql');
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Temporary file is {$this->tempFile}");
// Get the base name of the dump file
$partNumber = intval($partNumber);
$baseName = $this->dumpFile;
if ($partNumber > 0)
{
// The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc
if (strtolower(substr($baseName, -4)) == '.sql')
{
$baseName = substr($baseName, 0, -4) . '.s' . sprintf('%02u', $partNumber);
}
else
{
$baseName = $baseName . '.s' . sprintf('%02u', $partNumber);
}
}
if (empty($this->installerSettings))
{
// Fetch the installer settings
$this->installerSettings = (object)array(
'installerroot' => 'installation',
'sqlroot' => 'installation/sql',
'databasesini' => 1,
'readme' => 1,
'extrainfo' => 1
);
$config = Factory::getConfiguration();
$installerKey = $config->get('akeeba.advanced.embedded_installer');
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
if (array_key_exists($installerKey, $installerDescriptors))
{
// The selected installer exists, use it
$this->installerSettings = (object)$installerDescriptors[$installerKey];
}
elseif (array_key_exists('angie', $installerDescriptors))
{
// The selected installer doesn't exist, but ANGIE exists; use that instead
$this->installerSettings = (object)$installerDescriptors['angie'];
}
}
switch (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal'))
{
case 'output':
// The SQL file will be stored uncompressed in the output directory
$statistics = Factory::getStatistics();
$statRecord = $statistics->getRecord();
$this->saveAsName = $statRecord['absolute_path'];
break;
case 'normal':
// The SQL file will be stored in the SQL root of the archive, as
// specified by the particular embedded installer's settings
$this->saveAsName = $this->installerSettings->sqlroot . '/' . $baseName;
break;
case 'short':
// The SQL file will be stored on archive's root
$this->saveAsName = $baseName;
break;
}
if ($partNumber > 0)
{
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
}
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
}
/**
* Deletes any leftover files from previous backup attempts
*
*/
protected function removeOldFiles()
{
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: Deleting leftover files, if any");
if (file_exists($this->tempFile))
{
@unlink($this->tempFile);
}
}
/**
* Populates the table arrays with the information for the db entities to backup
*
* @return null
*/
protected abstract function getTablesToBackup();
/**
* Runs a step of the database dump
*
* @return null
*/
protected abstract function stepDatabaseDump();
/**
* Implements the _prepare abstract method
*
*/
protected function _prepare()
{
$this->setStep('Initialization');
$this->setSubstep('');
// Process parameters, passed to us using the setup() public method
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processing parameters");
if (is_array($this->_parametersArray))
{
$this->driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : $this->driver;
$this->host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : $this->host;
$this->port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : $this->port;
$this->username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : $this->username;
$this->username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $this->username;
$this->password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : $this->password;
$this->database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : $this->database;
$this->prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : $this->prefix;
$this->dumpFile = array_key_exists('dumpFile', $this->_parametersArray) ? $this->_parametersArray['dumpFile'] : $this->dumpFile;
$this->processEmptyPrefix = array_key_exists('process_empty_prefix', $this->_parametersArray) ? $this->_parametersArray['process_empty_prefix'] : $this->processEmptyPrefix;
}
// Make sure we have self-assigned the first part
$this->partNumber = 0;
// Get DB backup only mode
$configuration = Factory::getConfiguration();
// Find tables to be included and put them in the $_tables variable
$this->getTablesToBackup();
if ($this->getError())
{
return;
}
// Find where to store the database backup files
$this->getBackupFilePaths($this->partNumber);
// Remove any leftovers
$this->removeOldFiles();
// Initialize the extended INSERTs feature
$this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0);
$this->packetSize = $configuration->get('engine.dump.common.packet_size', 0);
if ($this->packetSize == 0)
{
$this->extendedInserts = false;
}
// Initialize the split dump feature
$this->partSize = $configuration->get('engine.dump.common.splitsize', 1048576);
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
{
$this->partSize = 0;
}
if (($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize))
{
$this->packetSize = $this->partSize / 2;
}
// Initialize the algorithm
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Initializing algorithm for first run");
$this->nextTable = array_shift($this->tables);
// If there is no table to back up we are done with the database backup
if (empty($this->nextTable))
{
$this->setState('postrun');
return;
}
$this->nextRange = 0;
$this->query = '';
// FIX 2.2: First table of extra databases was not being written to disk.
// This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in
// _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's
// definition and first chunk of its data) were not written to disk. This call causes $fp to be
// nullified, causing it to be recreated, pointing to the correct file.
$null = null;
$this->writeline($null);
// Finally, mark ourselves "prepared".
$this->setState('prepared');
}
/**
* Implements the _run() abstract method
*/
protected function _run()
{
// Check if we are already done
if ($this->getState() == 'postrun')
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
$this->setStep("");
$this->setSubstep("");
return;
}
// Mark ourselves as still running (we will test if we actually do towards the end ;) )
$this->setState('running');
/**
* Resume packing / post-processing part files if necessary.
*
* @see \Akeeba\Engine\Archiver\BaseArchiver::putDataFromFileIntoArchive
*
* Sometimes the SQL part file may be bigger than the big file threshold (engine.archiver.common.
* big_file_threshold). In this case when we try to add it to the backup archive the archiver engine figures
* out it has to be added uncompressed, one chunk (engine.archiver.common.chunk_size) bytes at a time. This
* happens in a loop. We read a chunk, push it to the archive, rinse and repeat.
*
* There are two cases when we might break the loop:
*
* 1. Not enough free space in the backup archive part and engine.postproc.common.after_part (immediate post-
* processing) is enabled. We break the step to let the backup part be post-processed.
*
* 2. We ran out of time copying data.
*
* The following if-blocks deal with these two cases.
*/
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
{
$archiver = Factory::getArchiverEngine();
$configuration = Factory::getConfiguration();
// Case 1. Immediate post-processing was triggered in the previous step
if ($configuration->get('engine.postproc.common.after_part', 0) && !empty($archiver->finishedPart))
{
if (Pack::postProcessDonePartFile($this, $archiver, $configuration))
{
return;
}
}
// We had already started archiving the db file, but it needs more time
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
/**
* We MUST NOT try to continue adding the file to the backup archive manually. Instead, we have to go
* through getNextDumpPart. This method will continue adding the part to the backup archive and when
* this is done it will remove the file and create a new one.
*
* If that method returns false it means that we either hit an error or the archiver didn't have enough
* time to add the part to the backup archive. In either case we have to return and let the Engine step.
*/
if ($this->getNextDumpPart() === false)
{
return;
}
}
}
$this->stepDatabaseDump();
$null = null;
$this->writeline($null);
}
/**
* Implements the _finalize() abstract method
*
*/
protected function _finalize()
{
Factory::getLog()->log(LogLevel::DEBUG, "Adding any extra SQL statements imposed by the filters");
$filters = Factory::getFilters();
$this->writeline($filters->getExtraSQL($this->databaseRoot));
// Close the file pointer (otherwise the SQL file is left behind)
$this->closeFile();
// If we are not just doing a main db only backup, add the SQL file to the archive
$finished = true;
$configuration = Factory::getConfiguration();
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
{
$archiver = Factory::getArchiverEngine();
$configuration = Factory::getConfiguration();
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
// We had already started archiving the db file, but it needs more time
Factory::getLog()->log(LogLevel::DEBUG, "Continuing adding the SQL dump to the archive");
$archiver->addFile(null, null, null);
$this->propagateFromObject($archiver);
if ($this->getError())
{
return;
}
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
}
else
{
// We have to add the dump file to the archive
Factory::getLog()->log(LogLevel::DEBUG, "Adding the final SQL dump to the archive");
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
$this->propagateFromObject($archiver);
if ($this->getError())
{
return;
}
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
}
}
else
{
// We just have to move the dump file to its final destination
Factory::getLog()->log(LogLevel::DEBUG, "Moving the SQL dump to its final location");
$result = Platform::getInstance()->move($this->tempFile, $this->saveAsName);
if (!$result)
{
$this->setError('Could not move the SQL dump to its final location');
}
}
// Make sure that if the archiver needs more time to process the file we can supply it
if ($finished)
{
Factory::getLog()->log(LogLevel::DEBUG, "Removing temporary file of final SQL dump");
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
if ($this->getError())
{
return;
}
$this->setState('finished');
}
}
/**
* Creates a new dump part
*/
protected function getNextDumpPart()
{
// On database dump only mode we mustn't create part files!
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
{
return false;
}
// Is the archiver still processing?
$configuration = Factory::getConfiguration();
$archiver = Factory::getArchiverEngine();
$stillProcessing = $configuration->get('volatile.engine.archiver.processingfile', false);
if ($stillProcessing)
{
/**
* The archiver is still adding the previous dump part. This means that we are called from the top few lines
* of the _run method. We must continue adding the previous dump part.
*/
Factory::getLog()->log(LogLevel::DEBUG, "Continuing adding the SQL dump part to the archive");
$result = $archiver->addFile('', '', '');
}
else
{
/**
* There is no other dump part being processed. Therefore the current SQL dump part is still open. We must
* close it and ask the archiver to add it to the backup archive.
*/
$this->closeFile();
Factory::getLog()->log(LogLevel::DEBUG, "Adding the SQL dump part to the archive");
$result = $archiver->addFileRenamed($this->tempFile, $this->saveAsName);
}
// Propagate errors and warnings from the archiver
$this->propagateFromObject($archiver);
// Did the archiver return an error?
if ($this->getError())
{
return false;
}
// Return false if the file didn't finish getting added to the archive
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
Factory::getLog()->debug("The SQL dump file has not been processed thoroughly by the archiver. Resuming in the next step.");
return false;
}
/**
* If you are here the SQL dump part file is completely added to the backup archive. All we have to do now is
* remove it and create a new dump part file.
*/
// Remove the old file
Factory::getLog()->log(LogLevel::DEBUG, "Removing dump part's temporary file");
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
// Create the new dump part
$this->partNumber++;
$this->getBackupFilePaths($this->partNumber);
$null = null;
$this->writeline($null);
return true;
}
/**
* Creates a new dump part, but only if required to do so
*
* @return bool
*/
protected function createNewPartIfRequired()
{
if ($this->partSize == 0)
{
return true;
}
$filesize = 0;
if (@file_exists($this->tempFile))
{
$filesize = @filesize($this->tempFile);
}
$projectedSize = $filesize + strlen($this->query);
if ($this->extendedInserts)
{
$projectedSize = $filesize + $this->packetSize;
}
if ($projectedSize > $this->partSize)
{
return $this->getNextDumpPart();
}
return true;
}
/**
* Returns a table's abstract name (replacing the prefix with the magic #__ string)
*
* @param string $tableName The canonical name, e.g. 'jos_content'
*
* @return string The abstract name, e.g. '#__content'
*/
protected function getAbstract($tableName)
{
// Don't return abstract names for non-CMS tables
if (is_null($this->prefix))
{
return $tableName;
}
switch ($this->prefix)
{
case '':
if ($this->processEmptyPrefix)
{
// This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty.
return '#__' . $tableName;
}
else
{
// If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't
// assume anything.
return $tableName;
}
break;
default:
// Normal behaviour for 99% of sites
// Fix 2.4 : Abstracting the prefix only if it's found in the beginning of the table name
$tableAbstract = $tableName;
if (!empty($this->prefix))
{
if (substr($tableName, 0, strlen($this->prefix)) == $this->prefix)
{
$tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
}
else
{
// FIX 2.4: If there is no prefix, it's a non-core table.
$tableAbstract = $tableName;
}
}
return $tableAbstract;
break;
}
}
/**
* Writes the SQL dump into the output files. If it fails, it sets the error
*
* @param string $data Data to write to the dump file. Pass NULL to force flushing to file.
*
* @return boolean TRUE on successful write, FALSE otherwise
*/
protected function writeDump(&$data)
{
if (!empty($data))
{
$this->data_cache .= $data;
if (strlen($data) > $this->largest_query)
{
$this->largest_query = strlen($data);
Factory::getConfiguration()->set('volatile.database.largest_query', $this->largest_query);
}
}
if ((strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache))))
{
Factory::getLog()->log(LogLevel::DEBUG, "Writing " . strlen($this->data_cache) . " bytes to the dump file");
$result = $this->writeline($this->data_cache);
if (!$result)
{
$errorMessage = 'Couldn\'t write to the SQL dump file ' . $this->tempFile . '; check the temporary directory permissions and make sure you have enough disk space available.';
$this->setError($errorMessage);
return false;
}
$this->data_cache = '';
}
return true;
}
/**
* Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving
* failed, return value is FALSE.
*
* @param string $fileData Data to write. Set to null to close the file handle.
*
* @return boolean TRUE is saving to the file succeeded
*/
protected function writeline(&$fileData)
{
if (!is_resource($this->fp))
{
$this->fp = @fopen($this->tempFile, 'a');
if ($this->fp === false)
{
$this->setError('Could not open ' . $this->tempFile . ' for append, in DB dump.');
return;
}
}
if (is_null($fileData))
{
if (is_resource($this->fp))
{
@fclose($this->fp);
}
$this->fp = null;
return true;
}
else
{
if ($this->fp)
{
$ret = fwrite($this->fp, $fileData);
@clearstatcache();
// Make sure that all data was written to disk
return ($ret == strlen($fileData));
}
else
{
return false;
}
}
}
function _onSerialize()
{
$this->closeFile();
}
function __destruct()
{
$this->closeFile();
}
public function closeFile()
{
if (is_resource($this->fp))
{
Factory::getLog()->log(LogLevel::DEBUG, "Closing SQL dump file.");
@fclose($this->fp);
$this->fp = null;
}
}
/**
* Return an instance of DriverBase
*
* @return DriverBase
*/
protected function &getDB()
{
$host = $this->host . ($this->port != '' ? ':' . $this->port : '');
$user = $this->username;
$password = $this->password;
$driver = $this->driver;
$database = $this->database;
$prefix = is_null($this->prefix) ? '' : $this->prefix;
$options = array('driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'prefix' => $prefix);
$db = Factory::getDatabase($options);
if ($error = $db->getError())
{
$this->setError(__CLASS__ . ' :: Database Error: ' . $error);
$false = false;
return $false;
}
if ($db->getErrorNum() > 0)
{
$error = $db->getErrorMsg();
$this->setError(__CLASS__ . ' :: Database Error: ' . $error);
$false = false;
return $false;
}
return $db;
}
/**
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
*
* @return string
*/
abstract protected function getDatabaseNameFromConnection();
/**
* Returns the database name. If the name was not declared when the object was created we will go through the
* getDatabaseNameFromConnection method to populate it.
*
* @return string
*/
protected function getDatabaseName()
{
if (empty($this->database))
{
$this->database = $this->getDatabaseNameFromConnection();
}
return $this->database;
}
public function callStage($stage)
{
switch ($stage)
{
case '_prepare':
return $this->_prepare();
break;
case '_run':
return $this->_run();
break;
case '_finalize':
return $this->_finalize();
break;
}
}
/**
* Post process a quoted value before it's written to the database dump.
* So far it's only required for SQL Server which has a problem escaping
* newline characters...
*
* @param string $value The quoted value to post-process
*
* @return string
*/
protected function postProcessQuotedValue($value)
{
return $value;
}
/**
* Returns a preamble for the data dump portion of the SQL backup. This is
* used to output commands before the first INSERT INTO statement for a
* table when outputting a plain SQL file.
*
* Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server
*
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
* @param string $tableName Real name of the table, e.g. abc_foobar
* @param integer $maxRange Row count on this table
*
* @return string The SQL commands you want to be written in the dump file
*/
protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange)
{
return '';
}
/**
* Returns an epilogue for the data dump portion of the SQL backup. This is
* used to output commands after the last INSERT INTO statement for a
* table when outputting a plain SQL file.
*
* Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server
*
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
* @param string $tableName Real name of the table, e.g. abc_foobar
* @param integer $maxRange Row count on this table
*
* @return string The SQL commands you want to be written in the dump file
*/
protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange)
{
return '';
}
/**
* Return a list of field names for the INSERT INTO statements. This is only
* required for Microsoft SQL Server because without it the SET IDENTITY_INSERT
* has no effect.
*
* @param array $fieldNames A list of field names in array format
* @param integer $numOfFields The number of fields we should be dumping
*
* @return string
*/
protected function getFieldListSQL($fieldNames, $numOfFields)
{
return '';
}
}