import-from-xon.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import re, argparse
  2. import sqlite3 as sql
  3. import logging
  4. import logging.handlers
  5. from os.path import isfile, exists
  6. from urllib.parse import unquote
  7. import sys, traceback
  8. #------------------------------------------------+
  9. # get_list_from_server_txt
  10. #------------------------------------------------+
  11. # Rows in the game server database are
  12. # occasionally concatenated into one line.
  13. # To simplify, they are unmerged.
  14. #
  15. # The final result is every row in the game server
  16. # database with its own index in a list.
  17. #------------------------------------------------+
  18. def get_list_from_server_txt(filename):
  19. def unmerge_rows(line, char, x):
  20. chunks = line.split(char)
  21. newrows = [char.join(chunks[:x]), char.join(chunks[x:])]
  22. # need to prefix each row with a char.
  23. # only the last row will be missing it.
  24. newrows[-1] = char + newrows[-1]
  25. if newrows[-1].count(char) > (x - 1):
  26. newrows += unmerge_rows(newrows.pop(), char, x)
  27. return newrows
  28. rows = []
  29. with open(filename, 'r') as f:
  30. # server database has a lot of newlines to ignore
  31. rows = [line for line in f if line != "\n"]
  32. output = []
  33. n = 3
  34. backslash = '\\'
  35. for row in rows:
  36. # The first and last column is prefixed with a backslash.
  37. # So multiple rows on one line should be split at the 3rd backslash.
  38. if row.count(backslash) > (n - 1):
  39. unmerged = unmerge_rows(row, backslash, n)
  40. for u in unmerged:
  41. output.append(u)
  42. else:
  43. output.append(row)
  44. return output
  45. def init_logging(folder, base_file_name="dbimport-%s.log"):
  46. if not exists(folder):
  47. return False
  48. filename = "%s/%s" % (folder, base_file_name)
  49. i = 0
  50. while exists(filename % i):
  51. i += 1
  52. filename = filename % i
  53. f = open(filename, mode='a', encoding='utf-8')
  54. logging.basicConfig(stream=f, level=logging.DEBUG)
  55. return filename
  56. #------------------------------------------------+
  57. # uid2namefix
  58. #------------------------------------------------+
  59. # Unlike other rows,
  60. # the separator character, '/' is part of the value of the second column.
  61. # so an ordinary match for '/' or '\' can not be done like the other types of rows.
  62. # example from game server db:
  63. # \/uid2name/Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=\Tuxxy
  64. # it should become:
  65. # ["uid2name", "Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=", "Tuxxy"]
  66. def uid2namefix(row):
  67. # quick fix
  68. # replace first and last occurrence of backslash
  69. # this results in [,/uid2name/cryptoid_fp, name]
  70. e = re.sub(r'^([^\\]*)\\|\\(?=[^\\]*$)', ',', row)
  71. # replace first two occurence of forward slash
  72. # this results in [,,uid2name,cryptoid_fp, name]
  73. ee = e.replace('/', ',', 2)
  74. # split on comma
  75. # but start from index 2 because the first commas are left over
  76. # c is now a list of strings.
  77. # ["uid2name", <crypto_idfp value>, <player name value>]
  78. c = ee[2:].split(',')
  79. c[2] = unquote(c[2])
  80. c[2] = c[2].strip('\n')
  81. return c
  82. # O(n) and organize cts related data into list of rows.
  83. def filters(db):
  84. tt = [] # time (seconds)
  85. tr = [] # ranks
  86. ti = [] # id
  87. # xonotic only stores one player per map
  88. # for speed records (fastest player only)
  89. s = [] # speed
  90. sid = [] # speed id
  91. rank_index = 2
  92. for d in db:
  93. if d.find("uid2name") != -1:
  94. ti.append(uid2namefix(d))
  95. else:
  96. # regex:
  97. # find substrings that do not contain backslash, forwardslash, or newline.
  98. e = re.findall(r'[^\\/\n]+', d)
  99. if d.find("cts100record/time") != -1:
  100. e[rank_index] = int(e[rank_index].replace("time", ""))
  101. tt.append(e)
  102. elif d.find("cts100record/crypto_idfp") != -1:
  103. e[3] = unquote(e[3])
  104. e[rank_index] = int(e[rank_index].replace("crypto_idfp", ""))
  105. tr.append(e)
  106. elif d.find("cts100record/speed/speed") != -1:
  107. # example:
  108. # ['zeel-omnitek', 'cts100record', 'speed', 'speed', '1584.598511']
  109. # --- note, index 1, 2, 3 are unneeded
  110. s.append([ e[0], unquote(e[-1]) ])
  111. elif d.find("cts100record/speed/crypto_idfp") != -1:
  112. # example:
  113. # ['minideck_cts_v4r4', 'cts100record', 'speed', 'crypto_idfp', 'duHTyaSGpdTk7oebwPFoo899xPoTwP9bja4DUjCjTLo%3D']
  114. sid.append([ e[0], unquote(e[-1]) ])
  115. return tt, tr, ti, s, sid
  116. def insert_to_database(d, s):
  117. def insert(c, q, d):
  118. for x in d:
  119. # possible to do executemany
  120. # but want to be able to catch the problematic rows
  121. # as it is iterated through.
  122. # and proceed with adding OK rows.
  123. try:
  124. c.execute(q, x)
  125. except sql.ProgrammingError as e:
  126. print(e)
  127. print(x)
  128. return
  129. con = sql.connect(d)
  130. with con:
  131. csr = con.cursor()
  132. try:
  133. times, \
  134. ranks, \
  135. ids, \
  136. speed, \
  137. speed_ids = filters(get_list_from_server_txt(s))
  138. if times:
  139. insert(csr, "INSERT OR REPLACE INTO Cts_times VALUES(?, ?, ?, ?)", times)
  140. logging.info('\n'.join(y for y in [str(x) for x in times]))
  141. if ranks:
  142. insert(csr, "INSERT OR REPLACE INTO Cts_ranks VALUES(?, ?, ?, ?)", ranks)
  143. logging.info('\n'.join(y for y in [str(x) for x in ranks]))
  144. if ids:
  145. insert(csr, "INSERT OR REPLACE INTO Id2alias VALUES(?, ?, ?)", ids)
  146. logging.info('\n'.join(y for y in [str(x) for x in ids]))
  147. if speed:
  148. insert(csr, "INSERT OR REPLACE INTO Speed VALUES(?, ?)", speed)
  149. if speed_ids:
  150. insert(csr, "INSERT OR REPLACE INTO Fastest_players VALUES(?, ?)", speed_ids)
  151. except sql.Error:
  152. logging.exception("sql error encountered in function 'i'")
  153. if con:
  154. con.rollback()
  155. def write_query(out_file, data):
  156. if exists(out_file):
  157. print("stopped: output file already exists", file=sys.stderr)
  158. return False
  159. times, \
  160. ranks, \
  161. ids, \
  162. speed, \
  163. speed_ids = filters(get_list_from_server_txt(data))
  164. with open(out_file, 'w', encoding='utf-8') as file_handle:
  165. for t in times:
  166. file_handle.write("INSERT OR REPLACE INTO Cts_times VALUES(\'%s\', \'%s\', %s, %s);\n" % tuple(t))
  167. for r in ranks:
  168. file_handle.write("INSERT OR REPLACE INTO Cts_ranks VALUES(\'%s\', \'%s\', %s, \'%s\');\n" % tuple(r))
  169. for i in ids:
  170. file_handle.write("INSERT OR REPLACE INTO Id2alias VALUES(\'%s\', \'%s\', \'%s\');\n" % tuple(i))
  171. return True
  172. # Test whether repeat rows are added.
  173. def check_duplicates(database, data):
  174. c = sql.connect(database)
  175. p = True
  176. with c:
  177. cs = c.cursor()
  178. try:
  179. logging.info("Inserting into database (1/2)")
  180. insert_to_database(database, data)
  181. logging.info("Querying (1/2)")
  182. cs.execute("SELECT * FROM Cts_times")
  183. a = cs.fetchall()
  184. cs.execute("SELECT * FROM Cts_ranks")
  185. b = cs.fetchall()
  186. cs.execute("SELECT * FROM Id2alias")
  187. c = cs.fetchall()
  188. logging.info("Inserting into database (2/2)")
  189. insert_to_database(database, data)
  190. logging.info("Querying (2/2)")
  191. cs.execute("SELECT * FROM Cts_times")
  192. x = cs.fetchall()
  193. cs.execute("SELECT * FROM Cts_ranks")
  194. y = cs.fetchall()
  195. cs.execute("SELECT * FROM Id2alias")
  196. z = cs.fetchall()
  197. if len(a) != len(x):
  198. logging.error("Issue with Cts_times")
  199. p = False
  200. if len(b) != len(y):
  201. logging.error("Issue with Cts_ranks")
  202. p = False
  203. if len(c) != len(z):
  204. logging.error("Issue with Id2alias")
  205. p = False
  206. if p:
  207. logging.info("Database ok - no repeat rows added.")
  208. except sql.Error:
  209. logging.exception("encountered sql error in function 'duplicate test'.")
  210. if __name__ == "__main__":
  211. ap = argparse.ArgumentParser()
  212. ap.add_argument('dest',
  213. help="destination, a sqlite3 database (or query file, if given -q flag)")
  214. ap.add_argument('src',
  215. help="source, should be data generated by a Xonotic server")
  216. ap.add_argument('-q', '--export-query',
  217. action='store_true',
  218. help="write query file (as opposed to executing / inserting rows into database)")
  219. ap.add_argument('-t', '--test',
  220. action='store_true',
  221. help="test database for duplicates")
  222. ap.add_argument('-l', '--log-dir',
  223. type=str,
  224. help="set folder to store log files")
  225. args = ap.parse_args()
  226. log_dir = args.log_dir or "logs"
  227. log_file = init_logging(log_dir)
  228. if log_file:
  229. print("writing log to folder '%s'," % log_dir, log_file, file=sys.stderr)
  230. else:
  231. print("exited: logging not initialized (folder '%s' does not exist)" % log_dir, file=sys.stderr)
  232. exit()
  233. try:
  234. if args.test:
  235. check_duplicates(args.dest, args.src)
  236. if args.export_query:
  237. write_query(args.dest, args.src)
  238. else:
  239. insert_to_database(args.dest, args.src)
  240. except FileNotFoundError:
  241. traceback.print_exc()
  242. print("\n\t exited: no input file to work with.", file=sys.stderr)