#!/usr/bin/env python3
"""harbor-checksum: create & verify SHA-256 manifests for show files, footage and backups.
Usage:
  harbor-checksum.py make   <folder> [-o manifest.sha256]   # hash every file
  harbor-checksum.py verify <folder> [-m manifest.sha256]   # report OK / CHANGED / MISSING / NEW
  harbor-checksum.py compare <folderA> <folderB>            # verify a copy matches the original
Manifest format is compatible with `sha256sum -c` / `shasum -a 256 -c`. MIT License, Pixel Harbor."""
import hashlib, os, sys, argparse
SKIP={'.DS_Store','Thumbs.db','desktop.ini'}
def h(p):
    s=hashlib.sha256()
    with open(p,'rb') as f:
        for c in iter(lambda:f.read(1<<20),b''): s.update(c)
    return s.hexdigest()
def walk(root):
    out={}
    for r,ds,fs in os.walk(root):
        ds[:]=[d for d in ds if not d.startswith('.')]
        for f in fs:
            if f in SKIP or f.endswith('.sha256'): continue
            p=os.path.join(r,f); out[os.path.relpath(p,root).replace(os.sep,'/')]=h(p)
    return out
def load(m):
    d={}
    for line in open(m,encoding='utf-8'):
        if line.strip(): k,v=line.rstrip('\n').split('  ',1); d[v]=k
    return d
def diff(a,b):
    bad=0
    for k in sorted(set(a)|set(b)):
        if k not in b: print('MISSING ',k); bad+=1
        elif k not in a: print('NEW     ',k); bad+=1
        elif a[k]!=b[k]: print('CHANGED ',k); bad+=1
    print(f'{len(a)} expected, {bad} problem(s)'); return 1 if bad else 0
ap=argparse.ArgumentParser(description=__doc__,formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('cmd',choices=['make','verify','compare']); ap.add_argument('a'); ap.add_argument('b',nargs='?')
ap.add_argument('-o','--out'); ap.add_argument('-m','--manifest')
x=ap.parse_args()
if x.cmd=='make':
    m=x.out or os.path.join(x.a,'manifest.sha256'); d=walk(x.a)
    with open(m,'w',encoding='utf-8') as f:
        for k in sorted(d): f.write(f'{d[k]}  {k}\n')
    print(f'Wrote {len(d)} hashes to {m}')
elif x.cmd=='verify': sys.exit(diff(load(x.manifest or os.path.join(x.a,'manifest.sha256')),walk(x.a)))
else:
    if not x.b: ap.error('compare needs two folders')
    sys.exit(diff(walk(x.a),walk(x.b)))
