mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-03 18:03:37 +00:00
56e2c28bad
Scene exceptions now uses a internal cache for scene exceptions and scene season exceptions, helps reduce overhead to DB and performs faster lookups when making scene exception requests.
354 lines
12 KiB
Python
354 lines
12 KiB
Python
# Author: Nic Wolfe <nic@wolfeden.ca>
|
|
# URL: http://code.google.com/p/sickbeard/
|
|
#
|
|
# This file is part of SickRage.
|
|
#
|
|
# SickRage 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.
|
|
#
|
|
# SickRage 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 SickRage. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import with_statement
|
|
|
|
import os.path
|
|
import re
|
|
import sqlite3
|
|
import time
|
|
import threading
|
|
|
|
import sickbeard
|
|
|
|
from sickbeard import encodingKludge as ek
|
|
from sickbeard import logger
|
|
from sickbeard.exceptions import ex
|
|
|
|
db_lock = threading.Lock()
|
|
|
|
|
|
def dbFilename(filename="sickbeard.db", suffix=None):
|
|
"""
|
|
@param filename: The sqlite database filename to use. If not specified,
|
|
will be made to be sickbeard.db
|
|
@param suffix: The suffix to append to the filename. A '.' will be added
|
|
automatically, i.e. suffix='v0' will make dbfile.db.v0
|
|
@return: the correct location of the database file.
|
|
"""
|
|
if suffix:
|
|
filename = "%s.%s" % (filename, suffix)
|
|
return ek.ek(os.path.join, sickbeard.DATA_DIR, filename)
|
|
|
|
|
|
class DBConnection(object):
|
|
def __init__(self, filename="sickbeard.db", suffix=None, row_type=None):
|
|
|
|
self.filename = filename
|
|
self.suffix = suffix
|
|
self.row_type = row_type
|
|
self.connection = None
|
|
|
|
try:
|
|
self.reconnect()
|
|
except Exception as e:
|
|
logger.log(u"DB error: " + ex(e), logger.ERROR)
|
|
raise
|
|
|
|
def reconnect(self):
|
|
"""Closes the existing database connection and re-opens it."""
|
|
self.close()
|
|
self.connection = sqlite3.connect(dbFilename(self.filename, self.suffix), 20, check_same_thread=False)
|
|
self.connection.isolation_level = None
|
|
|
|
if self.row_type == "dict":
|
|
self.connection.row_factory = self._dict_factory
|
|
else:
|
|
self.connection.row_factory = sqlite3.Row
|
|
|
|
def __del__(self):
|
|
self.close()
|
|
|
|
def _cursor(self):
|
|
"""Returns the cursor; reconnects if disconnected."""
|
|
if self.connection is None: self.reconnect()
|
|
return self.connection.cursor()
|
|
|
|
def execute(self, query, args=None, fetchall=False, fetchone=False):
|
|
"""Executes the given query, returning the lastrowid from the query."""
|
|
cursor = self._cursor()
|
|
|
|
try:
|
|
if fetchall:
|
|
return self._execute(cursor, query, args).fetchall()
|
|
elif fetchone:
|
|
return self._execute(cursor, query, args).fetchone()
|
|
else:
|
|
return self._execute(cursor, query, args)
|
|
finally:
|
|
cursor.close()
|
|
|
|
def _execute(self, cursor, query, args):
|
|
try:
|
|
if args == None:
|
|
return cursor.execute(query)
|
|
return cursor.execute(query, args)
|
|
except sqlite3.OperationalError as e:
|
|
logger.log(u"DB error: " + ex(e), logger.ERROR)
|
|
self.close()
|
|
raise
|
|
|
|
def checkDBVersion(self):
|
|
|
|
result = None
|
|
|
|
try:
|
|
if self.hasTable('db_version'):
|
|
result = self.select("SELECT db_version FROM db_version")
|
|
except:
|
|
return 0
|
|
|
|
if result:
|
|
return int(result[0]["db_version"])
|
|
else:
|
|
return 0
|
|
|
|
def mass_action(self, querylist, logTransaction=False):
|
|
|
|
with db_lock:
|
|
# remove None types
|
|
querylist = [i for i in querylist if i != None]
|
|
|
|
if querylist == None:
|
|
return
|
|
|
|
sqlResult = []
|
|
attempt = 0
|
|
|
|
while attempt < 5:
|
|
try:
|
|
for qu in querylist:
|
|
if len(qu) == 1:
|
|
if logTransaction:
|
|
logger.log(qu[0], logger.DEBUG)
|
|
sqlResult.append(self.execute(qu[0]))
|
|
elif len(qu) > 1:
|
|
if logTransaction:
|
|
logger.log(qu[0] + " with args " + str(qu[1]), logger.DEBUG)
|
|
sqlResult.append(self.execute(qu[0], qu[1]))
|
|
|
|
logger.log(u"Transaction with " + str(len(querylist)) + u" queries executed", logger.DEBUG)
|
|
|
|
# finished
|
|
break
|
|
except sqlite3.OperationalError, e:
|
|
sqlResult = []
|
|
if self.connection:
|
|
self.connection.rollback()
|
|
if "unable to open database file" in e.args[0] or "database is locked" in e.args[0]:
|
|
logger.log(u"DB error: " + ex(e), logger.WARNING)
|
|
attempt += 1
|
|
time.sleep(1)
|
|
else:
|
|
logger.log(u"DB error: " + ex(e), logger.ERROR)
|
|
raise
|
|
except sqlite3.DatabaseError, e:
|
|
sqlResult = []
|
|
if self.connection:
|
|
self.connection.rollback()
|
|
logger.log(u"Fatal error executing query: " + ex(e), logger.ERROR)
|
|
raise
|
|
|
|
time.sleep(0.05)
|
|
|
|
return sqlResult
|
|
|
|
def action(self, query, args=None, fetchall=False, fetchone=False):
|
|
|
|
with db_lock:
|
|
|
|
if query == None:
|
|
return
|
|
|
|
sqlResult = None
|
|
attempt = 0
|
|
|
|
while attempt < 5:
|
|
try:
|
|
if args == None:
|
|
logger.log(self.filename + ": " + query, logger.DB)
|
|
else:
|
|
logger.log(self.filename + ": " + query + " with args " + str(args), logger.DB)
|
|
|
|
sqlResult = self.execute(query, args, fetchall=fetchall, fetchone=fetchone)
|
|
|
|
# get out of the connection attempt loop since we were successful
|
|
break
|
|
except sqlite3.OperationalError, e:
|
|
if "unable to open database file" in e.args[0] or "database is locked" in e.args[0]:
|
|
logger.log(u"DB error: " + ex(e), logger.WARNING)
|
|
attempt += 1
|
|
time.sleep(1)
|
|
else:
|
|
logger.log(u"DB error: " + ex(e), logger.ERROR)
|
|
raise
|
|
except sqlite3.DatabaseError, e:
|
|
logger.log(u"Fatal error executing query: " + ex(e), logger.ERROR)
|
|
raise
|
|
|
|
time.sleep(0.05)
|
|
|
|
return sqlResult
|
|
|
|
def select(self, query, args=None):
|
|
|
|
sqlResults = self.action(query, args, fetchall=True)
|
|
|
|
if sqlResults == None:
|
|
return []
|
|
|
|
return sqlResults
|
|
|
|
def selectOne(self, query, args=None):
|
|
|
|
sqlResults = self.action(query, args, fetchone=True)
|
|
|
|
if sqlResults == None:
|
|
return []
|
|
|
|
return sqlResults
|
|
|
|
def upsert(self, tableName, valueDict, keyDict):
|
|
|
|
changesBefore = self.connection.total_changes
|
|
|
|
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
|
|
|
|
query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(
|
|
genParams(keyDict))
|
|
|
|
self.action(query, valueDict.values() + keyDict.values())
|
|
|
|
if self.connection.total_changes == changesBefore:
|
|
query = "INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \
|
|
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
|
self.action(query, valueDict.values() + keyDict.values())
|
|
|
|
def tableInfo(self, tableName):
|
|
|
|
# FIXME ? binding is not supported here, but I cannot find a way to escape a string manually
|
|
sqlResult = self.select("PRAGMA table_info(%s)" % tableName)
|
|
columns = {}
|
|
for column in sqlResult:
|
|
columns[column['name']] = {'type': column['type']}
|
|
return columns
|
|
|
|
# http://stackoverflow.com/questions/3300464/how-can-i-get-dict-from-sqlite-query
|
|
def _dict_factory(self, cursor, row):
|
|
d = {}
|
|
for idx, col in enumerate(cursor.description):
|
|
d[col[0]] = row[idx]
|
|
return d
|
|
|
|
def hasTable(self, tableName):
|
|
return len(self.select("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, ))) > 0
|
|
|
|
def close(self):
|
|
"""Close database connection"""
|
|
if getattr(self, "connection", None) is not None:
|
|
self.connection.close()
|
|
self.connection = None
|
|
|
|
def sanityCheckDatabase(connection, sanity_check):
|
|
sanity_check(connection).check()
|
|
|
|
|
|
class DBSanityCheck(object):
|
|
def __init__(self, connection):
|
|
self.connection = connection
|
|
|
|
def check(self):
|
|
pass
|
|
|
|
|
|
# ===============
|
|
# = Upgrade API =
|
|
# ===============
|
|
|
|
def upgradeDatabase(connection, schema):
|
|
logger.log(u"Checking database structure...", logger.MESSAGE)
|
|
_processUpgrade(connection, schema)
|
|
|
|
|
|
def prettyName(class_name):
|
|
return ' '.join([x.group() for x in re.finditer("([A-Z])([a-z0-9]+)", class_name)])
|
|
|
|
|
|
def restoreDatabase(version):
|
|
logger.log(u"Restoring database before trying upgrade again")
|
|
if not sickbeard.helpers.restoreVersionedFile(dbFilename(suffix='v' + str(version)), version):
|
|
logger.log_error_and_exit(u"Database restore failed, abort upgrading database")
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def _processUpgrade(connection, upgradeClass):
|
|
instance = upgradeClass(connection)
|
|
logger.log(u"Checking " + prettyName(upgradeClass.__name__) + " database upgrade", logger.DEBUG)
|
|
if not instance.test():
|
|
logger.log(u"Database upgrade required: " + prettyName(upgradeClass.__name__), logger.MESSAGE)
|
|
try:
|
|
instance.execute()
|
|
except sqlite3.DatabaseError, e:
|
|
# attemping to restore previous DB backup and perform upgrade
|
|
try:
|
|
instance.execute()
|
|
except:
|
|
restored = False
|
|
result = connection.select("SELECT db_version FROM db_version")
|
|
if result:
|
|
version = int(result[0]["db_version"])
|
|
if restoreDatabase(version):
|
|
# initialize the main SB database
|
|
upgradeDatabase(DBConnection(), sickbeard.mainDB.InitialSchema)
|
|
restored = True
|
|
|
|
if not restored:
|
|
print "Error in " + str(upgradeClass.__name__) + ": " + ex(e)
|
|
raise
|
|
logger.log(upgradeClass.__name__ + " upgrade completed", logger.DEBUG)
|
|
else:
|
|
logger.log(upgradeClass.__name__ + " upgrade not required", logger.DEBUG)
|
|
|
|
for upgradeSubClass in upgradeClass.__subclasses__():
|
|
_processUpgrade(connection, upgradeSubClass)
|
|
|
|
|
|
# Base migration class. All future DB changes should be subclassed from this class
|
|
class SchemaUpgrade(object):
|
|
def __init__(self, connection):
|
|
self.connection = connection
|
|
|
|
def hasTable(self, tableName):
|
|
return len(self.connection.select("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, ))) > 0
|
|
|
|
def hasColumn(self, tableName, column):
|
|
return column in self.connection.tableInfo(tableName)
|
|
|
|
def addColumn(self, table, column, type="NUMERIC", default=0):
|
|
self.connection.action("ALTER TABLE %s ADD %s %s" % (table, column, type))
|
|
self.connection.action("UPDATE %s SET %s = ?" % (table, column), (default,))
|
|
|
|
def checkDBVersion(self):
|
|
return self.connection.checkDBVersion()
|
|
|
|
def incDBVersion(self):
|
|
new_version = self.checkDBVersion() + 1
|
|
self.connection.action("UPDATE db_version SET db_version = ?", [new_version])
|
|
return new_version
|