2023-02-16 05:06:42 +00:00
import logging , coloredlogs
2024-01-09 18:57:59 +00:00
from typing import Optional
2023-02-16 05:06:42 +00:00
from sqlalchemy . orm import scoped_session , sessionmaker
from sqlalchemy import create_engine
from logging . handlers import TimedRotatingFileHandler
2024-01-09 18:57:59 +00:00
import os
2023-03-04 05:04:47 +00:00
import secrets , string
import bcrypt
2023-02-16 05:06:42 +00:00
from hashlib import sha256
2024-01-09 18:57:59 +00:00
import alembic . config
2024-01-09 22:11:49 +00:00
import glob
2023-02-16 05:06:42 +00:00
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
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
2024-01-09 18:57:59 +00:00
def __alembic_cmd ( self , command : str , * args : str ) - > None :
old_dir = os . path . abspath ( os . path . curdir )
base_dir = os . path . join ( os . path . abspath ( os . path . curdir ) , ' core ' , ' data ' , ' alembic ' )
alembicArgs = [
" -c " ,
os . path . join ( base_dir , " alembic.ini " ) ,
" -x " ,
f " script_location= { base_dir } " ,
" -x " ,
f " sqlalchemy.url= { self . __url } " ,
command ,
]
alembicArgs . extend ( args )
os . chdir ( base_dir )
alembic . config . main ( argv = alembicArgs )
os . chdir ( old_dir )
2023-02-19 19:52:20 +00:00
def create_database ( self ) :
self . logger . info ( " Creating databases... " )
2024-01-09 18:57:59 +00:00
metadata . create_all (
self . engine ,
checkfirst = True ,
)
2023-02-19 19:52:20 +00:00
2024-01-09 18:57:59 +00:00
for _ , mod in Utils . get_all_titles ( ) . items ( ) :
if hasattr ( mod , " database " ) :
mod . database ( self . config )
metadata . create_all (
self . engine ,
checkfirst = True ,
2023-03-09 16:38:58 +00:00
)
2024-01-09 18:57:59 +00:00
# Stamp the end revision as if alembic had created it, so it can take off after this.
self . __alembic_cmd (
" stamp " ,
" head " ,
2023-03-09 16:38:58 +00:00
)
2024-01-09 18:57:59 +00:00
def schema_upgrade ( self , ver : str = None ) :
self . __alembic_cmd (
" upgrade " ,
2024-03-04 05:50:51 +00:00
" head " if not ver else ver ,
)
def schema_downgrade ( self , ver : str ) :
self . __alembic_cmd (
" downgrade " ,
ver ,
2024-01-09 18:57:59 +00:00
)
2023-03-04 05:04:47 +00:00
2024-01-09 18:57:59 +00:00
async def create_owner ( self , email : Optional [ str ] = None , code : Optional [ str ] = " 00000000000000000000 " ) - > 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 ( ) )
2024-03-02 22:48:48 +00:00
user_id = await self . user . create_user ( username = " sysowner " , email = email , password = hash . decode ( ) , permission = 255 )
2023-03-04 05:04:47 +00:00
if user_id is None :
self . logger . error ( f " Failed to create owner with email { email } " )
return
2024-01-09 18:57:59 +00:00
card_id = await self . card . create_card ( user_id , code )
2023-03-04 05:04:47 +00:00
if card_id is None :
self . logger . error ( f " Failed to create card for owner with id { user_id } " )
return
2023-08-08 14:17:56 +00:00
self . logger . warning (
2024-01-09 18:57:59 +00:00
f " Successfully created owner with email { email } , access code { code } , and password { pw } Make sure to change this password and assign a real card ASAP! "
2023-03-09 16:38:58 +00:00
)
2024-01-09 18:57:59 +00:00
async def migrate ( self ) - > None :
exist = await self . base . execute ( " SELECT * FROM alembic_version " )
if exist is not None :
self . logger . warn ( " No need to migrate as you have already migrated to alembic. If you are trying to upgrade the schema, use `upgrade` instead! " )
2023-03-04 05:04:47 +00:00
return
2024-01-09 18:57:59 +00:00
2024-01-09 22:11:49 +00:00
self . logger . info ( " Upgrading to latest with legacy system " )
if not await self . legacy_upgrade ( ) :
self . logger . warn ( " No need to migrate as you have already deleted the old schema_versions system. If you are trying to upgrade the schema, use `upgrade` instead! " )
return
self . logger . info ( " Done " )
2024-01-09 18:57:59 +00:00
self . logger . info ( " Stamp with initial revision " )
self . __alembic_cmd (
" stamp " ,
" 835b862f9bf0 " ,
2023-03-09 16:38:58 +00:00
)
2023-04-24 01:04:52 +00:00
2024-01-09 18:57:59 +00:00
self . logger . info ( " Upgrade " )
self . __alembic_cmd (
" upgrade " ,
" head " ,
)
2024-01-09 22:11:49 +00:00
async def legacy_upgrade ( self ) - > bool :
vers = await self . base . execute ( " SELECT * FROM schema_versions " )
if vers is None :
self . logger . warn ( " Cannot legacy upgrade, schema_versions table unavailable! " )
return False
db_vers = { }
2024-01-12 01:39:29 +00:00
vers_list = vers . fetchall ( )
for x in vers_list :
2024-01-09 22:11:49 +00:00
db_vers [ x [ ' game ' ] ] = x [ ' version ' ]
core_now_ver = int ( db_vers [ ' CORE ' ] ) + 1
while os . path . exists ( f " core/data/schema/versions/CORE_ { core_now_ver } _upgrade.sql " ) :
with open ( f " core/data/schema/versions/CORE_ { core_now_ver } _upgrade.sql " , " r " ) as f :
result = await self . base . execute ( f . read ( ) )
if result is None :
self . logger . error ( f " Invalid upgrade script CORE_ { core_now_ver } _upgrade.sql " )
break
result = await self . base . execute ( f " UPDATE schema_versions SET version = { core_now_ver } WHERE game = ' CORE ' " )
if result is None :
self . logger . error ( f " Failed to update schema version for CORE to { core_now_ver } " )
break
self . logger . info ( f " Upgrade CORE to version { core_now_ver } " )
core_now_ver + = 1
for _ , mod in Utils . get_all_titles ( ) . items ( ) :
game_codes = getattr ( mod , " game_codes " , [ ] )
for game in game_codes :
if game not in db_vers :
self . logger . warn ( f " { game } does not have an antry in schema_versions, skipping " )
continue
now_ver = int ( db_vers [ game ] ) + 1
while os . path . exists ( f " core/data/schema/versions/ { game } _ { now_ver } _upgrade.sql " ) :
with open ( f " core/data/schema/versions/ { game } _ { now_ver } _upgrade.sql " , " r " ) as f :
result = await self . base . execute ( f . read ( ) )
if result is None :
self . logger . error ( f " Invalid upgrade script { game } _ { now_ver } _upgrade.sql " )
break
result = await self . base . execute ( f " UPDATE schema_versions SET version = { now_ver } WHERE game = ' { game } ' " )
if result is None :
self . logger . error ( f " Failed to update schema version for { game } to { now_ver } " )
break
self . logger . info ( f " Upgrade { game } to version { now_ver } " )
now_ver + = 1
return True
2023-04-24 01:04:52 +00:00
2024-01-12 01:48:27 +00:00
async def create_revision ( self , message : str ) - > None :
if not message :
self . logger . info ( " Message is required for create-revision " )
return
self . __alembic_cmd (
" revision " ,
" -m " ,
message ,
)