38 lines
2.4 KiB
Python
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')
|