2023-02-16 06:06:42 +01:00
import logging , coloredlogs
2023-03-04 06:04:47 +01:00
from typing import Optional
2023-02-16 06:06:42 +01:00
from sqlalchemy . orm import scoped_session , sessionmaker
from sqlalchemy . exc import SQLAlchemyError
from sqlalchemy import create_engine
from logging . handlers import TimedRotatingFileHandler
2023-03-04 06:04:47 +01:00
import importlib , os
import secrets , string
import bcrypt
2023-02-16 06:06:42 +01:00
from hashlib import sha256
from core . config import CoreConfig
from core . data . schema import *
2023-02-19 20:52:20 +01:00
from core . utils import Utils
2023-02-16 06:06:42 +01:00
2023-03-09 17:38:58 +01:00
2023-02-16 06:06:42 +01:00
class Data :
def __init__ ( self , cfg : CoreConfig ) - > None :
self . config = cfg
if self . config . database . sha2_password :
passwd = sha256 ( self . config . database . password . encode ( ) ) . digest ( )
self . __url = f " { self . config . database . protocol } :// { self . config . database . username } : { passwd . hex ( ) } @ { self . config . database . host } / { self . config . database . name } ?charset=utf8mb4 "
else :
self . __url = f " { self . config . database . protocol } :// { self . config . database . username } : { self . config . database . password } @ { self . config . database . host } / { self . config . database . name } ?charset=utf8mb4 "
2023-03-09 17:38:58 +01:00
2023-02-16 06:06:42 +01:00
self . __engine = create_engine ( self . __url , pool_recycle = 3600 )
session = sessionmaker ( bind = self . __engine , autoflush = True , autocommit = True )
self . session = scoped_session ( session )
self . user = UserData ( self . config , self . session )
self . arcade = ArcadeData ( self . config , self . session )
self . card = CardData ( self . config , self . session )
self . base = BaseData ( self . config , self . session )
2023-03-04 03:31:23 +01:00
self . schema_ver_latest = 4
2023-02-16 06:06:42 +01:00
log_fmt_str = " [ %(asctime)s ] %(levelname)s | Database | %(message)s "
log_fmt = logging . Formatter ( log_fmt_str )
self . logger = logging . getLogger ( " database " )
# Prevent the logger from adding handlers multiple times
2023-03-09 17:38:58 +01:00
if not getattr ( self . logger , " handler_set " , None ) :
fileHandler = TimedRotatingFileHandler (
" {0} / {1} .log " . format ( self . config . server . log_dir , " db " ) ,
encoding = " utf-8 " ,
when = " d " ,
backupCount = 10 ,
)
2023-02-16 06:06:42 +01:00
fileHandler . setFormatter ( log_fmt )
2023-03-09 17:38:58 +01:00
2023-02-16 06:06:42 +01:00
consoleHandler = logging . StreamHandler ( )
consoleHandler . setFormatter ( log_fmt )
self . logger . addHandler ( fileHandler )
self . logger . addHandler ( consoleHandler )
self . logger . setLevel ( self . config . database . loglevel )
2023-03-09 17:38:58 +01:00
coloredlogs . install (
cfg . database . loglevel , logger = self . logger , fmt = log_fmt_str
)
self . logger . handler_set = True # type: ignore
2023-02-16 06:06:42 +01:00
2023-02-19 20:52:20 +01:00
def create_database ( self ) :
self . logger . info ( " Creating databases... " )
try :
metadata . create_all ( self . __engine . connect ( ) )
except SQLAlchemyError as e :
self . logger . error ( f " Failed to create databases! { e } " )
return
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
games = Utils . get_all_titles ( )
for game_dir , game_mod in games . items ( ) :
try :
2023-03-13 03:37:44 +01:00
if hasattr ( game_mod , " database " ) and hasattr ( game_mod , " current_schema_version " ) :
2023-03-12 06:26:48 +01:00
game_mod . database ( self . config )
2023-02-19 20:52:20 +01:00
metadata . create_all ( self . __engine . connect ( ) )
2023-03-09 17:38:58 +01:00
self . base . set_schema_ver (
game_mod . current_schema_version , game_mod . game_codes [ 0 ]
)
2023-02-19 20:52:20 +01:00
except Exception as e :
2023-03-09 17:38:58 +01:00
self . logger . warning (
f " Could not load database schema from { game_dir } - { e } "
)
2023-02-19 20:52:20 +01:00
self . logger . info ( f " Setting base_schema_ver to { self . schema_ver_latest } " )
self . base . set_schema_ver ( self . schema_ver_latest )
2023-03-09 17:38:58 +01:00
self . logger . info (
f " Setting user auto_incrememnt to { self . config . database . user_table_autoincrement_start } "
)
self . user . reset_autoincrement (
self . config . database . user_table_autoincrement_start
)
2023-02-19 20:52:20 +01:00
def recreate_database ( self ) :
self . logger . info ( " Dropping all databases... " )
self . base . execute ( " SET FOREIGN_KEY_CHECKS=0 " )
try :
metadata . drop_all ( self . __engine . connect ( ) )
except SQLAlchemyError as e :
self . logger . error ( f " Failed to drop databases! { e } " )
return
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
for root , dirs , files in os . walk ( " ./titles " ) :
2023-03-09 17:38:58 +01:00
for dir in dirs :
2023-02-19 20:52:20 +01:00
if not dir . startswith ( " __ " ) :
try :
mod = importlib . import_module ( f " titles. { dir } " )
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
try :
2023-03-12 06:26:48 +01:00
if hasattr ( mod , " database " ) :
mod . database ( self . config )
2023-02-19 20:52:20 +01:00
metadata . drop_all ( self . __engine . connect ( ) )
except Exception as e :
2023-03-09 17:38:58 +01:00
self . logger . warning (
f " Could not load database schema from { dir } - { e } "
)
2023-02-19 20:52:20 +01:00
except ImportError as e :
2023-03-09 17:38:58 +01:00
self . logger . warning (
f " Failed to load database schema dir { dir } - { e } "
)
2023-02-19 20:52:20 +01:00
break
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
self . base . execute ( " SET FOREIGN_KEY_CHECKS=1 " )
self . create_database ( )
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
def migrate_database ( self , game : str , version : int , action : str ) - > None :
old_ver = self . base . get_schema_ver ( game )
sql = " "
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
if old_ver is None :
2023-03-09 17:38:58 +01:00
self . logger . error (
f " Schema for game { game } does not exist, did you run the creation script? "
)
2023-02-19 20:52:20 +01:00
return
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
if old_ver == version :
2023-03-09 17:38:58 +01:00
self . logger . info (
f " Schema for game { game } is already version { old_ver } , nothing to do "
)
2023-02-19 20:52:20 +01:00
return
2023-03-09 17:38:58 +01:00
2023-03-18 07:12:58 +01:00
if action == " upgrade " :
for x in range ( old_ver , version ) :
if not os . path . exists (
f " core/data/schema/versions/ { game . upper ( ) } _ { x + 1 } _ { action } .sql "
) :
self . logger . error (
f " Could not find { action } script { game . upper ( ) } _ { x + 1 } _ { action } .sql in core/data/schema/versions folder "
)
return
with open (
f " core/data/schema/versions/ { game . upper ( ) } _ { x + 1 } _ { action } .sql " ,
" r " ,
encoding = " utf-8 " ,
) as f :
sql = f . read ( )
result = self . base . execute ( sql )
if result is None :
self . logger . error ( " Error execuing sql script! " )
return None
else :
for x in range ( old_ver , version , - 1 ) :
if not os . path . exists (
f " core/data/schema/versions/ { game . upper ( ) } _ { x - 1 } _ { action } .sql "
) :
self . logger . error (
f " Could not find { action } script { game . upper ( ) } _ { x - 1 } _ { action } .sql in core/data/schema/versions folder "
)
return
with open (
f " core/data/schema/versions/ { game . upper ( ) } _ { x - 1 } _ { action } .sql " ,
" r " ,
encoding = " utf-8 " ,
) as f :
sql = f . read ( )
result = self . base . execute ( sql )
if result is None :
self . logger . error ( " Error execuing sql script! " )
return None
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
result = self . base . set_schema_ver ( version , game )
if result is None :
self . logger . error ( " Error setting version in schema_version table! " )
return None
2023-03-09 17:38:58 +01:00
2023-02-19 20:52:20 +01:00
self . logger . info ( f " Successfully migrated { game } to schema version { version } " )
2023-03-04 06:04:47 +01:00
def create_owner ( self , email : Optional [ str ] = None ) - > None :
2023-03-09 17:38:58 +01:00
pw = " " . join (
secrets . choice ( string . ascii_letters + string . digits ) for i in range ( 20 )
)
2023-03-04 06:04:47 +01:00
hash = bcrypt . hashpw ( pw . encode ( ) , bcrypt . gensalt ( ) )
user_id = self . user . create_user ( email = email , permission = 255 , password = hash )
if user_id is None :
self . logger . error ( f " Failed to create owner with email { email } " )
return
card_id = self . card . create_card ( user_id , " 00000000000000000000 " )
if card_id is None :
self . logger . error ( f " Failed to create card for owner with id { user_id } " )
return
2023-03-09 17:38:58 +01:00
self . logger . warn (
f " Successfully created owner with email { email } , access code 00000000000000000000, and password { pw } Make sure to change this password and assign a real card ASAP! "
)
2023-03-04 06:04:47 +01:00
def migrate_card ( self , old_ac : str , new_ac : str , should_force : bool ) - > None :
if old_ac == new_ac :
self . logger . error ( " Both access codes are the same! " )
return
2023-03-09 17:38:58 +01:00
2023-03-04 06:04:47 +01:00
new_card = self . card . get_card_by_access_code ( new_ac )
if new_card is None :
self . card . update_access_code ( old_ac , new_ac )
return
2023-03-09 17:38:58 +01:00
2023-03-04 06:04:47 +01:00
if not should_force :
2023-03-09 17:38:58 +01:00
self . logger . warn (
f " Card already exists for access code { new_ac } (id { new_card [ ' id ' ] } ). If you wish to continue, rerun with the ' --force ' flag. "
f " All exiting data on the target card { new_ac } will be perminently erased and replaced with data from card { old_ac } . "
)
2023-03-04 06:04:47 +01:00
return
2023-03-09 17:38:58 +01:00
self . logger . info (
f " All exiting data on the target card { new_ac } will be perminently erased and replaced with data from card { old_ac } . "
)
2023-03-04 06:04:47 +01:00
self . card . delete_card ( new_card [ " id " ] )
self . card . update_access_code ( old_ac , new_ac )
hanging_user = self . user . get_user ( new_card [ " user " ] )
if hanging_user [ " password " ] is None :
self . logger . info ( f " Delete hanging user { hanging_user [ ' id ' ] } " )
2023-03-09 17:38:58 +01:00
self . user . delete_user ( hanging_user [ " id " ] )
2023-03-04 06:04:47 +01:00
def delete_hanging_users ( self ) - > None :
"""
Finds and deletes users that have not registered for the webui that have no cards assocated with them .
"""
unreg_users = self . user . get_unregistered_users ( )
if unreg_users is None :
self . logger . error ( " Error occoured finding unregistered users " )
2023-03-09 17:38:58 +01:00
2023-03-04 06:04:47 +01:00
for user in unreg_users :
2023-03-09 17:38:58 +01:00
cards = self . card . get_user_cards ( user [ " id " ] )
2023-03-04 06:04:47 +01:00
if cards is None :
self . logger . error ( f " Error getting cards for user { user [ ' id ' ] } " )
continue
if not cards :
self . logger . info ( f " Delete hanging user { user [ ' id ' ] } " )
2023-03-09 17:38:58 +01:00
self . user . delete_user ( user [ " id " ] )
2023-03-18 07:12:58 +01:00
def autoupgrade ( self ) - > None :
all_games = self . base . get_all_schema_vers ( )
if all_games is None :
self . logger . warn ( " Failed to get schema versions " )
for x in all_games :
game = x [ " game " ] . upper ( )
update_ver = 1
for y in range ( 2 , 100 ) :
if os . path . exists ( f " core/data/schema/versions/ { game } _ { y } _upgrade.sql " ) :
update_ver = y
else :
break
self . migrate_database ( game , update_ver , " upgrade " )