import pathlib,subprocess,datetime,shutil,time,urllib.request,urllib.parse,hashlib,json,sqlite3 base=pathlib.Path('/www/dk_project/dk_app/trilium');release=pathlib.Path('/tmp/trilium-korean-phase9');m=json.loads((release/'manifest.json').read_text());digest=lambda p:hashlib.sha256(p.read_bytes()).hexdigest() meta='doc_notes/en/User Guide/!!!meta.json';pending=base/'data/korean-phase9-pending.json' assert digest(base/'patched/main.cjs')==m['previousMainSha256'] assert digest(base/'i18n_ko'/meta)==m['previousMetaSha256'] assert not pending.exists();assert not (base/'i18n_ko/doc_notes/en/api-ko').exists() for rel,h in m['files'].items():assert digest(release/rel)==h,rel backup=base/'backups'/('before-korean-phase9-'+datetime.datetime.now().strftime('%Y%m%d-%H%M%S'));backup.mkdir(parents=True,mode=0o700) shutil.copy2(base/'patched/main.cjs',backup/'main.cjs');shutil.copy2(base/'i18n_ko'/meta,backup/'meta.json') db=sqlite3.connect('file:'+str(base/'data/document.db')+'?mode=ro',uri=True) with sqlite3.connect(str(backup/'document.db')) as dest:db.backup(dest) db.close() for rel in m['files']: if rel.startswith('doc_notes/'): dest=base/'i18n_ko'/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes((release/rel).read_bytes()) shutil.copy2(release/'korean-phase9-pending.json',pending) (base/'patched/main.cjs').write_bytes((release/'main.cjs').read_bytes()) try: subprocess.run(['docker','restart','trilium'],check=True) for i in range(40): try: with urllib.request.urlopen('http://127.0.0.1:18082/api/health-check',timeout=2) as r:assert r.status==200 if pending.exists():raise RuntimeError('Migration pending') break except Exception: if i==39:raise time.sleep(1) checked=0 for rel,h in m['files'].items(): if not rel.startswith('doc_notes/'):continue with urllib.request.urlopen('http://127.0.0.1:18082/assets/v0.95.0-ko-20260913-r8/'+urllib.parse.quote(rel),timeout=5) as r:assert hashlib.sha256(r.read()).hexdigest()==h,rel checked+=1 for route,needle in [('/etapi/docs/','외부 API 참고 자료'),('/api/docs/','내부 API 참고 자료')]: with urllib.request.urlopen('http://127.0.0.1:18082'+route,timeout=5) as r:assert needle in r.read().decode() for route,source in [('/etapi/docs/swagger-ui-init.js','etapi.json'),('/api/docs/swagger-ui-init.js','internal.json')]: with urllib.request.urlopen('http://127.0.0.1:18082'+route,timeout=5) as r:text=r.read().decode() expected=json.loads((release/'doc_notes/en/api-ko'/source).read_text())['info']['description'];assert json.dumps(expected,ensure_ascii=False)[1:-1] in text,route db=sqlite3.connect('file:'+str(base/'data/document.db')+'?mode=ro',uri=True) for row in json.loads((release/'korean-phase9-pending.json').read_text()):assert db.execute('SELECT value FROM attributes WHERE attributeId=?',(row['attributeId'],)).fetchone()[0]==row['value'] db.close() result={'status':'verified','backup':str(backup),'servedFiles':checked,'swaggerPages':2,'scriptLinks':3,'assetVersion':'v0.95.0-ko-20260913-r8'} (base/'i18n_ko/phase9-result.json').write_text(json.dumps(result,indent=2));print(json.dumps(result)) except Exception: print('Verification failed; full backup preserved at '+str(backup));raise