extract_mes_icons_files.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. # -*- coding: utf-8 -*-
  2. """Extract mes_icons image mappings from large mysqldump; generate restore SQL."""
  3. from __future__ import print_function
  4. import os
  5. import re
  6. import sys
  7. DUMP = r"d:\产线\大洋\mes_cloud_20260919.sql\mes_cloud_20260919.sql"
  8. OUT_DIR = r"D:\IdeaProjects\mes-ahdy\mescloud\_apk_work\extracted"
  9. os.makedirs(OUT_DIR, exist_ok=True)
  10. CHUNK = 16 * 1024 * 1024
  11. def find_offsets(path, names):
  12. needles = [(("INSERT INTO `%s`" % n).encode("ascii"), n) for n in names]
  13. needles += [(("CREATE TABLE `%s`" % n).encode("ascii"), "CREATE:" + n) for n in names]
  14. keep = max(len(n) for n, _ in needles)
  15. buf = b""
  16. pos = 0
  17. found = []
  18. with open(path, "rb") as f:
  19. while True:
  20. chunk = f.read(CHUNK)
  21. if not chunk:
  22. break
  23. data = buf + chunk
  24. base = pos - len(buf)
  25. for needle, name in needles:
  26. start = 0
  27. while True:
  28. i = data.find(needle, start)
  29. if i < 0:
  30. break
  31. found.append((base + i, name))
  32. start = i + len(needle)
  33. buf = data[-keep:]
  34. pos += len(chunk)
  35. if pos % (512 * 1024 * 1024) < CHUNK:
  36. print("scanned %d MB, hits=%d" % (pos // (1024 * 1024), len(found)), flush=True)
  37. seen = set()
  38. out = []
  39. for p, name in sorted(found):
  40. if p in seen:
  41. continue
  42. seen.add(p)
  43. out.append((p, name))
  44. return out
  45. def read_until_stmt_end(path, offset, max_bytes=500 * 1024 * 1024):
  46. """Read SQL statement starting at offset until top-level ');' end (string-aware)."""
  47. with open(path, "rb") as f:
  48. f.seek(offset)
  49. parts = []
  50. total = 0
  51. in_str = False
  52. prev = None
  53. # We need to detect end: ) then ; outside string
  54. # Process incrementally with a small rolling window of decoded... keep as bytes, track quote state for '
  55. # MySQL dump uses \' escapes inside strings.
  56. while total < max_bytes:
  57. chunk = f.read(4 * 1024 * 1024)
  58. if not chunk:
  59. break
  60. parts.append(chunk)
  61. total += len(chunk)
  62. data = b"".join(parts)
  63. i = 0
  64. # Re-scan whole each time is O(n^2) — better keep state across chunks
  65. # Restart parse from 0 each time is bad for GB. Use stateful scan on new chunk only.
  66. break
  67. # Stateful:
  68. with open(path, "rb") as f:
  69. f.seek(offset)
  70. out = bytearray()
  71. in_str = False
  72. total = 0
  73. while total < max_bytes:
  74. chunk = f.read(1024 * 1024)
  75. if not chunk:
  76. break
  77. i = 0
  78. while i < len(chunk):
  79. c = chunk[i]
  80. out.append(c)
  81. total += 1
  82. if in_str:
  83. if c == 0x5C and i + 1 < len(chunk): # backslash
  84. out.append(chunk[i + 1])
  85. i += 2
  86. total += 1
  87. continue
  88. if c == 0x5C and i + 1 >= len(chunk):
  89. # escape at boundary: pull next byte
  90. nxt = f.read(1)
  91. if nxt:
  92. out.append(nxt[0])
  93. total += 1
  94. i += 1
  95. continue
  96. if c == 0x27: # '
  97. in_str = False
  98. i += 1
  99. continue
  100. else:
  101. if c == 0x27:
  102. in_str = True
  103. i += 1
  104. continue
  105. # end of statement: );
  106. if c == 0x3B and len(out) >= 2 and out[-2] == 0x29: # );
  107. return out.decode("utf-8", "replace")
  108. i += 1
  109. return out.decode("utf-8", "replace")
  110. def parse_create_columns(stmt):
  111. m = re.search(r"CREATE TABLE `[^`]+`\s*\((.*)\)\s*ENGINE", stmt, re.S | re.I)
  112. if not m:
  113. return []
  114. cols = []
  115. for line in m.group(1).split("\n"):
  116. cm = re.match(r"\s*`([^`]+)`\s+", line)
  117. if cm:
  118. cols.append(cm.group(1))
  119. return cols
  120. def iter_value_rows(path, insert_offset):
  121. """
  122. Yield list-of-field-strings for each VALUES row of an INSERT statement.
  123. Does not load whole statement into memory.
  124. """
  125. with open(path, "rb") as f:
  126. f.seek(insert_offset)
  127. # skip to VALUES
  128. buf = b""
  129. while True:
  130. chunk = f.read(1024 * 1024)
  131. if not chunk:
  132. return
  133. buf += chunk
  134. m = re.search(br"\bVALUES\b", buf, re.I)
  135. if m:
  136. # position after VALUES
  137. start_in_buf = m.end()
  138. # leftover after VALUES
  139. leftover = buf[start_in_buf:]
  140. break
  141. # keep tail
  142. if len(buf) > 64:
  143. buf = buf[-64:]
  144. # Now stream leftover + rest of file for rows
  145. def byte_iter(first):
  146. for b in first:
  147. yield b
  148. while True:
  149. ch = f.read(1024 * 1024)
  150. if not ch:
  151. break
  152. for b in ch:
  153. yield b
  154. it = byte_iter(leftover)
  155. # parse rows
  156. try:
  157. while True:
  158. # skip whitespace and commas
  159. c = next(it)
  160. while c in (0x20, 0x09, 0x0A, 0x0D, 0x2C):
  161. c = next(it)
  162. if c == 0x3B: # ;
  163. return
  164. if c != 0x28: # (
  165. # unexpected
  166. return
  167. fields = []
  168. cur = bytearray()
  169. in_str = False
  170. while True:
  171. c = next(it)
  172. if in_str:
  173. if c == 0x5C:
  174. cur.append(c)
  175. cur.append(next(it))
  176. continue
  177. cur.append(c)
  178. if c == 0x27:
  179. in_str = False
  180. continue
  181. if c == 0x27:
  182. in_str = True
  183. cur.append(c)
  184. continue
  185. if c == 0x2C: # ,
  186. fields.append(cur.decode("utf-8", "replace").strip())
  187. cur = bytearray()
  188. continue
  189. if c == 0x29: # )
  190. fields.append(cur.decode("utf-8", "replace").strip())
  191. yield fields
  192. break
  193. cur.append(c)
  194. except StopIteration:
  195. return
  196. def unquote(s):
  197. if s is None:
  198. return None
  199. s = s.strip()
  200. if s.upper() == "NULL":
  201. return None
  202. if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
  203. inner = s[1:-1]
  204. inner = (
  205. inner.replace("\\\\", "\\")
  206. .replace("\\'", "'")
  207. .replace('\\"', '"')
  208. .replace("\\n", "\n")
  209. .replace("\\r", "\r")
  210. .replace("\\t", "\t")
  211. .replace("\\0", "\0")
  212. .replace("\\Z", "\x1a")
  213. )
  214. return inner
  215. return s
  216. def sql_quote(s):
  217. if s is None:
  218. return "NULL"
  219. return "'" + str(s).replace("\\", "\\\\").replace("'", "''") + "'"
  220. def row_to_dict(fields, cols):
  221. d = {}
  222. for i, v in enumerate(fields):
  223. key = cols[i] if i < len(cols) else "col%d" % i
  224. d[key] = unquote(v)
  225. return d
  226. def main():
  227. size_gb = os.path.getsize(DUMP) / (1024.0 ** 3)
  228. print("Dump size: %.2f GB" % size_gb, flush=True)
  229. names = ["mes_icons", "js_sys_file_upload", "js_sys_file_entity"]
  230. hits = find_offsets(DUMP, names)
  231. for off, name in hits:
  232. print(" %s @ %d" % (name, off), flush=True)
  233. create_cols = {}
  234. inserts = {}
  235. for off, name in hits:
  236. if name.startswith("CREATE:"):
  237. t = name.split(":", 1)[1]
  238. print("Reading CREATE %s..." % t, flush=True)
  239. stmt = read_until_stmt_end(DUMP, off, max_bytes=200 * 1024)
  240. create_cols[t] = parse_create_columns(stmt)
  241. print(" cols=%d: %s" % (len(create_cols[t]), create_cols[t][:12]), flush=True)
  242. else:
  243. inserts[name] = off
  244. icons_cols = create_cols.get("mes_icons") or []
  245. upload_cols = create_cols.get("js_sys_file_upload") or []
  246. entity_cols = create_cols.get("js_sys_file_entity") or []
  247. # 1) Load mes_icons (small)
  248. print("Loading mes_icons...", flush=True)
  249. icons = []
  250. for fields in iter_value_rows(DUMP, inserts["mes_icons"]):
  251. icons.append(row_to_dict(fields, icons_cols))
  252. print(" mes_icons rows=%d" % len(icons), flush=True)
  253. # 2) Stream file_upload, keep only mesIcons_image
  254. print("Scanning js_sys_file_upload for mesIcons_image...", flush=True)
  255. biz_type_idx = upload_cols.index("biz_type") if "biz_type" in upload_cols else None
  256. icon_uploads = []
  257. n = 0
  258. for fields in iter_value_rows(DUMP, inserts["js_sys_file_upload"]):
  259. n += 1
  260. if n % 200000 == 0:
  261. print(" scanned upload rows=%d kept=%d" % (n, len(icon_uploads)), flush=True)
  262. if biz_type_idx is not None:
  263. bt = unquote(fields[biz_type_idx] if biz_type_idx < len(fields) else None)
  264. if bt != "mesIcons_image":
  265. continue
  266. else:
  267. # fallback: any field equals mesIcons_image
  268. if not any(unquote(x) == "mesIcons_image" for x in fields):
  269. continue
  270. icon_uploads.append(row_to_dict(fields, upload_cols))
  271. print(" upload total scanned=%d, mesIcons_image=%d" % (n, len(icon_uploads)), flush=True)
  272. needed_ids = set(u["file_id"] for u in icon_uploads if u.get("file_id"))
  273. print(" needed file_entity ids=%d" % len(needed_ids), flush=True)
  274. # 3) Stream file_entity, keep needed
  275. print("Scanning js_sys_file_entity for needed ids...", flush=True)
  276. file_id_idx = entity_cols.index("file_id") if "file_id" in entity_cols else 0
  277. entities = {}
  278. n = 0
  279. for fields in iter_value_rows(DUMP, inserts["js_sys_file_entity"]):
  280. n += 1
  281. if n % 200000 == 0:
  282. print(" scanned entity rows=%d kept=%d" % (n, len(entities)), flush=True)
  283. fid = unquote(fields[file_id_idx] if file_id_idx < len(fields) else None)
  284. if fid in needed_ids:
  285. entities[fid] = row_to_dict(fields, entity_cols)
  286. if len(entities) == len(needed_ids):
  287. print(" all needed entities found early at row %d" % n, flush=True)
  288. break
  289. print(" entity scanned=%d kept=%d / needed=%d" % (n, len(entities), len(needed_ids)), flush=True)
  290. icon_by_id = {i["id"]: i for i in icons}
  291. # Mapping TSV
  292. map_path = os.path.join(OUT_DIR, "mes_icons_image_map_from_old.txt")
  293. with open(map_path, "w", encoding="utf-8") as wf:
  294. wf.write("icon_id\ttitle\tmode\tstatus\tupload_id\tfile_id\tfile_name\tfile_path\tfile_md5\text\tdisk_rel\n")
  295. for u in sorted(icon_uploads, key=lambda x: (x.get("biz_key") or "", x.get("id") or "")):
  296. e = entities.get(u.get("file_id") or "", {})
  297. ic = icon_by_id.get(u.get("biz_key") or "", {})
  298. path = (e.get("file_path") or "").replace("\\", "/")
  299. md5 = e.get("file_md5") or ""
  300. ext = e.get("file_extension") or ""
  301. disk = ("%s%s.%s" % (path, md5, ext)) if ext else ("%s%s" % (path, md5))
  302. wf.write(
  303. "\t".join(
  304. [
  305. u.get("biz_key") or "",
  306. ic.get("title") or "",
  307. ic.get("mode") or "",
  308. ic.get("status") or "",
  309. u.get("id") or "",
  310. u.get("file_id") or "",
  311. u.get("file_name") or "",
  312. path,
  313. md5,
  314. ext,
  315. disk,
  316. ]
  317. )
  318. + "\n"
  319. )
  320. print("Wrote", map_path, flush=True)
  321. # Restore SQL
  322. sql_path = os.path.join(OUT_DIR, "restore_mes_icons_image_mapping.sql")
  323. by_biz = {}
  324. for u in icon_uploads:
  325. by_biz.setdefault(u.get("biz_key"), []).append(u)
  326. with open(sql_path, "w", encoding="utf-8") as wf:
  327. wf.write("-- ============================================================\n")
  328. wf.write("-- Restore PDA功能模块(mes_icons) 图片映射,来源: mes_cloud_20260919.sql\n")
  329. wf.write("-- 关联: js_sys_file_upload.biz_key = mes_icons.id\n")
  330. wf.write("-- js_sys_file_upload.biz_type = 'mesIcons_image'\n")
  331. wf.write("-- js_sys_file_upload.file_id = js_sys_file_entity.file_id\n")
  332. wf.write("-- 注意: 还须把磁盘文件拷到新服务器 userfiles 下(见同目录 files_to_copy 列表)\n")
  333. wf.write("-- ============================================================\n")
  334. wf.write("SET NAMES utf8mb4;\nSTART TRANSACTION;\n\n")
  335. wf.write("-- 1) 写入/覆盖老库中的文件实体\n")
  336. for fid in sorted(needed_ids):
  337. e = entities.get(fid)
  338. if not e:
  339. wf.write("-- WARN missing entity file_id=%s\n" % fid)
  340. continue
  341. size = e.get("file_size")
  342. size_sql = size if size is not None else "NULL"
  343. wf.write(
  344. "INSERT INTO `js_sys_file_entity` "
  345. "(`file_id`,`file_md5`,`file_path`,`file_content_type`,`file_extension`,`file_size`,`file_meta`,`file_preview`) "
  346. "VALUES (%s,%s,%s,%s,%s,%s,%s,%s) "
  347. "ON DUPLICATE KEY UPDATE "
  348. "`file_md5`=VALUES(`file_md5`),`file_path`=VALUES(`file_path`),"
  349. "`file_content_type`=VALUES(`file_content_type`),`file_extension`=VALUES(`file_extension`),"
  350. "`file_size`=VALUES(`file_size`),`file_meta`=VALUES(`file_meta`),`file_preview`=VALUES(`file_preview`);\n"
  351. % (
  352. sql_quote(e.get("file_id")),
  353. sql_quote(e.get("file_md5")),
  354. sql_quote(e.get("file_path")),
  355. sql_quote(e.get("file_content_type")),
  356. sql_quote(e.get("file_extension")),
  357. size_sql,
  358. sql_quote(e.get("file_meta")),
  359. sql_quote(e.get("file_preview")),
  360. )
  361. )
  362. wf.write("\n-- 2) 按功能模块 id 替换图片绑定\n")
  363. for biz_key in sorted(k for k in by_biz if k):
  364. ic = icon_by_id.get(biz_key, {})
  365. wf.write(
  366. "\n-- id=%s title=%s mode=%s\n"
  367. % (biz_key, ic.get("title") or "", ic.get("mode") or "")
  368. )
  369. wf.write(
  370. "DELETE FROM `js_sys_file_upload` WHERE `biz_type`='mesIcons_image' AND `biz_key`=%s;\n"
  371. % sql_quote(biz_key)
  372. )
  373. for u in by_biz[biz_key]:
  374. sortv = u.get("file_sort")
  375. sort_sql = sortv if sortv is not None else "NULL"
  376. wf.write(
  377. "INSERT INTO `js_sys_file_upload` "
  378. "(`id`,`file_id`,`file_name`,`file_type`,`file_sort`,`biz_key`,`biz_type`,`status`,"
  379. "`create_by`,`create_date`,`update_by`,`update_date`,`remarks`) "
  380. "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);\n"
  381. % (
  382. sql_quote(u.get("id")),
  383. sql_quote(u.get("file_id")),
  384. sql_quote(u.get("file_name")),
  385. sql_quote(u.get("file_type")),
  386. sort_sql,
  387. sql_quote(u.get("biz_key")),
  388. sql_quote(u.get("biz_type")),
  389. sql_quote(u.get("status") or "0"),
  390. sql_quote(u.get("create_by") or "system"),
  391. sql_quote(u.get("create_date")),
  392. sql_quote(u.get("update_by") or "system"),
  393. sql_quote(u.get("update_date")),
  394. sql_quote(u.get("remarks")),
  395. )
  396. )
  397. missing = [
  398. i
  399. for i in icons
  400. if i.get("id") not in by_biz and (i.get("status") or "0") == "0"
  401. ]
  402. if missing:
  403. wf.write("\n-- 老库中正常状态但无图片的模块(未改绑定):\n")
  404. for i in missing:
  405. wf.write(
  406. "-- id=%s title=%s mode=%s\n"
  407. % (i.get("id"), i.get("title"), i.get("mode"))
  408. )
  409. wf.write("\nCOMMIT;\n")
  410. wf.write(
  411. "\n-- 校验:\n"
  412. "-- SELECT u.biz_key, i.title, i.mode, u.file_name, e.file_path, e.file_md5, e.file_extension\n"
  413. "-- FROM js_sys_file_upload u\n"
  414. "-- LEFT JOIN js_sys_file_entity e ON e.file_id = u.file_id\n"
  415. "-- LEFT JOIN mes_icons i ON i.id = u.biz_key\n"
  416. "-- WHERE u.biz_type = 'mesIcons_image'\n"
  417. "-- ORDER BY i.title;\n"
  418. )
  419. print("Wrote", sql_path, flush=True)
  420. files_list = os.path.join(OUT_DIR, "mes_icons_image_files_to_copy.txt")
  421. seen = set()
  422. with open(files_list, "w", encoding="utf-8") as wf:
  423. wf.write("# 相对路径(位于 {file.baseDir}/userfiles/ 下)\n")
  424. wf.write("# 实际文件名一般为: {file_path}{file_md5}.{file_extension}\n")
  425. for u in icon_uploads:
  426. e = entities.get(u.get("file_id") or "")
  427. if not e:
  428. continue
  429. path = (e.get("file_path") or "").replace("\\", "/")
  430. md5 = e.get("file_md5") or ""
  431. ext = e.get("file_extension") or ""
  432. rel = ("%s%s.%s" % (path, md5, ext)) if ext else ("%s%s" % (path, md5))
  433. if rel in seen:
  434. continue
  435. seen.add(rel)
  436. wf.write(rel + "\n")
  437. print("Wrote %s (%d unique files)" % (files_list, len(seen)), flush=True)
  438. print("DONE", flush=True)
  439. if __name__ == "__main__":
  440. main()