Files
newznab-tmux/nntmux/db/DbUpdate.php
T
2017-07-17 13:23:45 +02:00

472 lines
14 KiB
PHP
Executable File

<?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 3 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 (see LICENSE.txt in the base directory. If
* not, see:
*
* @link <http://www.gnu.org/licenses/>.
* @author niel
* @copyright 2014 nZEDb
*/
namespace nntmux\db;
use App\Models\Settings;
use nntmux\ColorCLI;
use nntmux\db\DB;
use nntmux\utility\Git;
use nntmux\utility\Utility;
class DbUpdate
{
public $backedup;
/**
* @var \nntmux\db\DB Instance variable for DB object.
*/
public $pdo;
/**
* @var \nntmux\utility\Git instance
*/
public $git;
/**
* @var object Instance variable for logging object. Currently only ColorCLI supported,
* but expanding for full logging with agnostic API planned.
*/
public $log;
/**
* @var object Instance object for sites/settings class.
*/
public $settings;
protected $_DbSystem;
/**
* @var bool Has the Db been backed up?
*/
private $backedUp = false;
/**
* @var bool Should we perform a backup?
*/
private $backup = false;
public function __construct(array $options = [])
{
$options += [
'backup' => true,
'db' => null,
'git' => new Git(),
'logger' => new ColorCLI(),
];
$this->backup = $options['backup'];
$this->git = $options['git'];
$this->log = $options['logger'];
// Must be DB not Settings because the Settings table may not exist yet.
$this->pdo = (($options['db'] instanceof DB) ? $options['db'] : new DB());
$this->_DbSystem = strtolower($this->pdo->DbSystem());
}
public function loadTables(array $options = [])
{
$defaults = [
'enclosedby' => null,
'ext' => 'tsv',
'files' => [],
'path' => NN_RES . 'db' . DS . 'schema' . DS . 'data',
'regex' => '#^' . Utility::PATH_REGEX . '(?P<order>\d+)-(?P<table>\w+)\.tsv$#',
];
$options += $defaults;
$show = (Utility::isCLI() || NN_DEBUG);
$files = empty($options['files']) ? Utility::getDirFiles($options) : $options['files'];
natsort($files);
$local = $this->pdo->isLocalDb() ? '' : 'LOCAL ';
$enclosedby = empty($options['enclosedby']) ? '' : 'OPTIONALLY ENCLOSED BY "' .
$options['enclosedby'] . '"';
$sql = 'LOAD DATA ' .
$local . 'INFILE "%s" IGNORE INTO TABLE `%s` FIELDS TERMINATED BY "\t" ' . $enclosedby . 'LINES TERMINATED BY "\n" IGNORE 1 LINES (%s)';
foreach ($files as $file) {
if ($show === true) {
echo "File: $file\n";
}
if (is_readable($file)) {
if (preg_match($options['regex'], $file, $matches)) {
$table = $matches['table'];
// Get the first line of the file which holds the columns used.
$handle = @fopen($file, 'rb');
if (is_resource($handle)) {
$line = fgets($handle);
fclose($handle);
if ($line === false) {
echo "FAILED reading first line of '$file'\n";
continue;
}
$fields = trim($line);
if ($show === true) {
ColorCLI::doEcho(ColorCLI::info('Inserting data into table: ' . $table));
}
if (Utility::isWin()) {
$file = str_replace("\\", '\/', $file);
}
$this->pdo->exec("SET @@session.sql_mode =
CASE WHEN @@session.sql_mode NOT LIKE '%NO_AUTO_VALUE_ON_ZERO%'
THEN CASE WHEN LENGTH(@@session.sql_mode)>0
THEN CONCAT_WS(',',@@session.sql_mode,'NO_AUTO_VALUE_ON_ZERO')
ELSE 'NO_AUTO_VALUE_ON_ZERO'
END
ELSE @@session.sql_mode
END;");
$this->pdo->queryExec(sprintf($sql, $file, $table, $fields));
if ($table !== 'settings') {
$success = $this->pdo->query(sprintf('SELECT COUNT(id) AS num FROM %s', $table));
if (empty($success[0]['num'])) {
ColorCLI::doEcho(ColorCLI::error('Failed to insert data into table: ' . $table));
} else {
ColorCLI::doEcho(ColorCLI::notice('Successfully inserted data into ' . $table . ' table'));
}
}
} else {
exit("Failed to open file: '$file'\n");
}
} else {
echo "Incorrectly formatted filename '$file' (should match " .
str_replace('#', '', $options['regex']) . "\n";
}
} else {
echo $this->log->error(" Unable to read file: '$file'");
}
}
}
/**
* Takes new files in the correct format from the patches directory and turns them into proper patches.
*
* The files should be name as '+x~<table>.sql' where x is a number starting at 1 for your first
* patch. <table> should be the name of the primary table affected. If you have to modify more
* than one table, consider splitting into multiple patches using different patch modifier
* numbers to order them. i.e. +1~settings.sql, +2~predb.sql, etc.
*
* @param array $options
*/
public function newPatches(array $options = [])
{
$defaults = [
'data' => NN_RES . 'db' . DS . 'schema' . DS . 'data' . DS,
'ext' => 'sql',
'path' => NN_RES . 'db' . DS . 'patches' . DS . $this->_DbSystem,
'regex' => '#^' . Utility::PATH_REGEX . '\+(?P<order>\d+)~(?P<table>\w+)\.sql$#',
'safe' => true,
];
$options += $defaults;
$this->processPatches(['safe' => $options['safe']]); // Make sure we are completely up to date!
echo $this->log->primaryOver('Looking for new patches...');
$files = Utility::getDirFiles($options);
$count = count($files);
echo $this->log->header(" $count found");
if ($count > 0) {
echo $this->log->header('Processing...');
natsort($files);
$local = $this->pdo->isLocalDb() ? '' : 'LOCAL ';
foreach ($files as $file) {
if (!preg_match($options['regex'], $file, $matches)) {
$this->log->error("$file does not match the pattern {$options['regex']}\nPlease fix this before continuing");
} else {
echo $this->log->header('Processing patch file: ' . $file);
$this->splitSQL($file, ['local' => $local, 'data' => $options['data']]);
$current = (integer)Settings::value('..sqlpatch');
$current++;
$this->pdo->queryExec("UPDATE settings SET value = '$current' WHERE setting = 'sqlpatch';");
$newName = $matches['drive'] . $matches['path'] .
str_pad($current, 4, '0', STR_PAD_LEFT) . '~' .
$matches['table'] . '.sql';
rename($matches[0], $newName);
$this->git->add($newName);
if ($this->git->isCommited($this->git->getBranch() . ':' . str_replace(NN_ROOT, '', $matches[0]))) {
$this->git->add(" -u {$matches[0]}"); // remove old filename from the index.
}
}
}
}
}
public function processPatches(array $options = [])
{
$patched = 0;
$defaults = [
'data' => NN_RES . 'db' . DS . 'schema' . DS . 'data' . DS,
'ext' => 'sql',
'path' => NN_RES . 'db' . DS . 'patches' . DS . $this->_DbSystem,
'regex' => '#^' . Utility::PATH_REGEX . '(?P<patch>\d{4})~(?P<table>\w+)\.sql$#',
'safe' => true,
];
$options += $defaults;
$currentVersion = Settings::value('..sqlpatch');
if (!is_numeric($currentVersion)) {
exit("Bad sqlpatch value: '$currentVersion'\n");
}
$files = empty($options['files']) ? Utility::getDirFiles($options) : $options['files'];
if (count($files)) {
natsort($files);
$local = $this->pdo->isLocalDb() ? '' : 'LOCAL ';
$data = $options['data'];
echo $this->log->primary('Looking for unprocessed patches...');
foreach ($files as $file) {
$setPatch = false;
$fp = fopen($file, 'r');
$patch = fread($fp, filesize($file));
if (preg_match($options['regex'], str_replace('\\', '/', $file), $matches)) {
$patch = (integer)$matches['patch'];
$setPatch = true;
} else if (preg_match(
'/UPDATE `?site`? SET `?value`? = \'?(?P<patch>\d+)\'? WHERE `?setting`? = \'sqlpatch\'/i',
$patch,
$matches)
) {
$patch = (integer)$matches['patch'];
} else {
throw new \RuntimeException("No patch information available, stopping!!");
}
if ($patch > $currentVersion) {
echo $this->log->header('Processing patch file: ' . $file);
if ($options['safe'] && !$this->backedUp) {
$this->_backupDb();
}
$this->splitSQL($file, ['local' => $local, 'data' => $data]);
if ($setPatch) {
$this->pdo->queryExec("UPDATE settings SET value = '$patch' WHERE setting = 'sqlpatch';");
}
$patched++;
}
}
} else {
exit($this->log->error("\nHave you changed the path to the patches folder, or do you have the right permissions?\n"));
}
if ($patched === 0) {
echo $this->log->info("Nothing to patch, you are already on version $currentVersion");
}
return $patched;
}
public function processSQLFile(array $options = [])
{
$defaults = [
'filepath' => NN_RES . 'db' . DS . 'schema' . DS . $this->_DbSystem . '-ddl.sql',
];
$options += $defaults;
$sql = file_get_contents($options['filepath']);
$sql = str_replace(['DELIMITER $$', 'DELIMITER ;', '$$'], '', $sql);
$this->pdo->exec($sql);
}
public function splitSQL($file, array $options = [])
{
$defaults = [
'data' => null,
'delimiter' => ';',
'local' => null,
];
$options += $defaults;
if (!empty($options['vars'])) {
extract($options['vars']);
}
set_time_limit(0);
if (is_file($file)) {
$file = fopen($file, 'r');
if (is_resource($file)) {
$query = [];
$delimiter = $options['delimiter'];
while (!feof($file)) {
$line = fgets($file);
if ($line === false) {
continue;
}
// Skip comments.
if (preg_match('!^\s*(#|--|//)\s*(.+?)\s*$!', $line, $matches)) {
echo ColorCLI::info("COMMENT: " . $matches[2]);
continue;
}
// Check for non default delimiters ($$ for example).
if (preg_match('#^\s*DELIMITER\s+(?P<delimiter>.+)\s*$#i', $line, $matches)) {
$delimiter = $matches['delimiter'];
if (NN_DEBUG) {
echo ColorCLI::debug("DEBUG: Delimiter switched to $delimiter");
}
if ($delimiter != $options['delimiter']) {
continue;
}
}
// Check if the line has delimiter that is non default ($$ for example).
if ($delimiter != $options['delimiter'] && preg_match('#^(.+?)' . preg_quote($delimiter) . '\s*$#', $line, $matches)) {
// Check if the line has also the default delimiter (;), remove it.
if (preg_match('#^(.+?)' . preg_quote($options['delimiter']) . '\s*$#', $matches[1], $matches2)) {
$matches[1] = $matches2[1];
}
// Change the non default delimiter ($$) to the default one(;).
$line = $matches[1] . $options['delimiter'];
}
$query[] = $line;
if (preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', $line) == 1) {
$query = trim(implode('', $query));
if ($options['local'] !== null) {
$query = str_replace('{:local:}', $options['local'], $query);
}
if (!empty($options['data'])) {
$query = str_replace('{:data:}', $options['data'], $query);
}
try {
$qry = $this->pdo->prepare($query);
$qry->execute();
echo $this->log->alternateOver('SUCCESS: ') . $this->log->primary($query);
} catch (\PDOException $e) {
// Log the problem and the query.
file_put_contents(
NN_LOGS . 'patcherrors.log',
'[' . date('r') . '] [ERROR] [' .
trim(preg_replace('/\s+/', ' ', $e->getMessage())) . ']' . PHP_EOL .
'[' . date('r') . '] [QUERY] [' .
trim(preg_replace('/\s+/', ' ', $query)) . ']' . PHP_EOL,
FILE_APPEND
);
if (
in_array($e->errorInfo[1], [1091, 1060, 1061, 1071, 1146]) ||
in_array($e->errorInfo[0], [23505, 42701, 42703, '42P07', '42P16'])
) {
if ($e->errorInfo[1] == 1060) {
echo $this->log->warning(
"$query The column already exists - No need to worry \{" .
$e->errorInfo[1] . "}.\n"
);
} else {
echo $this->log->warning(
"$query Skipped - No need to worry \{" .
$e->errorInfo[1] . "}.\n"
);
}
} else {
if (preg_match('/ALTER IGNORE/i', $query)) {
$this->pdo->queryExec("SET SESSION old_alter_table = 1");
try {
$this->pdo->exec($query);
echo $this->log->alternateOver('SUCCESS: ') . $this->log->primary($query);
} catch (\PDOException $e) {
exit($this->log->error("$query Failed \{" . $e->errorInfo[1] . "}\n\t" . $e->errorInfo[2]));
}
} else {
exit($this->log->error("$query Failed \{" . $e->errorInfo[1] . "}\n\t" . $e->errorInfo[2]));
}
}
}
while (ob_get_level() > 0) {
ob_end_flush();
}
flush();
}
if (is_string($query) === true) {
$query = [];
}
}
}
}
}
public function updateSchemaData(array $options = [])
{
$changed = false;
$default = [
'file' => '10-settings.tsv',
'path' => 'resources' . DS . 'db' . DS . 'schema' . DS . 'data' . DS,
'regex' => '#^(?P<section>.*)\t(?P<subsection>.*)\t(?P<name>.*)\t(?P<value>.*)\t(?P<hint>.*)\t(?P<setting>.*)$#',
'value' => function(array $matches) {
return "{$matches['section']}\t{$matches['subsection']}\t{$matches['name']}\t{$matches['value']}\t{$matches['hint']}\t{$matches['setting']}";
} // WARNING: leaving this empty will blank not remove lines.
];
$options += $default;
$file = [];
$filespec = Utility::trailingSlash($options['path']) . $options['path'];
if (file_exists($filespec) && ($file = file($filespec, FILE_IGNORE_NEW_LINES))) {
$count = count($file);
$index = 0;
while ($index < $count) {
if (preg_match($options['regex'], $file[$index], $matches)) {
if (VERBOSE) {
echo $this->log->primary("Matched: " . $file[$index]);
}
$index++;
if (is_callable($options['value'])) {
$file[$index] = $options['value']($matches);
} else {
$file[$index] = $options['value'];
}
$changed = true;
}
}
}
if ($changed) {
if (file_put_contents($filespec, implode("\n", $file)) === false) {
echo $this->log->error("Error writing file to disc!!");
}
}
}
protected function _backupDb()
{
if (Utility::hasCommand("php5")) {
$PHP = "php5";
} else {
$PHP = "php";
}
system("$PHP " . NN_MISC . 'testing' . DS . 'DB' . DS . $this->_DbSystem .
'dump_tables.php db dump');
$this->backedup = true;
}
}
?>