50 lines
2.7 KiB
Python
50 lines
2.7 KiB
Python
"""Consistent logical backup of the complete current MySQL database; no DB writes."""
|
|
import gzip, hashlib, json, os, pathlib, time
|
|
import pymysql
|
|
from db_access import connect
|
|
os.umask(0o077)
|
|
root=pathlib.Path('/opt/lingniu-go-native/backups/mileage-reconciliation-20260916')
|
|
root.mkdir(parents=True,exist_ok=True)
|
|
path=root/'database.sql.gz'
|
|
if path.exists(): raise RuntimeError('Backup already exists; do not overwrite')
|
|
c=connect(); manifest={'started_at':time.strftime('%Y-%m-%dT%H:%M:%S%z'),'tables':{}}
|
|
with c.cursor() as q:
|
|
q.execute('SELECT DATABASE() AS db'); db=q.fetchone()['db'];manifest['database']=db
|
|
q.execute('SHOW FULL TABLES'); entries=q.fetchall()
|
|
tables=[list(r.values())[0] for r in entries if list(r.values())[1]=='BASE TABLE']
|
|
q.execute('SHOW TRIGGERS'); triggers=q.fetchall()
|
|
q.execute('START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY')
|
|
def literal(v):
|
|
if isinstance(v,bytes): return '0x'+v.hex()
|
|
return c.escape(v)
|
|
with gzip.open(str(path)+'.partial','wt',encoding='utf-8',compresslevel=3) as f:
|
|
f.write('SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n')
|
|
for table in tables:
|
|
q.execute('SHOW CREATE TABLE `'+table+'`');ddl=list(q.fetchone().values())[1]
|
|
f.write('DROP TABLE IF EXISTS `'+table+'`;\n'+ddl+';\n')
|
|
q.execute('SHOW COLUMNS FROM `'+table+'`');cols=[r['Field'] for r in q.fetchall() if 'GENERATED' not in r['Extra']]
|
|
names=','.join('`'+v+'`' for v in cols);count=0
|
|
with c.cursor(pymysql.cursors.SSCursor) as stream:
|
|
stream.execute('SELECT '+names+' FROM `'+table+'`')
|
|
while True:
|
|
rows=stream.fetchmany(500)
|
|
if not rows:break
|
|
f.write('INSERT INTO `'+table+'` ('+names+') VALUES\n'+',\n'.join('('+','.join(literal(v) for v in row)+')' for row in rows)+';\n')
|
|
count+=len(rows)
|
|
manifest['tables'][table]=count
|
|
print(json.dumps({'table':table,'rows':count}),flush=True)
|
|
for row in entries:
|
|
if list(row.values())[1]=='VIEW':
|
|
name=list(row.values())[0];q.execute('SHOW CREATE VIEW `'+name+'`');ddl=q.fetchone()['Create View'];f.write(ddl+';\n')
|
|
for trigger in triggers:
|
|
q.execute('SHOW CREATE TRIGGER `'+trigger['Trigger']+'`');ddl=q.fetchone()['SQL Original Statement'];f.write('DELIMITER ;;\n'+ddl+';;\nDELIMITER ;\n')
|
|
f.write('SET FOREIGN_KEY_CHECKS=1;\n')
|
|
c.rollback();c.close()
|
|
os.rename(str(path)+'.partial',path)
|
|
h=hashlib.sha256()
|
|
with path.open('rb') as f:
|
|
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
|
manifest.update(sha256=h.hexdigest(),bytes=path.stat().st_size,finished_at=time.strftime('%Y-%m-%dT%H:%M:%S%z'))
|
|
(root/'manifest.json').write_text(json.dumps(manifest,indent=2))
|
|
print(json.dumps({'complete':True,'bytes':manifest['bytes'],'sha256':manifest['sha256']}),flush=True)
|