Buckets:
| """Download-back SHA checks, JSON parsing, and independent browser media decoding. | |
| Do not run concurrently with verify_games.py (shared temporary stage). | |
| """ | |
| import os,sys,json,hashlib,shutil,threading,importlib.util,re | |
| from pathlib import Path | |
| from urllib.parse import urljoin,urlparse,unquote | |
| from playwright.sync_api import sync_playwright | |
| H=Path('/home/user/game-archive');R=H/'reports';sys.argv=[sys.argv[0]] | |
| spec=importlib.util.spec_from_file_location('v',H/'tools/verify_games.py');v=importlib.util.module_from_spec(spec);spec.loader.exec_module(v) | |
| ids=[180,181,182,183,359,373,374,376,575,582];declared={x['game_id']:x for x in json.loads((R/'next-batch-declared-assets.json').read_text())};results=[] | |
| for ident in ids: | |
| m=json.loads((R/f'game-{ident}.json').read_text());files={f['path']:f for f in m['files']};report={'id':ident,'name':m['name'],'files':len(files),'scope':'Stored payload integrity and media format validation; not a full playthrough.'};shutil.rmtree(v.STAGE,ignore_errors=True);v.STAGE.mkdir();server=None | |
| try: | |
| v.api.download_bucket_files(v.BUCKET,[(k,v.STAGE/k) for k in files],raise_on_missing_files=True) | |
| hashes=[];bad_json=[];json_checked=0;map_references=[] | |
| for key,f in files.items(): | |
| data=(v.STAGE/key).read_bytes();sha=hashlib.sha256(data).hexdigest();hashes.append({'path':key,'bytes':len(data),'sha256':sha,'size_matches':len(data)==f['bytes'],'recorded_sha256_matches':sha==f['sha256'] if f.get('sha256') else None}) | |
| if key.endswith('.json'): | |
| try: | |
| doc=json.loads(data.decode('utf-8-sig'));json_checked+=1 | |
| if isinstance(doc,dict) and 'tilesets' in doc and 'layers' in doc: | |
| for t in doc['tilesets']: | |
| if t.get('source'): | |
| resolved='site'+unquote(urlparse(urljoin('https://garbsoftball.com/'+key.removeprefix('site/'),t['source'])).path) | |
| map_references.append({'map':key,'tileset':resolved,'stored':resolved in files}) | |
| except Exception as e:bad_json.append({'path':key,'error':str(e)[:180]}) | |
| expected=declared[ident]['explicit_expected_paths'];report.update(sha256_files=hashes,recorded_sha256_comparisons=sum(x['recorded_sha256_matches'] is not None for x in hashes),sha256_mismatches=[x['path'] for x in hashes if x['recorded_sha256_matches'] is False],size_mismatches=[x['path'] for x in hashes if not x['size_matches']],json_parsed=json_checked,json_errors=bad_json,declared_paths=len(expected),missing_declared_paths=[x['path'] for x in expected if x['path'] not in files],map_tileset_references=map_references) | |
| (v.STAGE/'site/__asset_validation__.html').write_text('<!doctype html><meta charset="utf-8"><title>Asset validation fixture</title>') | |
| state=v.State(m);state.repair=False;server=v.ThreadingHTTPServer(('127.0.0.1',0),v.Handler);server.state=state;server.daemon_threads=False;threading.Thread(target=server.serve_forever,daemon=True).start() | |
| media=[{'path':'/'+k.removeprefix('site/'),'kind':'audio' if re.search(r'\.(mp3|ogg|m4a|webm|wav)$',k,re.I) else 'image'} for k in files if k.startswith('site/') and re.search(r'\.(mp3|ogg|m4a|webm|wav|png|jpe?g|gif|webp|svg)$',k,re.I)] | |
| with sync_playwright() as p: | |
| b=p.chromium.launch(headless=True,args=['--no-sandbox','--disable-dev-shm-usage','--js-flags=--expose-gc']);c=b.new_context(service_workers='block');origin=f'http://127.0.0.1:{server.server_port}' | |
| c.route('**/*',lambda route:route.continue_() if route.request.url.startswith(origin+'/') else route.abort());page=c.new_page();page.goto(origin+'/__asset_validation__.html') | |
| report['media']=page.evaluate('''async(list)=>{const ctx=new AudioContext();let results=[];async function check(f){try{if(f.kind==='audio'){const r=await fetch(f.path);if(!r.ok)throw Error('HTTP '+r.status);const data=await r.arrayBuffer();const a=await ctx.decodeAudioData(data);let peak=0;const samples=a.getChannelData(0);for(let i=0;i<samples.length;i+=Math.max(1,Math.floor(samples.length/4096)))peak=Math.max(peak,Math.abs(samples[i]));return {...f,decoded:true,duration:a.duration,frames:a.length,channels:a.numberOfChannels,sampled_peak:peak};}else{const img=new Image();img.src=f.path;await img.decode();return {...f,decoded:true,width:img.naturalWidth,height:img.naturalHeight};}}catch(e){return {...f,decoded:false,error:String(e)};}}for(const f of list){results.push(await check(f));if(window.gc)window.gc();}await ctx.close();return results;}''',media);b.close() | |
| report['audio_files']=sum(x['kind']=='audio' for x in report['media']);report['image_files']=sum(x['kind']=='image' for x in report['media']);report['media_decode_failures']=[x for x in report['media'] if not x['decoded']] | |
| print(ident,'SHA',report['recorded_sha256_comparisons'],'audio',report['audio_files'],'images',report['image_files'],'decode errors',len(report['media_decode_failures']),'missing declared',len(report['missing_declared_paths']),'missing tilesets',sum(not x['stored'] for x in map_references),flush=True) | |
| results.append(report);(R/'next-batch-payload-validation.json').write_text(json.dumps(results,indent=2)) | |
| finally: | |
| if server:server.shutdown();server.server_close() | |
| shutil.rmtree(v.STAGE,ignore_errors=True) | |
| v.api.batch_bucket_files(v.BUCKET,add=[(R/'next-batch-payload-validation.json','_verification/next-batch-payload-validation.json')]) | |
Xet Storage Details
- Size:
- 5.26 kB
- Xet hash:
- b1b031eb1aebc4b20368a53ccb852a015d33215c84a56a9d1c42588fecb85929
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.