#!/usr/bin/env python3
"""Restore a published delivery manifest using Python standard library only.
Usage: python3 restore_music_first_delivery.py MANIFEST_HTTPS_URL EMPTY_DIRECTORY
Downloads are read-only. No credentials, media purchase or project execution.
"""
import hashlib,json,pathlib,shutil,sys,tarfile,tempfile,urllib.request,urllib.parse

def sha(p):
 with open(p,'rb')as f:return hashlib.file_digest(f,'sha256').hexdigest()
def download(url,path):
 if urllib.parse.urlparse(url).scheme!='https':raise ValueError('HTTPS required')
 request=urllib.request.Request(url,headers={'User-Agent':'AdFactory-ArchiveRestore/1.0','Accept':'*/*'})
 with urllib.request.urlopen(request,timeout=120)as r,open(path,'wb')as f:shutil.copyfileobj(r,f)
def main(url,destination):
 root=pathlib.Path(destination).resolve()
 if root.exists()and any(root.iterdir()):raise ValueError('Destination must be empty')
 root.mkdir(parents=True,exist_ok=True)
 with tempfile.TemporaryDirectory(prefix='music-first-restore-')as tmp:
  tmp=pathlib.Path(tmp);manifest=tmp/'manifest.json';download(url,manifest);d=json.loads(manifest.read_text())
  assert d['schema']=='adfactory.music-first-delivery-archive/v1'
  inv=tmp/'inventory.json';download(urllib.parse.urljoin(url,pathlib.PurePosixPath(d['inventory']['path']).name),inv);assert sha(inv)==d['inventory']['sha256'];rows=json.loads(inv.read_text())['files'];expected={r['path']:r for r in rows};assert len(expected)==len(rows)==d['fileCount']
  bundle=tmp/'delivery.tar.gz'
  with bundle.open('wb')as output:
   for i,part in enumerate(d['archive']['parts']):
    p=tmp/'part.bin';download(part['url'],p);assert p.stat().st_size==part['bytes']and sha(p)==part['sha256']
    with p.open('rb')as source:shutil.copyfileobj(source,output)
    print('Verified remote part',i+1,'/',len(d['archive']['parts']),flush=True);p.unlink()
  assert bundle.stat().st_size==d['archive']['bytes']and sha(bundle)==d['archive']['sha256']
  with tarfile.open(bundle,'r:*')as archive:
   members=archive.getmembers();assert len(members)==len(expected)and{m.name for m in members}==set(expected)
   # Verify every path/type/content before creating any extracted file.
   for m in members:
    p=pathlib.PurePosixPath(m.name)
    if p.is_absolute()or'..'in p.parts or'\\'in m.name or not m.isfile():raise ValueError('Unsafe member')
    target=root/m.name
    if not target.resolve().is_relative_to(root):raise ValueError('Escaping member')
    assert m.size==expected[m.name]['bytes']
    with archive.extractfile(m)as f:assert hashlib.file_digest(f,'sha256').hexdigest()==expected[m.name]['sha256']
   for m in members:
    target=root/m.name;target.parent.mkdir(parents=True,exist_ok=True)
    with archive.extractfile(m)as source,target.open('xb')as output:shutil.copyfileobj(source,output)
  print(json.dumps({'status':'verified-and-restored','files':len(expected),'destination':str(root),'archiveSha256':d['archive']['sha256']}),flush=True)
if __name__=='__main__':
 if len(sys.argv)!=3:raise SystemExit(__doc__)
 main(sys.argv[1],sys.argv[2])
