improved database backend support for larger files (100 KiB - 16 MiB),

introduced database versioning to reduce amount of checks done per
request
This commit is contained in:
El RIDO 2015-11-01 17:02:20 +01:00
parent 9d27e7a65d
commit 42a9c92b5e
3 changed files with 281 additions and 81 deletions

View file

@ -22,7 +22,7 @@ class zerobin
* *
* @const string * @const string
*/ */
const VERSION = '0.21.1'; const VERSION = '0.22';
/** /**
* show the same error message if the paste expired or does not exist * show the same error message if the paste expired or does not exist

View file

@ -84,102 +84,54 @@ class zerobin_db extends zerobin_abstract
$options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; $options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
$options['opt'][PDO::ATTR_EMULATE_PREPARES] = false; $options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
$options['opt'][PDO::ATTR_PERSISTENT] = true; $options['opt'][PDO::ATTR_PERSISTENT] = true;
$db_tables_exist = true;
// check if the database contains the required tables // setup type and dabase connection
self::$_type = strtolower( self::$_type = strtolower(
substr($options['dsn'], 0, strpos($options['dsn'], ':')) substr($options['dsn'], 0, strpos($options['dsn'], ':'))
); );
switch(self::$_type) $tableQuery = self::_getTableQuery(self::$_type);
{
case 'ibm':
$sql = 'SELECT tabname FROM SYSCAT.TABLES ';
break;
case 'informix':
$sql = 'SELECT tabname FROM systables ';
break;
case 'mssql':
$sql = "SELECT name FROM sysobjects "
. "WHERE type = 'U' ORDER BY name";
break;
case 'mysql':
$sql = 'SHOW TABLES';
break;
case 'oci':
$sql = 'SELECT table_name FROM all_tables';
break;
case 'pgsql':
$sql = "SELECT c.relname AS table_name "
. "FROM pg_class c, pg_user u "
. "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND c.relname !~ '^(pg_|sql_)' "
. "UNION "
. "SELECT c.relname AS table_name "
. "FROM pg_class c "
. "WHERE c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
. "AND c.relname !~ '^pg_'";
break;
case 'sqlite':
$sql = "SELECT name FROM sqlite_master WHERE type='table' "
. "UNION ALL SELECT name FROM sqlite_temp_master "
. "WHERE type='table' ORDER BY name";
break;
default:
throw new Exception(
'PDO type ' .
self::$_type .
' is currently not supported.',
5
);
}
self::$_db = new PDO( self::$_db = new PDO(
$options['dsn'], $options['dsn'],
$options['usr'], $options['usr'],
$options['pwd'], $options['pwd'],
$options['opt'] $options['opt']
); );
$statement = self::$_db->query($sql);
$tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if needed // check if the database contains the required tables
$tables = self::$_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if necessary
if (!in_array(self::$_prefix . 'paste', $tables)) if (!in_array(self::$_prefix . 'paste', $tables))
{ {
self::$_db->exec( self::_createPasteTable();
'CREATE TABLE ' . self::$_prefix . 'paste ( ' . $db_tables_exist = false;
'dataid CHAR(16), ' .
'data TEXT, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT, ' .
'meta TEXT );'
);
}
// check if the meta column exists
else
{
try {
self::_exec('SELECT meta FROM ' . self::$_prefix . 'paste LIMIT 1;', array());
} catch (PDOException $e) {
self::$_db->exec('ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN meta TEXT;');
}
} }
// create comment table if needed // create comment table if necessary
if (!in_array(self::$_prefix . 'comment', $tables)) if (!in_array(self::$_prefix . 'comment', $tables))
{ {
self::$_db->exec( self::_createCommentTable();
'CREATE TABLE ' . self::$_prefix . 'comment ( ' . $db_tables_exist = false;
'dataid CHAR(16), ' . }
'pasteid CHAR(16), ' .
'parentid CHAR(16), ' . // create config table if necessary
'data TEXT, ' . $db_version = zerobin::VERSION;
'nickname VARCHAR(255), ' . if (!in_array(self::$_prefix . 'config', $tables))
'vizhash TEXT, ' . {
'postdate INT );' self::_createConfigTable();
); // if we only needed to create the config table, the DB is older then 0.22
if ($db_tables_exist) $db_version = '0.21';
}
else
{
$db_version = self::_getConfig('VERSION');
}
// update database structure if necessary
if (version_compare($db_version, zerobin::VERSION, '<'))
{
self::_upgradeDatabase($db_version);
} }
} }
} }
@ -208,6 +160,7 @@ class zerobin_db extends zerobin_abstract
} }
$opendiscussion = $burnafterreading = false; $opendiscussion = $burnafterreading = false;
$attachment = $attachmentname = '';
$meta = $paste['meta']; $meta = $paste['meta'];
unset($meta['postdate']); unset($meta['postdate']);
$expire_date = 0; $expire_date = 0;
@ -226,8 +179,18 @@ class zerobin_db extends zerobin_abstract
$burnafterreading = (bool) $paste['meta']['burnafterreading']; $burnafterreading = (bool) $paste['meta']['burnafterreading'];
unset($meta['burnafterreading']); unset($meta['burnafterreading']);
} }
if (array_key_exists('attachment', $paste['meta']))
{
$attachment = $paste['meta']['attachment'];
unset($meta['attachment']);
}
if (array_key_exists('attachmentname', $paste['meta']))
{
$attachmentname = $paste['meta']['attachmentname'];
unset($meta['attachmentname']);
}
return self::_exec( return self::_exec(
'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?,?)', 'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?,?,?,?)',
array( array(
$pasteid, $pasteid,
$paste['data'], $paste['data'],
@ -236,6 +199,8 @@ class zerobin_db extends zerobin_abstract
(int) $opendiscussion, (int) $opendiscussion,
(int) $burnafterreading, (int) $burnafterreading,
json_encode($meta), json_encode($meta),
$attachment,
$attachmentname,
) )
); );
} }
@ -265,6 +230,8 @@ class zerobin_db extends zerobin_abstract
$meta = json_decode($paste['meta']); $meta = json_decode($paste['meta']);
if (!is_object($meta)) $meta = new stdClass; if (!is_object($meta)) $meta = new stdClass;
// support older attachments
if (property_exists($meta, 'attachment')) if (property_exists($meta, 'attachment'))
{ {
self::$_cache[$pasteid]->attachment = $meta->attachment; self::$_cache[$pasteid]->attachment = $meta->attachment;
@ -275,6 +242,15 @@ class zerobin_db extends zerobin_abstract
unset($meta->attachmentname); unset($meta->attachmentname);
} }
} }
// support current attachments
elseif (array_key_exists('attachment', $paste) && strlen($paste['attachment']))
{
self::$_cache[$pasteid]->attachment = $paste['attachment'];
if (array_key_exists('attachmentname', $paste) && strlen($paste['attachmentname']))
{
self::$_cache[$pasteid]->attachmentname = $paste['attachmentname'];
}
}
self::$_cache[$pasteid]->meta = $meta; self::$_cache[$pasteid]->meta = $meta;
self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate']; self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
$expire_date = (int) $paste['expiredate']; $expire_date = (int) $paste['expiredate'];
@ -450,4 +426,227 @@ class zerobin_db extends zerobin_abstract
$statement->closeCursor(); $statement->closeCursor();
return $result; return $result;
} }
/**
* get table list query, depending on the database type
*
* @access private
* @static
* @param string $type
* @throws Exception
* @return string
*/
private static function _getTableQuery($type)
{
switch($type)
{
case 'ibm':
$sql = 'SELECT tabname FROM SYSCAT.TABLES ';
break;
case 'informix':
$sql = 'SELECT tabname FROM systables ';
break;
case 'mssql':
$sql = "SELECT name FROM sysobjects "
. "WHERE type = 'U' ORDER BY name";
break;
case 'mysql':
$sql = 'SHOW TABLES';
break;
case 'oci':
$sql = 'SELECT table_name FROM all_tables';
break;
case 'pgsql':
$sql = "SELECT c.relname AS table_name "
. "FROM pg_class c, pg_user u "
. "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND c.relname !~ '^(pg_|sql_)' "
. "UNION "
. "SELECT c.relname AS table_name "
. "FROM pg_class c "
. "WHERE c.relkind = 'r' "
. "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
. "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
. "AND c.relname !~ '^pg_'";
break;
case 'sqlite':
$sql = "SELECT name FROM sqlite_master WHERE type='table' "
. "UNION ALL SELECT name FROM sqlite_temp_master "
. "WHERE type='table' ORDER BY name";
break;
default:
throw new Exception(
"PDO type $type is currently not supported.", 5
);
}
return $sql;
}
/**
* get a value by key from the config table
*
* @access private
* @static
* @param string $key
* @throws PDOException
* @return string
*/
private static function _getConfig($key)
{
$row = self::_select(
'SELECT value FROM ' . self::$_prefix . 'config WHERE id = ?',
array($key), true
);
return $row['value'];
}
/**
* get the primary key clauses, depending on the database driver
*
* @access private
* @static
* @param string $key
* @return array
*/
private static function _getPrimaryKeyClauses($key = 'dataid')
{
$main_key = $after_key = '';
if (self::$_type === 'mysql')
{
$after_key = ", PRIMARY KEY ($key)";
}
else
{
$main_key = ' PRIMARY KEY';
}
return array($main_key, $after_key);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createPasteTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
"dataid CHAR(16) NOT NULL$main_key, " .
'data BLOB, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT, ' .
'meta TEXT, ' .
'attachment MEDIUMBLOB, ' .
"attachmentname BLOB$after_key );"
);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createCommentTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
"dataid CHAR(16) NOT NULL$main_key, " .
'pasteid CHAR(16), ' .
'parentid CHAR(16), ' .
'data BLOB, ' .
'nickname BLOB, ' .
'vizhash BLOB, ' .
"postdate INT$after_key );"
);
self::$_db->exec(
'CREATE INDEX parent ON ' . self::$_prefix . 'comment(pasteid);'
);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createConfigTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses('id');
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'config ( ' .
"id CHAR(16) NOT NULL$main_key, value TEXT$after_key );"
);
self::_exec(
'INSERT INTO ' . self::$_prefix . 'config VALUES(?,?)',
array('VERSION', zerobin::VERSION)
);
}
/**
* upgrade the database schema from an old version
*
* @access private
* @static
* @param string $oldversion
* @return void
*/
private static function _upgradeDatabase($oldversion)
{
switch ($oldversion)
{
case '0.21':
// create the meta column if necessary (pre 0.21 change)
try {
self::$_db->exec('SELECT meta FROM ' . self::$_prefix . 'paste LIMIT 1;', array());
} catch (PDOException $e) {
self::$_db->exec('ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN meta TEXT;');
}
// SQLite only allows one ALTER statement at a time...
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN attachment MEDIUMBLOB;'
);
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN attachmentname BLOB;'
);
// SQLite doesn't support MODIFY, but it allows TEXT of similar
// size as BLOB, so there is no need to change it there
if (self::$_type !== 'sqlite')
{
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ' .
'ADD PRIMARY KEY (dataid),' .
'MODIFY COLUMN data BLOB;'
);
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'comment ' .
'ADD PRIMARY KEY (dataid),' .
'MODIFY COLUMN data BLOB, ' .
'MODIFY COLUMN nickname BLOB, ' .
'MODIFY COLUMN vizhash BLOB;'
);
}
else
{
self::$_db->exec(
'CREATE UNIQUE INDEX primary ON ' . self::$_prefix . 'paste(dataid);'
);
self::$_db->exec(
'CREATE UNIQUE INDEX primary ON ' . self::$_prefix . 'comment(dataid);'
);
}
self::$_db->exec(
'CREATE INDEX parent ON ' . self::$_prefix . 'comment(pasteid);'
);
}
}
} }

View file

@ -184,6 +184,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$this->reset(); $this->reset();
$options = parse_ini_file(CONF, true); $options = parse_ini_file(CONF, true);
$options['traffic']['header'] = 'X_FORWARDED_FOR'; $options['traffic']['header'] = 'X_FORWARDED_FOR';
$options['traffic']['limit'] = 100;
helper::confBackup(); helper::confBackup();
helper::createIniFile(CONF, $options); helper::createIniFile(CONF, $options);
$_POST = helper::getPaste(); $_POST = helper::getPaste();