Files

38 lines
2.4 KiB
Python

"""Restore byte-verified multi-archive objects to a fresh destination only."""
import argparse,contextlib,gzip,hashlib,json,os,tarfile
from pathlib import Path
def sha(path):
with Path(path).open('rb')as f:return hashlib.file_digest(f,'sha256').hexdigest()
p=argparse.ArgumentParser();p.add_argument('manifest');p.add_argument('destination');p.add_argument('archives',nargs='+');a=p.parse_args();mp=Path(a.manifest);raw=gzip.decompress(mp.read_bytes())if mp.suffix=='.gz'else mp.read_bytes();m=json.loads(raw);dest=Path(a.destination).resolve();rows=m['members'];expected={v['sha256']:k for k,v in m['archives'].items()};provided={}
for archive in a.archives:
h=sha(archive);assert h in expected and expected[h] not in provided;provided[expected[h]]=Path(archive)
needed={x['archive']for x in rows if x['type']=='file'};assert needed<=provided.keys();assert len({x['path']for x in rows})==len(rows);by_hash={}
for x in rows:
path=Path(x['path']);assert not path.is_absolute()and '..'not in path.parts and '.'not in path.parts;q=dest/path;assert q.resolve().is_relative_to(dest)and not q.exists()and not q.is_symlink()
if x['type']=='file':by_hash.setdefault(x['sha256'],[]).append(x)
else:assert x['type']=='directory'
for x in rows:
if x['type']=='directory':(dest/x['path']).mkdir(parents=True,exist_ok=True)
seen=set();restored=0
for label,path in provided.items():
with tarfile.open(path,'r|gz')as t:
for entry in t:
assert entry.isfile()
if not entry.name.startswith('objects/'):continue
key=entry.name[8:];group=[x for x in by_hash.get(key,[])if x['archive']==label]
if not group:continue
assert key not in seen;seen.add(key);assert all(x['bytes']==entry.size for x in group);digest=hashlib.sha256()
with contextlib.ExitStack()as stack:
handles=[stack.enter_context((dest/x['path']).open('xb'))for x in group];source=stack.enter_context(t.extractfile(entry))
for chunk in iter(lambda:source.read(1024**2),b''):
digest.update(chunk)
for f in handles:f.write(chunk)
assert digest.hexdigest()==key
for x in group:
q=dest/x['path'];assert q.stat().st_size==x['bytes'];os.chmod(q,x['mode']);os.utime(q,ns=(x['mtime_ns'],x['mtime_ns']));restored+=1
assert seen==set(by_hash)and restored==m['files']
for x in reversed(rows):
if x['type']=='directory':q=dest/x['path'];os.chmod(q,x['mode']);os.utime(q,ns=(x['mtime_ns'],x['mtime_ns']))
print('Restored and verified',restored,'files')