#!/usr/bin/env python3 """Import a template bundle (from export_template.py) into ANY FlatRender DB. Emits idempotent SQL to STDOUT — pipe it to the target's psql. It replaces any existing template with the same slug (one cascading delete), recreates the rows verbatim (original UUIDs, so FKs stay intact), and merges categories & tags BY SLUG (so they line up with whatever the target DB already calls them). Usage: python scripts/import_template.py | # e.g. local: python scripts/import_template.py dist/template-bundles/fr-instagram-promo \\ | docker exec -i fr2-postgres psql -U postgres -d flatrender # e.g. live: python scripts/import_template.py dist/template-bundles/fr-instagram-promo \\ | psql "postgresql://user:pass@live-host:5432/flatrender" Assets: copy the bundle's assets/ into the target's template-media (the SQL only moves DB rows). Pass --assets-to to copy them locally; for a remote box use `docker cp` / `mc cp` (printed below). """ import os, sys, json, shutil args = [a for a in sys.argv[1:] if not a.startswith("--")] if not args: print("usage: import_template.py [--assets-to ] | ", file=sys.stderr); sys.exit(1) BUNDLE = args[0] ASSETS_TO = None if "--assets-to" in sys.argv: ASSETS_TO = sys.argv[sys.argv.index("--assets-to") + 1] with open(os.path.join(BUNDLE, "template.json"), encoding="utf-8") as f: b = json.load(f) SLUG = b["slug"] def qv(v): if v is None: return "NULL" if isinstance(v, bool): return "TRUE" if v else "FALSE" if isinstance(v, (int, float)): return str(v) if isinstance(v, (dict, list)): return "'" + json.dumps(v, ensure_ascii=False).replace("'", "''") + "'" return "'" + str(v).replace("'", "''") + "'" def ins(table, row, conflict=None): cols = list(row.keys()) s = f"INSERT INTO content.{table} ({','.join(cols)}) VALUES ({','.join(qv(row[c]) for c in cols)})" if conflict: s += f" ON CONFLICT ({conflict}) DO NOTHING" return s + ";" out = ["BEGIN;"] # categories & tags first (merge by slug — never clobber the target's own) for cat in b.get("categories") or []: out.append(ins("categories", cat, "slug")) for tg in b.get("tags") or []: out.append(ins("tags", tg, "slug")) # replace any existing template with this slug (cascades to all children) out.append(f"DELETE FROM content.project_containers WHERE slug={qv(SLUG)};") # rows verbatim (original UUIDs keep the FK tree intact) out.append(ins("project_containers", b["container"])) for p in b.get("projects") or []: out.append(ins("projects", p)) for s in b.get("scenes") or []: out.append(ins("scenes", s)) for e in b.get("content_elements") or []: out.append(ins("scene_content_elements", e)) for sc in b.get("shared_colors") or []: out.append(ins("shared_colors", sc)) for sl in b.get("shared_layers") or []: out.append(ins("shared_layers", sl)) # links resolve the category/tag id BY SLUG in the target DB cid = b["container"]["id"] for link in b.get("category_links") or []: out.append(f"INSERT INTO content.container_categories (container_id,category_id,sort) " f"SELECT {qv(cid)}, id, {qv(link.get('sort', 0))} FROM content.categories WHERE slug={qv(link['category_slug'])} ON CONFLICT DO NOTHING;") for link in b.get("tag_links") or []: out.append(f"INSERT INTO content.container_tags (container_id,tag_id) " f"SELECT {qv(cid)}, id FROM content.tags WHERE slug={qv(link['tag_slug'])} ON CONFLICT DO NOTHING;") out.append("COMMIT;") out.append(f"SELECT slug, name, primary_mode FROM content.project_containers WHERE slug={qv(SLUG)};") print("\n".join(out)) # Assets: copy locally if asked; otherwise print placement guidance to stderr. assets = b.get("assets") or [] if ASSETS_TO and assets: os.makedirs(ASSETS_TO, exist_ok=True) for a in assets: src = os.path.join(BUNDLE, "assets", a) if os.path.isfile(src): shutil.copy2(src, os.path.join(ASSETS_TO, a)) sys.stderr.write(f"-- copied {len(assets)} asset(s) → {ASSETS_TO}\n") elif assets: sys.stderr.write(f"\n-- {len(assets)} asset(s) in {BUNDLE}/assets/ — place them in the target's template-media, e.g.:\n") sys.stderr.write(f"-- local docker: for f in {BUNDLE}/assets/*; do docker cp \"$f\" fr2-frontend:/app/public/template-media/; done\n") sys.stderr.write(f"-- live (MinIO): mc cp --recursive {BUNDLE}/assets/ myminio//template-media/\n") sys.stderr.write(f"-- or just copy {BUNDLE}/assets/* into the live app's public/template-media/\n")