Files

45 lines
4.9 KiB
Python

import pathlib,subprocess,json,hashlib,tarfile,gzip,stat,shutil,os,time,io
R=pathlib.Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部');BASE=pathlib.Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p04-01');E=R/'docs/plans/original-completion-2026-10-02/execution/drive-offload-completed-checkouts-r14-20261016';TEMP=BASE/'.runtime/offload-validation-r14-20261016';CANDIDATES=json.loads(pathlib.Path('/private/tmp/p04-completed-checkouts-r14-candidates.json').read_text())
def git(root,*args):return subprocess.check_output(['git','-C',str(root),*args],text=True).strip()
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
assert not E.exists() and not TEMP.exists()
initial=shutil.disk_usage(R).free;started=time.time();roots=[];unique={};total=0
for root_text in CANDIDATES['roots']:
root=pathlib.Path(root_text);name=root.name;assert root.is_dir() and not root.is_symlink();assert not git(root,'status','--porcelain','--untracked-files=all');assert not git(root,'clean','-ndx'),'ignored/untracked content must be classified first'
ps=subprocess.run(['/usr/sbin/lsof','-t','+D',str(root)],capture_output=True,text=True);assert ps.returncode==1 and not ps.stdout.strip(),('open files',name,ps.stdout)
files={};dirs={}
for p in sorted(root.rglob('*')):
rel=p.relative_to(root).as_posix();st=p.lstat();assert not stat.S_ISLNK(st.st_mode),str(p)
if rel=='.git':continue
if p.is_dir():dirs[rel]={'mode':stat.S_IMODE(st.st_mode),'mtime_ns':st.st_mtime_ns};continue
assert p.is_file(),str(p);v=p.read_bytes();h=hashlib.sha256(v).hexdigest();assert len(v)==st.st_size
files[rel]={'bytes':len(v),'sha256':h,'mode':stat.S_IMODE(st.st_mode),'mtime_ns':st.st_mtime_ns};total+=len(v)
unique.setdefault(h,{'source':str(p),'bytes':len(v)})
roots.append({'path':str(root),'name':name,'head':git(root,'rev-parse','HEAD'),'git_pointer':(root/'.git').read_text(),'files':files,'directories':dirs})
unique_bytes=sum(v['bytes'] for v in unique.values());assert shutil.disk_usage(R).free>=1024**3+unique_bytes+64*1024**2
E.mkdir(parents=True);TEMP.mkdir(parents=True)
(E/'CANDIDATES.json').write_text(json.dumps(CANDIDATES,ensure_ascii=False,indent=2)+'\n')
manifest={'protocol':'p04-ended-validation-cas-v1','production_repo':str(R),'scope':'Only 34 ended clean validation/review/integration/final checkouts from completed original units P08-02/P08-03/P24-03; current implementation/build/native raw/cache are excluded. All file bytes and metadata retained; .git administrative pointers are recorded for normal git worktree restoration.','roots':roots,'files':sum(len(v['files']) for v in roots),'logical_bytes':total,'unique_content_bytes':unique_bytes,'unique_blobs':len(unique)}
raw=json.dumps(manifest,ensure_ascii=False,sort_keys=True,separators=(',',':')).encode();mp=E/'MANIFEST.json.gz';mp.write_bytes(gzip.compress(raw,compresslevel=9,mtime=0))
archive=TEMP/'completed-checkouts-r14.cas.tar.gz'
with tarfile.open(archive,'w:gz',compresslevel=6) as tar:
ti=tarfile.TarInfo('manifest.json');ti.size=len(raw);ti.mtime=0;ti.mode=0o644;tar.addfile(ti,io.BytesIO(raw))
for h,v in sorted(unique.items()):
p=pathlib.Path(v['source']);assert sha(p)==h;ti=tarfile.TarInfo('blobs/'+h);ti.size=v['bytes'];ti.mtime=0;ti.mode=0o644
with p.open('rb') as f:tar.addfile(ti,f)
with tarfile.open(archive,'r:gz') as tar:
seen=set();m=tar.extractfile('manifest.json').read();assert m==raw
for member in tar:
if member.name=='manifest.json':continue
assert member.name.startswith('blobs/');h=member.name.split('/')[1];v=tar.extractfile(member).read();assert h in unique and len(v)==unique[h]['bytes'] and hashlib.sha256(v).hexdigest()==h;seen.add(h)
assert seen==set(unique)
parts=[];limit=16*1024**2
if archive.stat().st_size>limit:
with archive.open('rb') as f:
i=0
while block:=f.read(limit):
p=TEMP/(archive.name+'.part%03d'%i);p.write_bytes(block);parts.append({'path':str(p),'bytes':len(block),'sha256':hashlib.sha256(block).hexdigest()});i+=1
else:parts=[{'path':str(archive),'bytes':archive.stat().st_size,'sha256':sha(archive)}]
prep={'source_initial_free_bytes':initial,'source_logical_bytes':total,'unique_content_bytes':unique_bytes,'file_count':manifest['files'],'roots':[{k:v[k] for k in ['name','path','head']} for v in roots],'archive':{'path':str(archive),'bytes':archive.stat().st_size,'sha256':sha(archive)},'manifest':{'path':str(mp),'bytes':mp.stat().st_size,'sha256':sha(mp)},'upload_parts':parts,'local_tar_all_blobs_roundtrip':True,'prepared_seconds':time.time()-started,'free_after_preparation':shutil.disk_usage(R).free,'source_removed':False,'retirement_requires_cloud_full_readback_and_source_recheck':True}
(E/'PREPARATION.json').write_text(json.dumps(prep,ensure_ascii=False,indent=2)+'\n');(E/'prepare.py').write_bytes(pathlib.Path(__file__).read_bytes());print(json.dumps({k:prep[k] for k in ['source_logical_bytes','unique_content_bytes','file_count','archive','manifest','upload_parts','prepared_seconds','free_after_preparation']},ensure_ascii=False),flush=True)