2023-02-16 05:06:42 +00:00
import logging , coloredlogs
2023-04-15 05:31:52 +00:00
from typing import Optional , Dict , List
2023-02-16 05:06:42 +00: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 05:04:47 +00:00
import importlib , os
import secrets , string
import bcrypt
2023-02-16 05:06:42 +00:00
from hashlib import sha256
from core . config import CoreConfig
from core . data . schema import *
2023-02-19 19:52:20 +00:00
from core . utils import Utils
2023-02-16 05:06:42 +00:00
2023-03-09 16:38:58 +00:00
2023-02-16 05:06:42 +00:00
class Data :
2023-07-08 04:01:52 +00:00
current_schema_version = 4
engine = None
session = None
user = None
arcade = None
card = None
base = None
2023-02-16 05:06:42 +00:00
def __init__ ( self , cfg : CoreConfig ) - > None :
self . config = cfg
if self . config . database . sha2_password :
passwd = sha256 ( self . config . database . password . encode ( ) ) . digest ( )
2023-07-15 04:15:14 +00:00
self . __url = f " { self . config . database . protocol } :// { self . config . database . username } : { passwd . hex ( ) } @ { self . config . database . host } : { self . config . database . port } / { self . config . database . name } ?charset=utf8mb4 "
2023-02-16 05:06:42 +00:00
else :
2023-07-15 04:15:14 +00:00
self . __url = f " { self . config . database . protocol } :// { self . config . database . username } : { self . config . database . password } @ { self . config . database . host } : { self . config . database . port } / { self . config . database . name } ?charset=utf8mb4 "
2023-03-09 16:38:58 +00:00
2023-07-08 04:01:52 +00:00
if Data . engine is None :
Data . engine = create_engine ( self . __url , pool_recycle = 3600 )
self . __engine = Data . engine
2023-02-16 05:06:42 +00:00
2023-07-08 04:01:52 +00:00
if Data . session is None :
s = sessionmaker ( bind = Data . engine , autoflush = True , autocommit = True )
Data . session = scoped_session ( s )
if Data . user is None :
Data . user = UserData ( self . config , self . session )
if Data . arcade is None :
Data . arcade = ArcadeData ( self . config , self . session )
if Data . card is None :
Data . card = CardData ( self . config , self . session )
if Data . base is None :
Data . base = BaseData ( self . config , self . session )
2023-02-16 05:06:42 +00:00
self . logger = logging . getLogger ( " database " )
# Prevent the logger from adding handlers multiple times
2023-07-08 04:01:52 +00:00
if not getattr ( self . logger , " handler_set " , None ) :
log_fmt_str = " [ %(asctime)s ] %(levelname)s | Database | %(message)s "
log_fmt = logging . Formatter ( log_fmt_str )
2023-03-09 16:38:58 +00:00
fileHandler = TimedRotatingFileHandler (
" {0} / {1} .log " . format ( self . config . server . log_dir , " db " ) ,
encoding = " utf-8 " ,
when = " d " ,
backupCount = 10 ,
)
2023-02-16 05:06:42 +00:00
fileHandler . setFormatter ( log_fmt )
2023-03-09 16:38:58 +00:00
2023-02-16 05:06:42 +00: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 16:38:58 +00:00
coloredlogs . install (
cfg . database . loglevel , logger = self . logger , fmt = log_fmt_str
)
self . logger . handler_set = True # type: ignore
2023-02-16 05:06:42 +00:00
2023-02-19 19:52:20 +00: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 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
games = Utils . get_all_titles ( )
for game_dir , game_mod in games . items ( ) :
try :
2023-04-24 01:04:52 +00:00
if hasattr ( game_mod , " database " ) and hasattr (
game_mod , " current_schema_version "
) :
2023-03-12 05:26:48 +00:00
game_mod . database ( self . config )
2023-02-19 19:52:20 +00:00
metadata . create_all ( self . __engine . connect ( ) )
2023-07-15 04:15:14 +00:00
self . base . touch_schema_ver (
2023-03-09 16:38:58 +00:00
game_mod . current_schema_version , game_mod . game_codes [ 0 ]
)
2023-02-19 19:52:20 +00:00
except Exception as e :
2023-03-09 16:38:58 +00:00
self . logger . warning (
f " Could not load database schema from { game_dir } - { e } "
)
2023-04-15 04:13:04 +00:00
self . logger . info ( f " Setting base_schema_ver to { self . current_schema_version } " )
self . base . set_schema_ver ( self . current_schema_version )
2023-02-19 19:52:20 +00:00
2023-03-09 16:38:58 +00: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 19:52:20 +00: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 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
for root , dirs , files in os . walk ( " ./titles " ) :
2023-03-09 16:38:58 +00:00
for dir in dirs :
2023-02-19 19:52:20 +00:00
if not dir . startswith ( " __ " ) :
try :
mod = importlib . import_module ( f " titles. { dir } " )
2023-03-09 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
try :
2023-03-12 05:26:48 +00:00
if hasattr ( mod , " database " ) :
mod . database ( self . config )
2023-02-19 19:52:20 +00:00
metadata . drop_all ( self . __engine . connect ( ) )
except Exception as e :
2023-03-09 16:38:58 +00:00
self . logger . warning (
f " Could not load database schema from { dir } - { e } "
)
2023-02-19 19:52:20 +00:00
except ImportError as e :
2023-03-09 16:38:58 +00:00
self . logger . warning (
f " Failed to load database schema dir { dir } - { e } "
)
2023-02-19 19:52:20 +00:00
break
2023-03-09 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
self . base . execute ( " SET FOREIGN_KEY_CHECKS=1 " )
self . create_database ( )
2023-03-09 16:38:58 +00:00
2023-04-15 04:13:04 +00:00
def migrate_database ( self , game : str , version : Optional [ int ] , action : str ) - > None :
2023-02-19 19:52:20 +00:00
old_ver = self . base . get_schema_ver ( game )
sql = " "
2023-04-15 04:13:04 +00:00
if version is None :
if not game == " CORE " :
titles = Utils . get_all_titles ( )
2023-04-24 01:04:52 +00:00
2023-04-15 04:13:04 +00:00
for folder , mod in titles . items ( ) :
2023-04-24 01:04:52 +00:00
if not mod . game_codes [ 0 ] == game :
continue
2023-04-15 05:31:52 +00:00
if hasattr ( mod , " current_schema_version " ) :
version = mod . current_schema_version
2023-04-24 01:04:52 +00:00
2023-04-15 05:31:52 +00:00
else :
2023-04-24 01:04:52 +00:00
self . logger . warn (
f " current_schema_version not found for { folder } "
)
2023-04-15 04:13:04 +00:00
else :
version = self . current_schema_version
2023-04-24 01:04:52 +00:00
2023-04-15 04:13:04 +00:00
if version is None :
2023-04-24 01:04:52 +00:00
self . logger . warn (
f " Could not determine latest version for { game } , please specify --version "
)
2023-03-09 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
if old_ver is None :
2023-03-09 16:38:58 +00:00
self . logger . error (
f " Schema for game { game } does not exist, did you run the creation script? "
)
2023-02-19 19:52:20 +00:00
return
2023-03-09 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
if old_ver == version :
2023-03-09 16:38:58 +00:00
self . logger . info (
f " Schema for game { game } is already version { old_ver } , nothing to do "
)
2023-02-19 19:52:20 +00:00
return
2023-03-09 16:38:58 +00:00
2023-03-18 06:12:58 +00: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
2023-04-24 01:04:52 +00:00
2023-03-18 06:12:58 +00:00
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 16:38:58 +00:00
2023-02-19 19:52:20 +00: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 16:38:58 +00:00
2023-02-19 19:52:20 +00:00
self . logger . info ( f " Successfully migrated { game } to schema version { version } " )
2023-03-04 05:04:47 +00:00
def create_owner ( self , email : Optional [ str ] = None ) - > None :
2023-03-09 16:38:58 +00:00
pw = " " . join (
secrets . choice ( string . ascii_letters + string . digits ) for i in range ( 20 )
)
2023-03-04 05:04:47 +00: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 16:38:58 +00: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 05:04:47 +00: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 16:38:58 +00:00
2023-03-04 05:04:47 +00: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 16:38:58 +00:00
2023-03-04 05:04:47 +00:00
if not should_force :
2023-03-09 16:38:58 +00: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 05:04:47 +00:00
return
2023-03-09 16:38:58 +00: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 05:04:47 +00: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 16:38:58 +00:00
self . user . delete_user ( hanging_user [ " id " ] )
2023-03-04 05:04:47 +00: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 16:38:58 +00:00
2023-03-04 05:04:47 +00:00
for user in unreg_users :
2023-03-09 16:38:58 +00:00
cards = self . card . get_user_cards ( user [ " id " ] )
2023-03-04 05:04:47 +00: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 16:38:58 +00:00
self . user . delete_user ( user [ " id " ] )
2023-03-18 06:12:58 +00:00
def autoupgrade ( self ) - > None :
2023-04-15 05:31:52 +00:00
all_game_versions = self . base . get_all_schema_vers ( )
if all_game_versions is None :
2023-03-18 06:12:58 +00:00
self . logger . warn ( " Failed to get schema versions " )
2023-04-15 05:31:52 +00:00
return
2023-04-24 01:04:52 +00:00
2023-04-15 05:31:52 +00:00
all_games = Utils . get_all_titles ( )
all_games_list : Dict [ str , int ] = { }
for _ , mod in all_games . items ( ) :
if hasattr ( mod , " current_schema_version " ) :
all_games_list [ mod . game_codes [ 0 ] ] = mod . current_schema_version
2023-04-24 01:04:52 +00:00
2023-04-15 05:31:52 +00:00
for x in all_game_versions :
2023-04-15 07:13:14 +00:00
failed = False
2023-03-18 06:12:58 +00:00
game = x [ " game " ] . upper ( )
2023-04-15 05:31:52 +00:00
update_ver = int ( x [ " version " ] )
latest_ver = all_games_list . get ( game , 1 )
if game == " CORE " :
latest_ver = self . current_schema_version
2023-04-24 01:04:52 +00:00
if update_ver == latest_ver :
2023-04-15 05:31:52 +00:00
self . logger . info ( f " { game } is already latest version " )
continue
2023-04-24 01:04:52 +00:00
2023-04-15 05:31:52 +00:00
for y in range ( update_ver + 1 , latest_ver + 1 ) :
2023-03-18 06:12:58 +00:00
if os . path . exists ( f " core/data/schema/versions/ { game } _ { y } _upgrade.sql " ) :
2023-04-15 05:31:52 +00:00
with open (
2023-04-24 01:04:52 +00:00
f " core/data/schema/versions/ { game } _ { y } _upgrade.sql " ,
" r " ,
encoding = " utf-8 " ,
2023-04-15 05:31:52 +00:00
) as f :
sql = f . read ( )
result = self . base . execute ( sql )
if result is None :
2023-04-24 01:04:52 +00:00
self . logger . error (
f " Error execuing sql script for game { game } v { y } ! "
)
2023-04-15 07:13:14 +00:00
failed = True
break
2023-03-18 06:12:58 +00:00
else :
2023-04-15 05:31:52 +00:00
self . logger . warning ( f " Could not find script { game } _ { y } _upgrade.sql " )
2023-04-15 07:13:14 +00:00
failed = True
2023-04-24 01:04:52 +00:00
if not failed :
self . base . set_schema_ver ( latest_ver , game )
2023-06-24 17:14:40 +00:00
def show_versions ( self ) - > None :
all_game_versions = self . base . get_all_schema_vers ( )
for ver in all_game_versions :
self . logger . info ( f " { ver [ ' game ' ] } -> v { ver [ ' version ' ] } " )