Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
40 lines
2 KiB
Python
40 lines
2 KiB
Python
#!/usr/bin/env python3
|
|
"""Seal a measured essentials archive with an explicit recovery contract."""
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import os
|
|
from pathlib import PurePosixPath
|
|
import shutil
|
|
import stat
|
|
import zipfile
|
|
|
|
MANIFEST='backup-manifest.json'
|
|
OMITTED=('data/packages/','data/repo-archive/','data/indexers/','data/actions_log/','log/')
|
|
BUDGET=600*1024**2
|
|
|
|
|
|
def seal(path):
|
|
temporary=path.with_name(path.name+'.sealing')
|
|
fd=os.open(temporary,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600);os.close(fd)
|
|
try:
|
|
with zipfile.ZipFile(path) as source, zipfile.ZipFile(temporary,'w') as target:
|
|
kept=0
|
|
for member in source.infolist():
|
|
name=PurePosixPath(member.filename)
|
|
if name.is_absolute() or '..' in name.parts or stat.S_ISLNK(member.external_attr>>16):
|
|
raise ValueError('unsafe_archive_member')
|
|
if member.filename==MANIFEST: raise ValueError('archive_already_sealed')
|
|
if member.filename.startswith(OMITTED): continue
|
|
with source.open(member) as incoming, target.open(member,'w') as outgoing:
|
|
shutil.copyfileobj(incoming,outgoing,1024*1024)
|
|
kept+=1
|
|
target.writestr(MANIFEST,json.dumps({'schema':'platform.forgejo-backup-profile.v1',
|
|
'profile':'essentials','created_at':datetime.now(timezone.utc).isoformat(),
|
|
'complete_application_backup':False,'omitted_prefixes':list(OMITTED),
|
|
'kept_members':kept,'package_registry_available':False,
|
|
'recovery_contract':'Git and collaboration recovery; packages require full primary or an independent artifact source.',
|
|
'unique_data_policy':'Keep repositories, database, configuration, attachments, LFS and Actions artifacts when present.'},sort_keys=True),compress_type=zipfile.ZIP_DEFLATED)
|
|
if temporary.stat().st_size>BUDGET: raise ValueError('essentials_budget_exceeded')
|
|
os.replace(temporary,path)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|