Files
verstack/scripts/inventory_corpus.py

40 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Restartable read-only SPIKE content inventory. One hash operation at a time."""
import argparse, hashlib, json, os, pathlib, tempfile, zipfile
def atomic(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(mode='w', dir=path.parent, delete=False) as f:
json.dump(value, f, indent=2); f.flush(); os.fsync(f.fileno())
os.replace(f.name, path)
fd=os.open(path.parent, os.O_DIRECTORY); os.fsync(fd); os.close(fd)
def inventory(root, output):
prior=json.loads(output.read_text()) if output.exists() else {'schema':1,'packages':[]}
known={p['name']:p for p in prior['packages']}
paths=sorted(p for p in root.iterdir() if p.is_file() and (p.name.lower().endswith('.spk') or p.name.lower().endswith('.spk.zip')))
rows=[]
for path in paths:
stat=path.stat(); old=known.get(path.name)
if old and old.get('bytes')==stat.st_size and old.get('mtime_ns')==stat.st_mtime_ns:
rows.append(old); continue
with path.open('rb') as f: digest=hashlib.file_digest(f,'sha256').hexdigest()
with path.open('rb') as f: magic=f.read(8)
row={'name':path.name,'bytes':stat.st_size,'mtime_ns':stat.st_mtime_ns,'sha256':digest,'magic':magic.hex(),
'wrapper': 'SPKS' if magic[:4]==b'SPKS' else 'gzip' if magic[:2]==b'\x1f\x8b' else 'unknown',
'generation':'unconfirmed','media_coverage':'not_processed'}
if zipfile.is_zipfile(path):
with zipfile.ZipFile(path) as z:
row['wrapper']='ZIP'; row['entries']=[{'path':e.filename,'bytes':e.file_size,'crc32':f'{e.CRC:08x}'} for e in z.infolist()]
first=next((e for e in z.infolist() if e.filename.lower().endswith('.000')),None)
if first:
with z.open(first) as f: inner=f.read(8)
row['payload_wrapper']='LUKS2' if inner[:6]==b'LUKS\xba\xbe' else 'SquashFS' if inner[:4]==b'hsqs' else 'unknown'
if path.stat().st_size!=stat.st_size or path.stat().st_mtime_ns!=stat.st_mtime_ns: raise ValueError('source changed while hashing')
rows.append(row); known[row['name']]=row
atomic(output,{'schema':1,'root':str(root),'expected':len(paths),'packages':[known[p.name] for p in paths if p.name in known]})
print(f'{len(rows)}/{len(paths)} {path.name} {digest}',flush=True)
atomic(output,{'schema':1,'root':str(root),'expected':len(paths),'packages':rows})
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('root',type=pathlib.Path);p.add_argument('output',type=pathlib.Path);a=p.parse_args();inventory(a.root,a.output)