40 lines
2.6 KiB
Python
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)
|