"""Enrich blank PP display metadata from existing read-only snapshots. No network/source writes, uploads, visibility changes, or source versions. Default runs the scoped update in a transaction and rolls it back. """ import argparse import hashlib import json from pathlib import Path import subprocess ROOT = Path('/home/zheromiku/dobot-portal-sync/data') REPO = Path(__file__).resolve().parents[1] def read_snapshot(relative): path = ROOT / relative for part in [ROOT, *path.parents, path]: if part.is_symlink(): raise ValueError('snapshot symlinks are forbidden') if not path.is_file() or not path.resolve().is_relative_to(ROOT.resolve()): raise ValueError('invalid snapshot file') return json.loads(path.read_text()) def label(value): if not isinstance(value, str): return '' value = value.strip() if len(value) > 512 or any(ord(c) < 32 for c in value) or '://' in value: return '' return value def build_rows(manifest, inventory): by_id = {str(item['id']): item for item in inventory['items']} result = {} for item in manifest['results']: members = [by_id[str(i)] for i in item.get('source_ids', []) if str(i) in by_id] titles = sorted({label(m.get('fileName')) for m in members} - {''}) if not titles: filename = label(Path(item.get('local_path', '')).name) if filename: titles = [filename] if not titles: continue categories = {label((m.get('potalCategory') or {}).get('category')) for m in members} directory = next(iter(categories)) if len(categories) == 1 else '' # This is a PP category label, NOT a reconstructed ancestor hierarchy. if '/' in directory or '\\' in directory or directory in ('.', '..'): directory = '' source_hash = hashlib.sha256(item['url'].encode()).hexdigest() row = {'hash': source_hash, 'title': titles[0], 'directory': directory} if source_hash in result and result[source_hash] != row: raise ValueError('conflicting display metadata') result[source_hash] = row return sorted(result.values(), key=lambda x: x['hash']) def update_sql(rows, apply=False): payload = json.dumps(rows, ensure_ascii=False).replace("'", "''") return """BEGIN; SET LOCAL standard_conforming_strings = on; WITH metadata AS ( SELECT * FROM jsonb_to_recordset('%s'::jsonb) AS m(hash text, title text, directory text) ), changed AS ( UPDATE source_items AS i SET title = m.title, normalized_path = m.directory FROM metadata m, source_connections c WHERE c.id = i.source_connection_id AND c.source_type = 'pp' AND i.canonical_uri_hash = m.hash AND i.title = '' RETURNING i.id ) SELECT count(*) AS enriched FROM changed; %s; """ % (payload, 'COMMIT' if apply else 'ROLLBACK') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--apply', action='store_true') args = parser.parse_args() rows = build_rows(read_snapshot('public-documents/manifest.json'), read_snapshot('pp-full/inventory-snapshot.json')) print(f'matched={len(rows)} with_category={sum(bool(r["directory"]) for r in rows)} mode={"apply" if args.apply else "dry-run"}', flush=True) result = subprocess.run(['docker', 'compose', 'exec', '-T', 'postgres', 'psql', '-X', '-v', 'ON_ERROR_STOP=1', '-U', 'governance', '-d', 'governance', '-At'], input=update_sql(rows, args.apply), text=True, cwd=REPO, capture_output=True) if result.returncode: raise SystemExit('metadata update failed; transaction rolled back (details withheld)') print(result.stdout) if __name__ == '__main__': main()