987 lines
28 KiB
PHP
987 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* Akeeba Engine
|
|
*
|
|
* @package akeebaengine
|
|
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace Akeeba\Engine\Dump;
|
|
|
|
defined('AKEEBAENGINE') || 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 Akeeba\Engine\Util\FileCloseAware;
|
|
use Exception;
|
|
use RuntimeException;
|
|
|
|
abstract class Base extends Part
|
|
{
|
|
use FileCloseAware;
|
|
|
|
// **********************************************************************
|
|
// Configuration parameters
|
|
// **********************************************************************
|
|
|
|
/** @var int Current dump file part number */
|
|
public $partNumber = 0;
|
|
|
|
/** @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 = '';
|
|
|
|
// **********************************************************************
|
|
// File handling fields
|
|
// **********************************************************************
|
|
/** @var boolean Should I post process quoted values */
|
|
protected $postProcessValues = false;
|
|
|
|
/** @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 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 = '';
|
|
|
|
/** @var array Contains the sorted (by dependencies) list of tables/views to backup */
|
|
protected $tables = [];
|
|
|
|
// **********************************************************************
|
|
// Protected fields (data handling)
|
|
// **********************************************************************
|
|
/** @var array Contains the configuration data of the tables */
|
|
protected $tables_data = [];
|
|
|
|
/** @var array Maps database table names to their abstracted format */
|
|
protected $table_name_map = [];
|
|
|
|
/** @var array Contains the dependencies of tables and views (temporary) */
|
|
protected $dependencies = [];
|
|
|
|
/** @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;
|
|
|
|
/** @var resource Filepointer to the current dump part */
|
|
private $fp = null;
|
|
|
|
/**
|
|
* Should I be using an abstract prefix (#__) for table names?
|
|
*
|
|
* @var bool
|
|
* @since 9.1.0
|
|
*/
|
|
private $useAbstractPrefix;
|
|
|
|
/**
|
|
* Public constructor.
|
|
*
|
|
* @return void
|
|
* @since 9.1.0
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->useAbstractPrefix =
|
|
Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') !== 'output';
|
|
|
|
parent::__construct();
|
|
}
|
|
|
|
|
|
/**
|
|
* This method is called when the factory is being serialized and is used to perform necessary cleanup steps.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function _onSerialize()
|
|
{
|
|
$this->closeFile();
|
|
}
|
|
|
|
/**
|
|
* This method is called when the object is destroyed and is used to perform necessary cleanup steps.
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
$this->closeFile();
|
|
}
|
|
|
|
/**
|
|
* Close any open SQL dump (output) file.
|
|
*/
|
|
public function closeFile()
|
|
{
|
|
if (!is_resource($this->fp))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Factory::getLog()->debug("Closing SQL dump file.");
|
|
|
|
$this->conditionalFileClose($this->fp);
|
|
$this->fp = null;
|
|
}
|
|
|
|
/**
|
|
* Call a specific stage of the dump engine
|
|
*
|
|
* @param string $stage
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function callStage($stage)
|
|
{
|
|
switch ($stage)
|
|
{
|
|
case '_prepare':
|
|
$this->_prepare();
|
|
break;
|
|
|
|
case '_run':
|
|
$this->_run();
|
|
break;
|
|
|
|
case '_finalize':
|
|
$this->_finalize();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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()->debug(__CLASS__ . " :: Getting temporary file");
|
|
$this->tempFile = Factory::getTempFiles()->registerTempFile(dechex(crc32(microtime())) . '.sql');
|
|
Factory::getLog()->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) [
|
|
'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()->debug("AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
|
|
}
|
|
|
|
Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
|
|
Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
|
|
}
|
|
|
|
/**
|
|
* Deletes any leftover files from previous backup attempts
|
|
*
|
|
*/
|
|
protected function removeOldFiles()
|
|
{
|
|
Factory::getLog()->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
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
protected function _prepare()
|
|
{
|
|
$this->setStep('Initialization');
|
|
$this->setSubstep('');
|
|
|
|
// Process parameters, passed to us using the setup() public method
|
|
Factory::getLog()->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();
|
|
|
|
// 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 = (int) $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 = floor($this->partSize / 2);
|
|
}
|
|
|
|
// Initialize the algorithm
|
|
Factory::getLog()->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(self::STATE_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(self::STATE_PREPARED);
|
|
}
|
|
|
|
/**
|
|
* Implements the _run() abstract method
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
protected function _run()
|
|
{
|
|
// Check if we are already done
|
|
if ($this->getState() == self::STATE_POSTRUN)
|
|
{
|
|
Factory::getLog()->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(self::STATE_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();
|
|
|
|
// Check whether we need to immediately post-processing a done part
|
|
if (Pack::postProcessDonePartFile($archiver, $configuration))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We had already started putting the DB dump file into the archive 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
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
protected function _finalize()
|
|
{
|
|
Factory::getLog()->debug("Adding any extra SQL statements imposed by the filters");
|
|
|
|
foreach (Factory::getFilters()->getExtraSQL($this->databaseRoot) as $sqlStatement)
|
|
{
|
|
$sqlStatement = trim($sqlStatement) . "\n";
|
|
|
|
$this->writeDump($sqlStatement, true);
|
|
}
|
|
|
|
// We need this to write out the cached extra SQL statements before closing the file.
|
|
$this->writeDump(null);
|
|
|
|
// 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;
|
|
|
|
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()->debug("Continuing adding the SQL dump to the archive");
|
|
$archiver->addFile(null, null, null);
|
|
|
|
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
|
|
}
|
|
else
|
|
{
|
|
// We have to add the dump file to the archive
|
|
Factory::getLog()->debug("Adding the final SQL dump to the archive");
|
|
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
|
|
|
|
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We just have to move the dump file to its final destination
|
|
Factory::getLog()->debug("Moving the SQL dump to its final location");
|
|
$result = Platform::getInstance()->move($this->tempFile, $this->saveAsName);
|
|
|
|
if (!$result)
|
|
{
|
|
Factory::getLog()->debug("Removing temporary file of final SQL dump");
|
|
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
|
|
|
|
throw new RuntimeException('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()->debug("Removing temporary file of final SQL dump");
|
|
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
|
|
|
|
$this->setState(self::STATE_FINISHED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new dump part
|
|
*
|
|
* @return bool
|
|
* @throws Exception
|
|
*/
|
|
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()->debug("Continuing adding the SQL dump part to the archive");
|
|
$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()->debug("Adding the SQL dump part to the archive");
|
|
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
|
|
}
|
|
|
|
// 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()->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
|
|
* @throws Exception
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// 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. Start by assuming the table has no prefix, therefore is non-core.
|
|
$tableAbstract = $tableName;
|
|
|
|
// If there's a prefix use the abstract name
|
|
if (!empty($this->prefix) && (substr($tableName, 0, strlen($this->prefix)) == $this->prefix))
|
|
{
|
|
$tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
|
|
}
|
|
|
|
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.
|
|
* @param bool $addMarker Should I prefix the data with a marker?
|
|
*
|
|
* @return boolean TRUE on successful write, FALSE otherwise
|
|
* @throws Exception
|
|
*/
|
|
protected function writeDump($data, $addMarker = false)
|
|
{
|
|
if (!empty($data))
|
|
{
|
|
if ($addMarker && $this->useAbstractPrefix)
|
|
{
|
|
$this->data_cache .= '/**ABDB**/';
|
|
}
|
|
elseif (!$this->useAbstractPrefix)
|
|
{
|
|
// Replace #__ with the prefix when writing plain .sql files
|
|
$db = $this->getDB();
|
|
$data = $db->replacePrefix($data) . "\n";
|
|
}
|
|
|
|
$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()->debug("Writing " . strlen($this->data_cache) . " bytes to the dump file");
|
|
$result = $this->writeline($this->data_cache);
|
|
|
|
if (!$result)
|
|
{
|
|
$errorMessage = sprintf('Couldn\'t write to the SQL dump file %s; check the temporary directory permissions and make sure you have enough disk space available.', $this->tempFile);
|
|
throw new RuntimeException($errorMessage);
|
|
}
|
|
|
|
$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
|
|
* @throws Exception
|
|
*/
|
|
protected function writeline(&$fileData)
|
|
{
|
|
if (!is_resource($this->fp))
|
|
{
|
|
$this->fp = @fopen($this->tempFile, 'a');
|
|
|
|
if ($this->fp === false)
|
|
{
|
|
throw new RuntimeException('Could not open ' . $this->tempFile . ' for append, in DB dump.');
|
|
}
|
|
}
|
|
|
|
if (is_null($fileData))
|
|
{
|
|
$this->conditionalFileClose($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));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an instance of DriverBase
|
|
*
|
|
* @return DriverBase|bool
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
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 = [
|
|
'driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password,
|
|
'database' => $database, 'prefix' => $prefix,
|
|
];
|
|
|
|
$db = Factory::getDatabase($options);
|
|
|
|
if ($db->getErrorNum() > 0)
|
|
{
|
|
$error = $db->getErrorMsg();
|
|
|
|
throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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|string $fieldNames A list of field names in array format or '*' if it's all fields
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected function getFieldListSQL($fieldNames)
|
|
{
|
|
// If we get a literal '*' we dumped all columns so we don't need to add column names in the INSERT.
|
|
if ($fieldNames === '*')
|
|
{
|
|
return '';
|
|
}
|
|
|
|
return '(' . implode(', ', array_map([$this->getDB(), 'qn'], $fieldNames)) . ')';
|
|
}
|
|
|
|
/**
|
|
* Return a list of columns to use in the SELECT query for dumping table data.
|
|
*
|
|
* This is used to filter out all generated rows.
|
|
*
|
|
* @param string $tableAbstract
|
|
*
|
|
* @return string|array An array of table columns or the string literal '*' to quickly select all columns.
|
|
*
|
|
* @see https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
|
|
*/
|
|
protected function getSelectColumns($tableAbstract)
|
|
{
|
|
return '*';
|
|
}
|
|
|
|
/**
|
|
* Converts a human formatted size to integer representation of bytes,
|
|
* e.g. 1M to 1024768
|
|
*
|
|
* @param string $setting The value in human readable format, e.g. "1M"
|
|
*
|
|
* @return integer The value in bytes
|
|
*/
|
|
protected function humanToIntegerBytes($setting)
|
|
{
|
|
$val = trim($setting);
|
|
$last = strtolower($val[strlen($val) - 1]);
|
|
|
|
if (is_numeric($last))
|
|
{
|
|
return $setting;
|
|
}
|
|
|
|
switch ($last)
|
|
{
|
|
case 't':
|
|
$val *= 1024;
|
|
case 'g':
|
|
$val *= 1024;
|
|
case 'm':
|
|
$val *= 1024;
|
|
case 'k':
|
|
$val *= 1024;
|
|
}
|
|
|
|
return (int) $val;
|
|
}
|
|
|
|
/**
|
|
* Get the PHP memory limit in bytes
|
|
*
|
|
* @return int|null Memory limit in bytes or null if we can't figure it out.
|
|
*/
|
|
protected function getMemoryLimit()
|
|
{
|
|
if (!function_exists('ini_get'))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$memLimit = ini_get("memory_limit");
|
|
|
|
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
|
|
{
|
|
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
|
|
}
|
|
|
|
$memLimit = $this->humanToIntegerBytes($memLimit);
|
|
|
|
return $memLimit;
|
|
}
|
|
}
|