256 lines
9.4 KiB
Python
256 lines
9.4 KiB
Python
import re, argparse
|
|
import sqlite3 as sql
|
|
import logging
|
|
import logging.handlers
|
|
|
|
from os.path import isfile, exists
|
|
from urllib.parse import unquote
|
|
|
|
import sys, traceback
|
|
|
|
#------------------------------------------------+
|
|
# get_list_from_server_txt
|
|
#------------------------------------------------+
|
|
# Rows in the game server database are
|
|
# occasionally concatenated into one line.
|
|
# To simplify, they are unmerged.
|
|
#
|
|
# The final result is every row in the game server
|
|
# database with its own index in a list.
|
|
#------------------------------------------------+
|
|
|
|
def get_list_from_server_txt(filename):
|
|
def unmerge_rows(line, char, x):
|
|
chunks = line.split(char)
|
|
newrows = [char.join(chunks[:x]), char.join(chunks[x:])]
|
|
# need to prefix each row with a char.
|
|
# only the last row will be missing it.
|
|
newrows[-1] = char + newrows[-1]
|
|
if newrows[-1].count(char) > (x - 1):
|
|
newrows += unmerge_rows(newrows.pop(), char, x)
|
|
return newrows
|
|
rows = []
|
|
with open(filename, 'r') as f:
|
|
# server database has a lot of newlines to ignore
|
|
rows = [line for line in f if line != "\n"]
|
|
output = []
|
|
n = 3
|
|
backslash = '\\'
|
|
for row in rows:
|
|
# The first and last column is prefixed with a backslash.
|
|
# So multiple rows on one line should be split at the 3rd backslash.
|
|
if row.count(backslash) > (n - 1):
|
|
unmerged = unmerge_rows(row, backslash, n)
|
|
for u in unmerged:
|
|
output.append(u)
|
|
else:
|
|
output.append(row)
|
|
return output
|
|
|
|
def init_logging(folder, base_file_name="dbimport-%s.log"):
|
|
if not exists(folder):
|
|
return False
|
|
filename = folder + "/" + base_file_name
|
|
i = 0
|
|
while exists(filename % i):
|
|
i += 1
|
|
filename = filename % i
|
|
f = open(filename, mode='a', encoding='utf-8')
|
|
logging.basicConfig(stream=f, level=logging.DEBUG)
|
|
return filename
|
|
|
|
#------------------------------------------------+
|
|
# uid2namefix
|
|
#------------------------------------------------+
|
|
# Unlike other rows,
|
|
# the separator character, '/' is part of the value of the second column.
|
|
# so an ordinary match for '/' or '\' can not be done like the other types of rows.
|
|
# example from game server db:
|
|
# \/uid2name/Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=\Tuxxy
|
|
# it should become:
|
|
# ["uid2name", "Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=", "Tuxxy"]
|
|
|
|
def uid2namefix(row):
|
|
# quick fix
|
|
# replace first and last occurrence of backslash
|
|
# this results in [,/uid2name/cryptoid_fp, name]
|
|
e = re.sub(r'^([^\\]*)\\|\\(?=[^\\]*$)', ',', row)
|
|
# replace first two occurence of forward slash
|
|
# this results in [,,uid2name,cryptoid_fp, name]
|
|
ee = e.replace('/', ',', 2)
|
|
# split on comma
|
|
# but start from index 2 because the first commas are left over
|
|
# c is now a list of strings.
|
|
# ["uid2name", <crypto_idfp value>, <player name value>]
|
|
c = ee[2:].split(',')
|
|
c[2] = unquote(c[2])
|
|
c[2] = c[2].strip('\n')
|
|
return c
|
|
|
|
# O(n) and organize cts related data into list of rows.
|
|
def filters(db):
|
|
tt = [] # time (seconds)
|
|
tr = [] # ranks
|
|
ti = [] # id
|
|
# xonotic only stores one player per map
|
|
# for speed records (fastest player only)
|
|
s = [] # speed
|
|
sid = [] # speed id
|
|
rank_index = 2
|
|
for d in db:
|
|
if d.find("uid2name") != -1:
|
|
ti.append(uid2namefix(d))
|
|
else:
|
|
# regex:
|
|
# find substrings that do not contain backslash, forwardslash, or newline.
|
|
e = re.findall(r'[^\\/\n]+', d)
|
|
if d.find("cts100record/time") != -1:
|
|
e[rank_index] = int(e[rank_index].replace("time", ""))
|
|
tt.append(e)
|
|
elif d.find("cts100record/crypto_idfp") != -1:
|
|
e[3] = unquote(e[3])
|
|
e[rank_index] = int(e[rank_index].replace("crypto_idfp", ""))
|
|
tr.append(e)
|
|
elif d.find("cts100record/speed/speed") != -1:
|
|
# example:
|
|
# ['zeel-omnitek', 'cts100record', 'speed', 'speed', '1584.598511']
|
|
# --- note, index 1, 2, 3 are unneeded
|
|
s.append([ e[0], unquote(e[-1]) ])
|
|
elif d.find("cts100record/speed/crypto_idfp") != -1:
|
|
# example:
|
|
# ['minideck_cts_v4r4', 'cts100record', 'speed', 'crypto_idfp', 'duHTyaSGpdTk7oebwPFoo899xPoTwP9bja4DUjCjTLo%3D']
|
|
sid.append([ e[0], unquote(e[-1]) ])
|
|
return tt, tr, ti, s, sid
|
|
|
|
def insert_to_database(d, s):
|
|
def insert(c, q, d):
|
|
for x in d:
|
|
# possible to do executemany
|
|
# but want to be able to catch the problematic rows
|
|
# as it is iterated through.
|
|
# and proceed with adding OK rows.
|
|
try:
|
|
c.execute(q, x)
|
|
except sql.ProgrammingError as e:
|
|
print(e)
|
|
print(x)
|
|
return
|
|
|
|
con = sql.connect(d)
|
|
with con:
|
|
csr = con.cursor()
|
|
try:
|
|
times, \
|
|
ranks, \
|
|
ids, \
|
|
speed, \
|
|
speed_ids = filters(get_list_from_server_txt(s))
|
|
if times:
|
|
insert(csr, "INSERT OR REPLACE INTO Cts_times VALUES(?, ?, ?, ?)", times)
|
|
logging.info('\n'.join(y for y in [str(x) for x in times]))
|
|
if ranks:
|
|
insert(csr, "INSERT OR REPLACE INTO Cts_ranks VALUES(?, ?, ?, ?)", ranks)
|
|
logging.info('\n'.join(y for y in [str(x) for x in ranks]))
|
|
if ids:
|
|
insert(csr, "INSERT OR REPLACE INTO Id2alias VALUES(?, ?, ?)", ids)
|
|
logging.info('\n'.join(y for y in [str(x) for x in ids]))
|
|
if speed:
|
|
insert(csr, "INSERT OR REPLACE INTO Speed VALUES(?, ?)", speed)
|
|
if speed_ids:
|
|
insert(csr, "INSERT OR REPLACE INTO Fastest_players VALUES(?, ?)", speed_ids)
|
|
except sql.Error:
|
|
logging.exception("sql error encountered in function 'i'")
|
|
if con:
|
|
con.rollback()
|
|
|
|
def write_query(out_file, data):
|
|
if exists(out_file):
|
|
print("stopped: output file already exists", file=sys.stderr)
|
|
return False
|
|
times, \
|
|
ranks, \
|
|
ids, \
|
|
speed, \
|
|
speed_ids = filters(get_list_from_server_txt(data))
|
|
with open(out_file, 'w', encoding='utf-8') as file_handle:
|
|
for t in times:
|
|
file_handle.write("INSERT OR REPLACE INTO Cts_times VALUES(\'%s\', \'%s\', %s, %s);\n" % tuple(t))
|
|
for r in ranks:
|
|
file_handle.write("INSERT OR REPLACE INTO Cts_ranks VALUES(\'%s\', \'%s\', %s, \'%s\');\n" % tuple(r))
|
|
for i in ids:
|
|
file_handle.write("INSERT OR REPLACE INTO Id2alias VALUES(\'%s\', \'%s\', \'%s\');\n" % tuple(i))
|
|
return True
|
|
|
|
# Test whether repeat rows are added.
|
|
def check_duplicates(database, data):
|
|
c = sql.connect(database)
|
|
p = True
|
|
with c:
|
|
cs = c.cursor()
|
|
try:
|
|
logging.info("Inserting into database (1/2)")
|
|
insert_to_database(database, data)
|
|
logging.info("Querying (1/2)")
|
|
cs.execute("SELECT * FROM Cts_times")
|
|
a = cs.fetchall()
|
|
cs.execute("SELECT * FROM Cts_ranks")
|
|
b = cs.fetchall()
|
|
cs.execute("SELECT * FROM Id2alias")
|
|
c = cs.fetchall()
|
|
logging.info("Inserting into database (2/2)")
|
|
insert_to_database(database, data)
|
|
logging.info("Querying (2/2)")
|
|
cs.execute("SELECT * FROM Cts_times")
|
|
x = cs.fetchall()
|
|
cs.execute("SELECT * FROM Cts_ranks")
|
|
y = cs.fetchall()
|
|
cs.execute("SELECT * FROM Id2alias")
|
|
z = cs.fetchall()
|
|
if len(a) != len(x):
|
|
logging.error("Issue with Cts_times")
|
|
p = False
|
|
if len(b) != len(y):
|
|
logging.error("Issue with Cts_ranks")
|
|
p = False
|
|
if len(c) != len(z):
|
|
logging.error("Issue with Id2alias")
|
|
p = False
|
|
if p:
|
|
logging.info("Database ok - no repeat rows added.")
|
|
except sql.Error:
|
|
logging.exception("encountered sql error in function 'duplicate test'.")
|
|
|
|
if __name__ == "__main__":
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('dest',
|
|
help="destination, either an sqlite3 database or query file")
|
|
ap.add_argument('src',
|
|
help="source, should be data generated by a Xonotic server")
|
|
ap.add_argument('-t', '--test',
|
|
action='store_true',
|
|
help="test database for duplicates")
|
|
ap.add_argument('-q', '--export-query',
|
|
action='store_true',
|
|
help="write query file (as opposed to executing / inserting rows into database)")
|
|
ap.add_argument('-l', '--log',
|
|
type=str,
|
|
help="set folder to store log files")
|
|
args = ap.parse_args()
|
|
log_dir = args.log or "logs"
|
|
log_file = init_logging(log_dir)
|
|
if log_file:
|
|
print("writing log to folder '%s'," % log_dir, log_file, file=sys.stderr)
|
|
else:
|
|
print("exited: logging not initialized (folder '%s' does not exist)" % log_dir, file=sys.stderr)
|
|
exit()
|
|
try:
|
|
if args.test:
|
|
check_duplicates(args.dest, args.src)
|
|
if args.export_query:
|
|
write_query(args.dest, args.src)
|
|
else:
|
|
insert_to_database(args.dest, args.src)
|
|
except FileNotFoundError:
|
|
traceback.print_exc()
|
|
print("\n\t exited: no input file to work with.", file=sys.stderr)
|