#!/usr/bin/php * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. */ require_once ('DB.php'); require_once ('Console/Getopt.php'); define ('CONFIG_FILE', 'synchro_data.ini'); define ('FLAG_BASE_FILENAME', 'EGGS_Synchro_LOCK'); define ('MONITORING_BASE_FILENAME', 'monitoring'); /*************************************************************************/ /*************************** Class definition ****************************/ /*************************************************************************/ /** * Command line options management class * * This class manage command line options. * * @author Emmanuel Saracco */ class EGGS_GetOpt extends Console_Getopt { /** * All accepted command line options in short format. * * To add a script command line option, just add a entry here. * * Type: array */ private $optionsAll = array ( /* synchroIni */ 'synchro-ini' => array ( 'shortName' => 's', 'mandatory' => true, 'description' => "a alternative synchro_data.ini file"), /* dryRun */ 'dry-run' => array ( 'shortName' => 'n', 'mandatory' => false, 'description' => "dry-run mode"), /* verbose */ 'verbose' => array ( 'shortName' => 'v', 'mandatory' => false, 'description' => "verbose mode"), /* section */ 'section' => array ( 'shortName' => 'i', 'mandatory' => true, 'description' => "synchronize this section (must exists in .ini)"), /* monitoringFile */ 'monitoring-file' => array ( 'shortName' => 'm', 'mandatory' => true, 'description' => "alternate monitoring filename"), /* help */ 'help' => array ( 'shortName' => 'h', 'mandatory' => false, 'description' => "this help") ); /** * All accepted command line options in long format * * This array is initialized a class instanciation from the short options * array. * * Type: array */ private $optLong = array (); /** * All command line options given to the script * * Type: array */ private $options = array (); /** * Class constructor */ function __construct () { $this->optLong = array (); $optLong = array (); $optShort = array (); $optShortStr = ''; /* Build needed data from main definition array options */ foreach ($this->optionsAll as $k => $v) { $this->optLong[$k] = $v['shortName']; $optShort[$v['shortName']] = $k; $optShortStr .= $v['shortName'] . (($v['mandatory']) ? ':' : ''); $optLongStr = $k . (($v['mandatory']) ? '=' : ''); array_push ($optLong, $optLongStr); } $args = self::readPHPArgv (); array_shift ($args); $opts = self::getOpt ($args, $optShortStr, $optLong); /* Check for invalid options */ if (PEAR::isError ($opts)) $this->usage (); foreach ($opts[0] as $option) { $tmp = ereg_replace ('^\-\-', '', $option[0]); $name = (isset ($this->optLong[$tmp])) ? $tmp : $optShort[$tmp]; $this->options[$name] = $option[1]; } if ($this->help) $this->usage (); } /** * Class destructor */ function __destruct () {} /** * GET Accessor for this class * * Return the requested command line option value or null value if the * requested option does not exists. * * @author Emmanuel Saracco * * @retval mixed The requested value or null */ function __GET ($name) { $name = strtolower (preg_replace ('#([A-Z])#', '-\\1', $name)); if (!array_key_exists ($name, $this->options)) return null; return ($this->options[$name] != '') ? $this->options[$name] : true; } /** * Display help and exit * * Display help and exit the script * * @author Emmanuel Saracco * * @retval void */ private function usage () { print "\nUsage: synchro_obs_data.php [options]\n\n"; foreach ($this->optionsAll as $k => $v) { print "\t-" . $v['shortName'] . ', --' . $k . ((!$v['mandatory']) ? ' [optional] ' : ', ') . $v['description'] . "\n"; } print "\n\n"; exit (0); } } /** * Synchronization class * * This class manage synchronization between master server and others * servers. * * @author Emmanuel Saracco */ class EGGS_Synchro { /** * Name of the section (slave) currently synchronized * * Type: string */ private $currentSection = ''; /** * Command line options for this script * * Type: EGGS_GetOpt */ private $optionsCLI = null; /** * Full config file data array * * Type: array */ private $config = array (); /** * Connexion object for slaves databases * * Type: array */ private $slavesCnx = array (); /** * Connexion object for master database * * Type: object */ private $masterCnx = array ( 'cnx' => null, 'dsn' => '' ); /** * Options for DB connection method * * Type: array */ private $dbOptions = array ( 'persistent' => true ); /** * Tables already synchronized * * Type: array */ private $alreadyProcessedTables = array (); /** * Class constructor */ function __construct ($options) { $this->optionsCLI = $options; /* Set current section if given */ if ($this->optionCLI->section) $this->currentSection = $this->optionCLI->section; /* Set an alternative config file */ if ($this->optionCLI->synchroIni) $this->loadConfig ($this->optionsCLI->synchroIni); else $this->loadConfig (CONFIG_FILE); $this->checkConfig (); } /** * Class destructor */ function __destruct () {} /** * GET Accessor for this class * * Return the requested property value or a empty string if not found. * * @author Emmanuel Saracco * * @retval mixed The requested value or a empty string */ public function __GET ($name) { $name = strtolower (preg_replace ('#([A-Z])#', '_\\1', $name)); return (isset ($this->config['main'][$name])) ? $this->config['main'][$name] : ''; } /** * Initialize master database connexion * * Initialize master database connexion (master database). * * @author Emmanuel Saracco * * @retval void */ public function initMasterCnx () { $v = $this->config['main']; $this->masterCnx['dsn'] = $this->buildDSN ($v['user'], $v['password'], $v['host'], $v['port'], $v['database']); $this->masterCnx['cnx'] = DB::connect ($dsn, $this->dbOptions); if (PEAR::isError($this->masterCnx['cnx'])) $this->abort ("Error while connecting to the master " . "database '" . $this->masterCnx['dsn'] . "': " . $this->masterCnx['cnx']->getMessage ()); /* Do not automatically commit */ $this->masterCnx['cnx']->autoCommit (false); } /** * Initialize slaves databases connexions * * Initialize slaves databases connexions (UE and TP databases). * * @author Emmanuel Saracco * * @retval boolean true on success, false otherwise */ public function initSlavesCnx () { $ret = true; $i = 0; $this->slavesCnx = array (); foreach ($this->config as $section => $v) { /* Bypass main section */ if ($section == 'main') continue; $this->currentSection = $section; /* Must we only synchronize one section? */ if ($this->optionsCLI->section && strtolower ($section) != strtolower ($this->optionsCLI->section)) continue; $dsn = $this->buildDSN ($v['user'], $v['password'], $v['host'], $v['port'], $v['database']); $this->slavesCnx[$section] = array ( 'cnx' => DB::connect ($dsn, $this->dbOptions), 'commitNeeded' => false, 'host' => $v['host'], 'database' => $v['database'], 'dsn' => $dsn, 'tables' => array () ); if (PEAR::isError($this->slavesCnx[$section]['cnx'])) { $this->logPush ('error', "Error while connecting to the slave " . "database : '$dsn'" . $this->slavesCnx[$section]['cnx']->getMessage ()); array_splice ($this->slavesCnx, $i--, 1); $ret = false; } /** * Build tables informations (columns to synchronize and WHERE clause * if specified) */ else { $this->slavesCnx[$section]['tables'] = $this->buildTablesArray ($k, $v); $this->slavesCnx[$section]['cnx']->autoCommit (false); } ++$i; } /** * Abort if no connection to at least one slave database has been * successfull */ if (!count ($this->slavesCnx)) { if ($this->optionsCLI->section) $this->abort ("The specified section '" . $this->optionsCLI->section . "' does not exists in the .ini configuration file, " . "or connection failed"); else $this->abort ("No valid slave database has been found"); } return $ret; } /** * Build a table's properties array from config file * * Build a table's properties array from config file information. * * The returned array is: * * * Array * ( * [table_name] => SQL WHERE clause * [tb2] => id < 50 * [tb3] => id BETWEEN 50 AND 6000 * ) * * * @author Emmanuel Saracco * * @param[in] $section [required] string Name of the config file * section * @param[in] $data [required] array Data to process * * @retval array A array */ private function buildTablesArray ($section, $data) { $res = array (); if (!isset ($data['tables']) || !$data['tables']) $this->abort ("'tables' variable has not been defined for section " . "'$section'"); $tables = explode (',', $data['tables']); if (!count ($tables)) $this->abort ("'tables' variable is empty for section " . "'$section'"); foreach ($tables as $table) { /* Check if this table exists in master DB */ $infos = $this->masterCnx['cnx']->tableInfo ($table); if (!is_array ($infos) || !count ($infos)) $this->abort ("Table '$table' does not exists"); $res[$table] = (isset ($data["where_$table"])) ? $data["where_$table"] : ''; } /* Check that WHERE SQL clauses really match specified tables */ foreach ($data as $k => $v) { if (preg_match ('#^where_(.*)$#', $k, $m) && !in_array ($m[1], $tables)) $this->abort ("WHERE SQL clause \"$k\" does not correspond to " . "any table"); } return $res; } /** * Guess foreign key tables associated to a given table * * Guess foreign key tables associated to a given table. * * The returned array is: * * * Array * ( * [array_index] => Table name * [0] => channel_countries * ) * * * @author Emmanuel Saracco * * @param[in] $table [required] string Table name * * @warning Crappy way to detect tables associated to foreign keys... * Change it! * * @retval array Array of tables names corresponding to foreign keys */ private function getForeignTables ($table) { $fTables = ''; $fKeys = ($this->masterCnx['cnx']->tableInfo ($table)); if (is_array ($fKeys) && count ($fKeys)) { foreach ($fKeys as $c) { /* Check if item is really a DB table */ if (preg_match ('#^(.*)_id$#', $c['name'], $m)) { $isTable = $this->masterCnx['cnx']->tableInfo ($m[1]); if (is_array ($isTable) && count ($isTable)) $fTables[] = $m[1]; } } } return $fTables; } /** * Build a DSN string * * Build a PostgreSQL DSN string. * * @author Emmanuel Saracco * * @param[in] $user [required] string Database username * @param[in] $password [required] string Database password * @param[in] $host [required] string Database host * @param[in] $port [required] numeric Database port * @param[in] $database [required] string Database name * * @retval string A DSN string */ private function buildDSN ($user, $password, $host, $port, $database) { $dsn = "pgsql://$user"; if ($password) $dsn .= ":$password"; $dsn .= "@$host"; if ($port) $dsn .= ":$port"; $dsn .= "/$database"; return $dsn; } /** * Get row ids to synchronize * * Get row ids to synchronize from master database to slave. * * The returned array is: * * * Array * ( * [string_with_id] => action * [id_45] => delete * [id_48] => update * ) * * * @author Emmanuel Saracco * * @param[in] $table [required] string Table name * @param[in] $slaveCnx [required] array A reference on the slave * infos array * * @retval array Array containing ids to synchronize */ private function getIdsToProcess ($table, $slaveCnx) { $res = array (); $idsSlave = array (); $idsMaster = array (); $slaveLabel = $this->getSlaveLabel ($slaveCnx); $sql = sprintf ("SELECT id, md5 FROM get_table_md5(%s) ", $this->getSecureSQLValue ($table)); /* If a where clause was specified in config file, use it */ if (($where = $slaveCnx['tables'][$table])) $sql .= sprintf (" WHERE id IN (SELECT id FROM \"%s\" WHERE %s)", $this->getSecureSQLField ($table), $this->getSecureSQLWHERE ($where)); /* Get sums from master if not already initialized */ $idsMaster[$table] = array (); $this->logPush ('debug', "Retreiving MD5 sums for table '$table' " . "on master..."); $this->logPush ('debug', "SQL: $sql"); $resMaster = $this->masterCnx['cnx']->query ($sql); if (PEAR::isError ($resMaster)) $this->abort ("A problem occured while retreiving MD5 sums for " . "table '$table' on master: " . $resMaster->getMessage ()); if (!$resMaster || !count ($resMaster)) $this->abort ("Could not retreive MD5 sums for table '$table' " . "on master"); /* Build master ids array */ while ($row = $resMaster->fetchRow (DB_FETCHMODE_ASSOC)) { if (!$row['md5']) $this->abort ("MD5 sum is empty on master for table '$table', " . "id '" . $row['id'] . "'"); $idsMaster[$table]['id_' . $row['id']] = $row['md5']; } $this->masterCnx['cnx']->freeResult ($resMaster); /* Get sums from slave */ $this->logPush ('debug', "Retreiving MD5 sums for table '$table' " . "on slave '$slaveLabel'..."); $this->logPush ('debug', "SQL: $sql"); $resSlave = $slaveCnx['cnx']->query ($sql); if (PEAR::isError ($resSlave)) $this->abort ("A problem occured while retreiving MD5 sums for " . "table '$table' on slave '$slaveLabel': " . $resSlave->getMessage ()); if (!$resSlave || !count ($resSlave)) $this->abort ("Could not retreive MD5 sums for table '$table' " . "on slave"); /* Build slave ids array */ while ($row = $resSlave->fetchRow (DB_FETCHMODE_ASSOC)) { if (!$row['md5']) $this->abort ("MD5 sum is empty on slave '$slaveLabel' for " . "table '$table', id '" . $row['id'] . "'"); $idsSlave['id_' . $row['id']] = $row['md5']; } $this->masterCnx['cnx']->freeResult ($resSlave); /** * Calculate diff between arrays to determine which items should be * added and updated */ foreach ($idsMaster[$table] as $id => $md5) { /* If item exists in master but not in slave, insert it */ if (!isset ($idsSlave[$id])) $res[$id] = 'insert'; /** * If item exists in both master and slave, and the MD5 sums are * different, update it */ elseif ($idsMaster[$table][$id] != $idsSlave[$id]) $res[$id] = 'update'; } /** * Calculate diff between arrays to determine which items should be * removed */ foreach ($idsSlave as $id => $md5) { /* If item exists only in slave and not in master, delete it */ if (!isset ($idsMaster[$table][$id])) $res[$id] = 'delete'; } return $res; } /** * Secure a SQL value * * Secure a SQL value. * * @author Emmanuel Saracco * * @warning This method should convert value encoding before working * with it * * @retval string A secured SQL field */ private function getSecureSQLValue ($value) { return $this->masterCnx['cnx']->quoteSmart ($value); } /** * Secure a SQL field * * Secure a SQL field. * * @author Emmanuel Saracco * * @warning This method should convert value encoding before working * with it * * @retval string A secured SQL field */ private function getSecureSQLField ($field) { return preg_replace ('#[^a-zA-Z\d_]#', '', $field); } /** * Secure a SQL WHERE clause * * Secure a SQL WHERE clause. * * @author Emmanuel Saracco * * @warning This method should convert value encoding before working * with it * * @retval string A secured SQL WHERE clause */ private function getSecureSQLWHERE ($where) { return ereg_replace (';', '', $where); } /** * Process synchroniszation * * Synchronize slaves from master database. * * @author Emmanuel Saracco * * @retval void */ public function process () { foreach ($this->slavesCnx as $section => $data) { /* If we are already processing this section, just continue */ if ($this->isRunning ($section)) continue; $this->currentSection = $section; /* Create non-reentrance flag */ $this->createFlag ($section); /* Reinit already processed tables array */ $this->alreadyProcessedTables = array (); foreach ($data['tables'] as $k => $v) { /* Do the real job */ $this->processReal ($k, $data); } /* Commit transactions for the current slave connexion */ $this->commitSlave ($data); /* Delete non-reentrance flag */ $this->deleteFlag ($section); } } /** * Commit transaction on a given slave * * Commit transaction on a given slave host. If no transaction has been * done on the given connexion, nothing will be done. * * @author Emmanuel Saracco * * @param[in] $data [required] array Slave array data * * @retval void */ private function commitSlave ($data) { /* Commit only when needed */ if (!$data['commitNeeded']) { $this->logPush ('notice', "No need to commit for host '" . $this->getSlaveLabel ($data) . "'"); return; } if ($this->dryRun) { $this->logPush ('notice', "Running in dry-run mode. Nothing will " . "be committed"); } else { $this->logPush ('debug', "Committing transactions for slave '" . $this->getSlaveLabel ($data) . "'"); $data['cnx']->commit (); } $data['commitNeeded'] = false; } /** * Build a slave host label * * Build a slave host label from slave host information structure. * * @author Emmanuel Saracco * * @param[in] $slaveData [required] array A slave array * * @retval string The slave label */ private function getSlaveLabel ($slaveData) { return $slaveData['host'] . '/' . $slaveData['database']; } /** * Process synchroniszation for a given table * * Process synchroniszation for a given table regarding to its * dependencies on other tables. All dependent tables are recursivly * synchronized too. * * @author Emmanuel Saracco * * @retval void */ public function processReal ($table, &$data) { /** * Cancel process if the table has already been processed during * dependencies resolution */ if (isset ($this->alreadyProcessedTables[$fTable])) return; /* Process table dependencies recursivly */ $fTables = $this->getForeignTables ($table); if ($fTables && is_array ($fTables)) { foreach ($fTables as $fTable) { if (!isset ($this->alreadyProcessedTables[$fTable])) $this->processReal ($fTable, $data); } } /* Get ids to insert/update/delete */ $idsToProcess = $this->getIdsToProcess ($table, $data); $this->logPush ('debug', "Synchronizing table '$table'..."); /* Loop between ids and apply needed action */ foreach ($idsToProcess as $idString => $action) { /* Get id from array key */ list (,$id) = explode ('_', $idString); $this->logPush ('debug', strtoupper ($action) . " action for id '$id' " . "on table '$table' on '" . $this->getSlaveLabel ($data) . "'"); /* Guess what to do, and do it... */ switch ($action) { /* Insert item */ case 'insert': //print "insert id $table $id\n"; $this->insertItem ($data, $table, $id); break; /* update item */ case 'update': //print "update id $table $id\n"; $this->updateItem ($data, $table, $id); break; /* Delete item */ case 'delete': //print "delete id $table $id\n"; $this->deleteItem ($data, $table, $id); } } /* Mark the current table as 'already processed' */ $this->alreadyProcessedTables[$table] = 1; } /** * Insert a item in a slave DB * * Insert a item in a slave DB. * * @author Emmanuel Saracco * * @param[in] $slave [required] array Slave data array * @param[in] $table [required] string Table name * @param[in] $id [required] numeric Id of the item to add * * @retval void */ private function insertItem (&$slave, $table, $id) { $slaveLabel = $this->getSlaveLabel ($slave); /* Get master row data */ $row = $this->getMasterRowById ($slave, $table, $id); /* Build SQL INSERT from master column data */ $sql = $this->buildSQLINSERT ($table, $row); /* Insert item in slave DB */ $this->logPush ('debug', "Inserting data of id '$id' on " . "table '$table' on slave '$slaveLabel'..."); $this->logPush ('debug', "SQL: $sql"); $res1 = $slave['cnx']->query ($sql); if (PEAR::isError ($res1)) $this->abort ("A problem occured while inserting data of " . "id '$id' on table '$table' on slave '$slaveLabel': " . $res1->getMessage ()); $slave['commitNeeded'] = true; } /** * Update item in a slave DB * * Update a item in a slave DB. * * @author Emmanuel Saracco * * @param[in] $slave [required] array Slave data array * @param[in] $table [required] string Table name * @param[in] $id [required] numeric Id of the item to add * * @retval void */ private function updateItem (&$slave, $table, $id) { $slaveLabel = $this->getSlaveLabel ($slave); /* Get master row data */ $row = $this->getMasterRowById ($slave, $table, $id); /* Build SQL UPDATE from master column data */ $sql = $this->buildSQLUPDATE ($table, $row); /* Update item in slave DB */ $this->logPush ('debug', "Updating data of id '$id' on " . "table '$table' on slave '$slaveLabel'..."); $this->logPush ('debug', "SQL: $sql"); $res1 = $slave['cnx']->query ($sql); if (PEAR::isError ($res1)) $this->abort ("A problem occured while updating data of " . "id '$id' on table '$table' on slave '$slaveLabel': " . $res1->getMessage ()); $slave['commitNeeded'] = true; } /** * Delete a item to a slave server * * Delete a item to a slave server. * * @author Emmanuel Saracco * * @param[in] $slave [required] array Array data for slave server * @param[in] $table [required] string * @param[in] $id [required] array Array data for slave server * * @retval void */ private function deleteItem (&$slave, $table, $id) { $slaveLabel = $this->getSlaveLabel ($slave); $sql = sprintf ("DELETE FROM \"%s\" WHERE id = %s", $this->getSecureSQLField ($table), $this->getSecureSQLValue ($id)); $this->logPush ('debug', "Deleting id '$id' on table '$table' " . "on slave '$slaveLabel'..."); $this->logPush ('debug', "SQL: $sql"); $res = $slave['cnx']->query ($sql); if (PEAR::isError ($res)) $this->abort ("A problem occured while deleting id '$id' on table " . "'$table' on slave '$slaveLabel': " . $res->getMessage ()); $slave['commitNeeded'] = true; } /** * Retrieve master row data * * Retrieve master row data for a given id. * * The returned array is: * * * Array * ( * [field_name] => value * [url] => http://www.arte.fr * ) * * * @author Emmanuel Saracco * * @param[in] $slave [required] array Slave data array * @param[in] $table [required] string Table name * @param[in] $id [required] numeric Id of the item to add * * @retval array A associative array of column data */ private function getMasterRowById ($slave, $table, $id) { /* Get all data for the given id */ $sql = sprintf ("SELECT * FROM \"%s\" WHERE id = %s", $this->getSecureSQLField ($table), $this->getSecureSQLValue ($id)); $this->logPush ('debug', "Retrieving data of id '$id' on " . "table '$table' on master..."); $this->logPush ('debug', "SQL: $sql"); $res = $this->masterCnx['cnx']->query ($sql); if (PEAR::isError ($res)) $this->abort ("A problem occured while retreiving data of id '$id' " . "on table '$table' on master: " . $res->getMessage ()); /* Retreive master new column data */ $row = $res->fetchRow (DB_FETCHMODE_ASSOC); $this->masterCnx['cnx']->freeResult ($res); return $row; } /** * Build a SQL UPDATE query * * Build a SQL UPDATE query. * * @author Emmanuel Saracco * * @param[in] $table [required] string Table name * @param[in] $row [required] array Array of keys/values * * @retval void */ private function buildSQLUPDATE ($table, $row) { $update = 'UPDATE "' . $this->getSecureSQLField ($table) . '" SET '; $where = ' WHERE id = ' . $this->getSecureSQLValue ($row['id']); $fields = ''; foreach ($row as $k => $v) { $fields .= '"' . $this->getSecureSQLField ($k) . '" = '; $fields .= $this->getSecureSQLValue ($v) . ','; } $fields = ereg_replace ('.$', '', $fields); $sql = "$update $fields $where"; return $sql; } /** * Build a SQL INSERT query * * Build a SQL INSERT query. * * @author Emmanuel Saracco * * @param[in] $table [required] string Table name * @param[in] $row [required] array Array of keys/values * * @retval void */ private function buildSQLINSERT ($table, $row) { $insert = 'INSERT INTO "' . $this->getSecureSQLField ($table) . '" '; $fields = ''; $values = ''; foreach ($row as $k => $v) { $fields .= '"' . $this->getSecureSQLField ($k) . '",'; $values .= $this->getSecureSQLValue ($v) . ','; } $fields = ereg_replace ('.$', '', $fields); $values = ereg_replace ('.$', '', $values); $sql = "$insert ($fields) VALUES ($values)"; return $sql; } /** * Build section name for non-reentrance flag * * Build section name for non-reentrance flag. * * @author Emmanuel Saracco * * @param[in] $section string Section name * * @retval string Section name for flag management */ function getSectionName ($section = '') { $ret = ''; if ($section) $ret = '_' . $section; elseif ($this->currentSection) $ret = '_' . $this->currentSection; return $ret; } /** * Check either or not the non-reentrance flag exists * * Check if the non-reentrance flag exists for a given section. * * @author Emmanuel Saracco * * @param[in] $section string Section name * * @retval boolean true if the flag exists, false otherwise */ public function isRunning ($section = '') { $ret = false; $section = $this->getSectionName ($section); $name = $this->getFlagFilePath () . $section; if (file_exists ($name)) { $this->logPush ('debug', "Flag '$name' already exists"); $ret = true; } return $ret; } /** * Return monitoring file path * * Return the name of the monitoring file path. * * @author Emmanuel Saracco * * @retval string Monitoring file path */ public function getMonitoringFilePath () { $filename = ($this->optionsCLI->monitoringFile) ? $this->optionsCLI->monitoringFile : MONITORING_BASE_FILENAME; return $this->lockDir . '/' . $filename . '_' . $this->currentSection; } /** * Return non-reentrance flag name * * Return the name of the non-reentrance flag. * * @author Emmanuel Saracco * * @retval string Flag name */ public function getFlagFilePath () { return $this->lockDir . '/' . FLAG_BASE_FILENAME; } /** * Create non-reentrance flag * * Create the non-reentrance flag and reinitialize monitoring file. * * @author Emmanuel Saracco * * @param[in] $section string Section name * * @retval void */ public function createFlag ($section) { $section = $this->getSectionName ($section); $name = $this->getFlagFilePath () . $section; /* Check file permissions */ if (file_exists ($name) && !is_writable ($name)) $this->abort ("File '$name' is not writable"); /* Create non-reentrance flag */ file_put_contents ($name, '1'); /* Reinitialize monitoring file if needed */ if ($this->optionsCLI->section) { $monitoringName = $this->getMonitoringFilePath ($section); /* Check file permissions */ if (file_exists ($monitoringName) && !is_writable ($monitoringName)) $this->abort ("File '$monitoringName' is not writable"); /* Empty monitoring file */ file_put_contents ($monitoringName, ''); } } /** * Delete non-reentrance flag * * Delete the non-reentrance flag. * * @author Emmanuel Saracco * * @param[in] $section string Section name * * @retval void */ public function deleteFlag ($section) { $section = $this->getSectionName ($section); $name = $this->getFlagFilePath () . $section; if (!file_exists ($name)) $this->logPush ('debug', "Flag '$name' does not exists"); /* Check file permissions */ if (file_exists ($name) && !is_writable ($name)) $this->abort ("File '$name' is not writable"); @unlink ($name); } /** * Log writer * * Write data into log files regarding requested level. * * @author Emmanuel Saracco * * @param[in] $level [required] string Log level (debug, notice, * warn, error) * @param[in] $data [required] string Data to log * * @retval void */ public function logPush ($level, $data) { $log = false; $dateTime = date ('Y-m-d H:i:s'); $debugFile = $this->logDir . '/debug.log'; $noticeFile = $this->logDir . '/notice.log'; $warnFile = $this->logDir . '/warn.log'; $errorFile = $this->logDir . '/error.log'; $monitoringName = $this->getMonitoringFilePath (); $data = '[' . date ('Y-m-d H:i:s') . "] $data\n"; $file = $level . 'File'; switch ($this->logLevel) { /* Log everything */ case 'debug': $log = true; break; /* Log only notice, warnings and errors */ case 'notice': if ($level != 'debug') $log = true; break; /* Log only warnings and errors */ case 'warn': if ($level != 'debug' && $level != 'notice') $log = true; break; /* Log only errors */ case 'error': if ($level != 'debug' && $level != 'notice' && $level != 'warn') { /* Display error messages */ fprintf (STDERR, "$data\n"); $log = true; } } /* Log in monitoring file if needed */ if ($this->optionsCLI->section) { /* Check file permissions */ if (file_exists ($monitoringName) && !is_writable ($monitoringName)) $this->abort ("File '$monitoringName' is not writable"); if ($level == 'notice' || $level == 'warn' || $level == 'error') file_put_contents ($monitoringName, $data, FILE_APPEND); } /* Debug if raw debug has been requested */ if ($this->optionsCLI->verbose) print "$data"; if ($log === true) { /* Check file permissions */ if (file_exists ($$file) && !is_writable ($$file)) $this->abort ("File '$$file' is not writable"); $ret = file_put_contents ($$file, $data, FILE_APPEND); if (!$ret) $this->abort ("Unabled to open log file '" . $$file . "'"); } } /** * Load configuration file * * Load the configuration file for the synchronization script. * * @author Emmanuel Saracco * * @param[in] $configFile [required] string Configuration file for * this script * * @retval void */ protected function loadConfig ($configFile) { if (!file_exists ($configFile)) $this->abort ( "Config file '$configFile' does not exists"); $this->config = parse_ini_file ($configFile, true); if (!is_array ($this->config) || !count ($this->config)) $this->abort ( "Configuration file '$configFile' is empty or " . "does not contains valid data"); } /** * Check config variables * * Check all config variables. If a error occured, the execution is * aborted. * * @author Emmanuel Saracco * * @see isValidLogDir() * * @retval void */ protected function checkConfig () { /* Check lockDir */ if (!is_dir ($this->lockDir)) $this->abort ("'lockDir' variable value '" . $this->lockDir . "' is not a directory"); /* Check logDir */ if (!$this->isValidLogDir ()) $this->abort ("'logDir' variable value '" . $this->logDir . "' is not a directory"); /* Check logLevel */ if ($this->logLevel != 'debug' && $this->logLevel != 'notice' && $this->logLevel != 'warn' && $this->logLevel != 'error') $this->abort ("'logLevel' variable value '" . $this->logLevel . "' is not in [debug, notice, warn, error]"); /* Check dryRun */ if ($this->dryRun) { if ($this->dryRun != '1' && $this->dryRun != '0') $this->abort ("'dryRun' variable value '" . $this->dryRun . "' is not in [yes, no]"); } /* Foce option from command line */ if ($this->optionsCLI->dryRun) $this->dryRun = 1; } /** * Check if log directory exists * * Check log directory existence. * * @author Emmanuel Saracco * * @see checkConfig() * * @retval boolean true if directory exists, false otherwise */ private function isValidLogDir () { return is_dir ($this->logDir); } /** * Abort this script on error * * Abort the script on error after displaying a message. * The script return 1 on exit. * * @author Emmanuel Saracco * * @param[in] $data [required] string Data to print on stderr * * @retval void */ public function abort ($data) { /* avoid reentrance for this function */ static $in = false; if ($in) return; $in = true; /* Force flags deletion */ foreach ($this->config as $section => $v) if ($section != 'main') $this->deleteFlag ($section); /* If log directory is ok, log the error message */ if ($this->isValidLogDir ()) $this->logPush ('error', $data); fprintf (STDERR, "$data\n"); fprintf (STDERR, "Execution aborted!\n"); exit (1); } /** * Just exit this script * * Exit the script. * The script return 0 on exit. * * @author Emmanuel Saracco * * @param[in] $data string Data to log * * @retval void */ public function quit ($data = '') { if (!$data) $data = 'Just quit...'; $this->logPush ('debug', $data); exit (0); } } /*************************************************************************/ /*************************** Main ****************************************/ /*************************************************************************/ $Synchro = new EGGS_Synchro (new EGGS_GetOpt ()); /* Initialize databases connexions */ $Synchro->initMasterCnx (); if (!$Synchro->initSlavesCnx ()) $Synchro->abort ('A problem occured while connecting to the database'); /* Run synchronization */ $Synchro->process (); $Synchro->quit (); ?>