#!/usr/bin/env python3 """ LocalSpace Deployer — Hugging Face Space (Vanilla) Drag a DMG or GGUF. It gets OPENED on the server: extracted, inspected, and its app bundle metadata or model metadata is displayed. The Space itself hosts the contents. Pure FastAPI + HTML/JS. No Gradio, no Streamlit, no API keys. Public by default. """ from __future__ import annotations import hashlib import json import os import plistlib import re import shutil import struct import subprocess import time import uuid import zipfile from pathlib import Path from typing import Any from fastapi import FastAPI, File, Request, UploadFile from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles # Try biplist for binary plists, fall back to plistlib try: import biplist HAS_BIPLIST = True except ImportError: HAS_BIPLIST = False # Try llama-cpp-python for GGUF inference (optional) try: from llama_cpp import Llama as _Llama HAS_LLAMA = True except ImportError: HAS_LLAMA = False # ─── Persistent storage detection ────────────────────────────────────────── # HF Spaces mounts persistent storage at /data when enabled. # Fall back to local ./data when running outside HF or without persistent storage. _PERSISTENT = Path("/data") if _PERSISTENT.exists() and _PERSISTENT.is_dir() and os.access(_PERSISTENT, os.W_OK): DATA_DIR = _PERSISTENT / "localspace" else: DATA_DIR = Path("data") DATA_DIR.mkdir(parents=True, exist_ok=True) UPLOAD_DIR = DATA_DIR / "uploads" UPLOAD_DIR.mkdir(exist_ok=True) EXTRACT_DIR = DATA_DIR / "extracted" EXTRACT_DIR.mkdir(exist_ok=True) SITES_DIR = DATA_DIR / "sites" SITES_DIR.mkdir(exist_ok=True) DAPPS_DIR = DATA_DIR / "dapps" DAPPS_DIR.mkdir(exist_ok=True) DB_PATH = DATA_DIR / "apps.json" # Inference model cache (in-memory) _llm_cache: dict[str, Any] = {} app = FastAPI(title="LocalSpace Deployer") # Ensure static dir exists STATIC_DIR = Path("static") STATIC_DIR.mkdir(exist_ok=True) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") _store: dict[str, dict[str, Any]] = {} def _load_db() -> None: global _store if DB_PATH.exists(): with open(DB_PATH) as f: _store = json.load(f) else: _store = {} def _save_db() -> None: with open(DB_PATH, "w") as f: json.dump(_store, f, indent=2) def _slugify(name: str) -> str: return re.sub(r"[^a-z0-9]+", "-", name.lower().replace(".dmg", "").replace(".gguf", "").replace(".zip", "").replace(".json", "")).strip("-") def _hash_file(path: Path) -> str: h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest() def _format_bytes(n: int) -> str: for unit in ["B", "KB", "MB", "GB", "TB"]: if n < 1024: return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} PB" def _read_plist(path: Path) -> dict[str, Any]: """Read a plist file (XML or binary).""" try: with open(path, "rb") as f: data = f.read() try: return plistlib.loads(data) except Exception: pass if HAS_BIPLIST: try: return biplist.readPlistFromString(data) except Exception: pass except Exception: pass return {} def _find_app_bundles(root: Path) -> list[Path]: """Find all .app directories under root.""" apps = [] for p in root.rglob("*.app"): if p.is_dir(): apps.append(p) return apps def _extract_dmg(dmg_path: Path, dest: Path) -> bool: """Extract DMG using 7z. Returns True on success.""" try: dest.mkdir(parents=True, exist_ok=True) result = subprocess.run( ["7z", "x", "-y", "-o" + str(dest), str(dmg_path)], capture_output=True, text=True, timeout=120, ) return result.returncode == 0 except Exception: return False def _inspect_app_bundle(app_path: Path) -> dict[str, Any]: """Read Info.plist and extract metadata from an .app bundle.""" info_plist = app_path / "Contents" / "Info.plist" if not info_plist.exists(): info_plist = app_path / "Info.plist" info = _read_plist(info_plist) if info_plist.exists() else {} icon_name = info.get("CFBundleIconFile", "") icon_path = None if icon_name: icons_dir = app_path / "Contents" / "Resources" if icons_dir.exists(): icns = icons_dir / (icon_name if icon_name.endswith(".icns") else icon_name + ".icns") if icns.exists(): icon_path = str(icns) files = [] if app_path.exists(): for f in sorted(app_path.rglob("*")): if f.is_file(): try: rel = str(f.relative_to(app_path)) files.append(rel) except ValueError: pass return { "bundle_name": app_path.name, "bundle_id": info.get("CFBundleIdentifier", ""), "display_name": info.get("CFBundleDisplayName", "") or info.get("CFBundleName", ""), "version": info.get("CFBundleShortVersionString", "") or info.get("CFBundleVersion", ""), "min_os_version": info.get("LSMinimumSystemVersion", ""), "executable": info.get("CFBundleExecutable", ""), "icon_path": icon_path, "info_plist": info, "file_count": len(files), "files": files[:200], } # ─── GGUF parsing ─────────────────────────────────────────────────────────── GGUF_MAGIC = 0x46554747 # "GGUF" in little-endian GGUF_TYPE_MAP = { 0: "UINT8", 1: "INT8", 2: "UINT16", 3: "INT16", 4: "UINT32", 5: "INT32", 6: "FLOAT32", 7: "BOOL", 8: "STRING", 9: "ARRAY", 10: "UINT64", 11: "INT64", 12: "FLOAT64", } def _read_gguf_string(f) -> str: n = struct.unpack(" Any: if vtype == 0: return struct.unpack(" dict[str, Any]: """Parse GGUF header: version, tensor count, metadata KV pairs.""" try: with open(gguf_path, "rb") as f: magic = struct.unpack(" dict[str, Any]: """Open a DMG: extract it, find apps, inspect them.""" extract_to = EXTRACT_DIR / app_id if extract_to.exists(): shutil.rmtree(extract_to) success = _extract_dmg(dmg_path, extract_to) if not success: return {"opened": False, "error": "Extraction failed. DMG may be encrypted or use an unsupported format."} apps = _find_app_bundles(extract_to) inspected = [_inspect_app_bundle(a) for a in apps] # Build top-level tree tree = [] for item in sorted(extract_to.iterdir()): tree.append({ "name": item.name, "type": "directory" if item.is_dir() else "file", "size": item.stat().st_size if item.is_file() else 0, }) # Discover all HTML files recursively for iframe preview html_files: list[dict[str, Any]] = [] for f in extract_to.rglob("*.html"): rel = f.relative_to(extract_to) depth = len(rel.parts) html_files.append({"path": str(rel), "depth": depth, "size": f.stat().st_size}) # Prefer shallowest HTML, then shortest path, then largest html_files.sort(key=lambda x: (x["depth"], len(x["path"]), -x["size"])) return { "opened": True, "extracted_path": str(extract_to), "apps_found": len(inspected), "apps": inspected, "tree": tree, "html_files": html_files[:50], "has_preview": bool(html_files), "preview_entry": html_files[0]["path"] if html_files else None, } _load_db() @app.get("/", response_class=HTMLResponse) def index(request: Request) -> HTMLResponse: with open("static/index.html") as f: return HTMLResponse(content=f.read()) @app.post("/api/upload") async def upload_file(file: UploadFile = File(...)) -> JSONResponse: if not file.filename: return JSONResponse({"error": "No filename provided."}, status_code=400) fname = file.filename.lower() is_dmg = fname.endswith(".dmg") is_gguf = fname.endswith(".gguf") if not is_dmg and not is_gguf: return JSONResponse({"error": "Only .dmg and .gguf files are accepted."}, status_code=400) app_id = str(uuid.uuid4()) slug = _slugify(file.filename) base_slug = slug counter = 1 while any(a.get("slug") == slug for a in _store.values()): slug = f"{base_slug}-{counter}" counter += 1 ext = ".dmg" if is_dmg else ".gguf" dest = UPLOAD_DIR / f"{app_id}{ext}" with open(dest, "wb") as f: while True: chunk = await file.read(65536) if not chunk: break f.write(chunk) sha256 = _hash_file(dest) size = dest.stat().st_size if is_dmg: opened = _open_dmg(app_id, dest) entry = { "app_id": app_id, "slug": slug, "filename": file.filename, "file_type": "dmg", "size": size, "size_human": _format_bytes(size), "sha256": sha256, "download_url": f"/api/download/{app_id}", "opened": opened.get("opened", False), "apps_found": opened.get("apps_found", 0), "apps": opened.get("apps", []), "tree": opened.get("tree", []), "html_files": opened.get("html_files", []), "has_preview": opened.get("has_preview", False), "preview_entry": opened.get("preview_entry", None), "error": opened.get("error"), "created_at": time.time(), } msg = "DMG uploaded and opened." if entry["opened"] else f"DMG uploaded but could not be opened: {entry.get('error', '')}" else: gguf_info = _inspect_gguf(dest) entry = { "app_id": app_id, "slug": slug, "filename": file.filename, "file_type": "gguf", "size": size, "size_human": _format_bytes(size), "sha256": sha256, "download_url": f"/api/download/{app_id}", "gguf": gguf_info, "opened": gguf_info.get("valid", False), "has_preview": False, "preview_entry": None, "created_at": time.time(), } msg = "GGUF uploaded and inspected." if gguf_info.get("valid") else f"GGUF uploaded but inspection failed: {gguf_info.get('error', '')}" _store[app_id] = entry _save_db() return JSONResponse({"app": entry, "message": msg}, status_code=201) @app.get("/api/apps") def list_apps() -> JSONResponse: apps = sorted(_store.values(), key=lambda a: a["created_at"], reverse=True) return JSONResponse({"apps": apps}) @app.get("/app/{app_id}", response_class=HTMLResponse) def serve_app(app_id: str) -> HTMLResponse: """Serve a published app as a full-page iframe viewer.""" entry = _store.get(app_id) if not entry: return HTMLResponse("

Not found

", status_code=404) if not entry.get("has_preview"): return HTMLResponse("

This app has no web preview

", status_code=404) entry_path = entry.get("preview_entry", "") iframe_src = f"/api/preview/{app_id}/{entry_path}" if entry_path else f"/api/preview/{app_id}" app_name = entry.get("filename", "App") display_name = "" if entry.get("apps") and entry["apps"]: display_name = entry["apps"][0].get("display_name", "") or entry["apps"][0].get("bundle_name", "") if not display_name: display_name = app_name.replace(".dmg", "").replace(".zip", "") html = f''' {display_name} — LocalSpace
⬡ {display_name} Live
← Back ↗ Open
Loading app...
''' return HTMLResponse(content=html) @app.get("/api/apps/{app_id}") def get_app(app_id: str) -> JSONResponse: entry = _store.get(app_id) if not entry: return JSONResponse({"error": "Not found"}, status_code=404) return JSONResponse({"app": entry}) @app.get("/api/download/{app_id}") def download_app(app_id: str): entry = _store.get(app_id) if not entry: return JSONResponse({"error": "Not found"}, status_code=404) ext = ".dmg" if entry.get("file_type", "dmg") == "dmg" else ".gguf" path = UPLOAD_DIR / f"{app_id}{ext}" if not path.exists(): return JSONResponse({ "error": "File not found on disk", "detail": "This upload was stored in temporary storage that was cleared during a Space restart. Please re-upload." }, status_code=404) media = "application/x-apple-diskimage" if ext == ".dmg" else "application/octet-stream" return FileResponse(path=path, filename=entry["filename"], media_type=media) @app.get("/api/browse/{app_id}/{path:path}") def browse_extracted(app_id: str, path: str): entry = _store.get(app_id) if not entry: return JSONResponse({"error": "Not found"}, status_code=404) safe_path = Path(path).name if not path else path base = EXTRACT_DIR / app_id target = base / safe_path if not base.exists(): return JSONResponse({ "error": "Extracted files not found", "detail": "This upload was stored in temporary storage that was cleared during a Space restart. Please re-upload." }, status_code=404) try: target.resolve().relative_to(base.resolve()) except ValueError: return JSONResponse({"error": "Access denied"}, status_code=403) if not target.exists(): return JSONResponse({"error": "Not found"}, status_code=404) if target.is_dir(): items = [] for item in sorted(target.iterdir()): items.append({ "name": item.name, "type": "directory" if item.is_dir() else "file", "size": item.stat().st_size if item.is_file() else 0, }) return JSONResponse({"items": items}) return FileResponse(path=target) # ─── Preview extracted HTML content in iframe ────────────────────────────── @app.get("/api/preview/{app_id}/{path:path}") def preview_content(app_id: str, path: str): """Serve extracted DMG content for iframe preview.""" entry = _store.get(app_id) if not entry: return JSONResponse({"error": "Not found"}, status_code=404) base = EXTRACT_DIR / app_id if not base.exists(): return JSONResponse({ "error": "Extracted files not found", "detail": "This upload was stored in temporary storage that was cleared during a Space restart. Please re-upload." }, status_code=404) if path: target = base / path else: target = base / "index.html" if not target.exists(): for f in base.rglob("*.html"): target = f break if not target.exists(): return JSONResponse({"error": "Not found"}, status_code=404) try: target.resolve().relative_to(base.resolve()) except ValueError: return JSONResponse({"error": "Access denied"}, status_code=403) if target.is_dir(): idx = target / "index.html" if idx.exists(): target = idx else: return JSONResponse({"error": "No index.html"}, status_code=404) mime_types = { ".html": "text/html", ".htm": "text/html", ".js": "application/javascript", ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".json": "application/json", ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf", } suffix = target.suffix.lower() media_type = mime_types.get(suffix, "application/octet-stream") if media_type == "text/html": content = target.read_text(errors="replace") if "' if rel_dir else f'' content = content.replace("", f"{base_tag}", 1) content = content.replace("", f"{base_tag}", 1) return HTMLResponse(content=content, media_type=media_type) return FileResponse(path=target, media_type=media_type) # ─── Create DMG from iframe URL (Web App → macOS App) ───────────────────── WEBAPP_DIR = DATA_DIR / "webapps" WEBAPP_DIR.mkdir(exist_ok=True) def _create_app_bundle(url: str, app_name: str, bundle_id: str, version: str) -> Path: """Create a minimal macOS .app bundle that opens a URL.""" bundle_root = WEBAPP_DIR / f"{app_name}.app" if bundle_root.exists(): shutil.rmtree(bundle_root) contents = bundle_root / "Contents" macos = contents / "MacOS" resources = contents / "Resources" macos.mkdir(parents=True) resources.mkdir(parents=True) plist = { "CFBundleDevelopmentRegion": "en", "CFBundleExecutable": app_name.replace(" ", ""), "CFBundleIdentifier": bundle_id, "CFBundleInfoDictionaryVersion": "6.0", "CFBundleName": app_name, "CFBundlePackageType": "APPL", "CFBundleShortVersionString": version, "CFBundleVersion": version, "LSMinimumSystemVersion": "10.15", "LSUIElement": False, } with open(contents / "Info.plist", "wb") as f: plistlib.dump(plist, f) script_path = macos / app_name.replace(" ", "") script_content = f'''#!/bin/bash # Auto-generated web app wrapper URL="{url}" if command -v open >/dev/null 2>&1; then open "$URL" else xdg-open "$URL" 2>/dev/null || python3 -m webbrowser "$URL" fi ''' script_path.write_text(script_content) script_path.chmod(0o755) html_path = resources / "index.html" html_path.write_text(f''' {app_name}

{app_name}

Loading {url}...

''') return bundle_root def _create_dmg_from_app(app_bundle: Path, output_name: str) -> Path | None: """Best-effort DMG creation. Returns path to DMG or None.""" dmg_path = WEBAPP_DIR / f"{output_name}.dmg" try: iso_path = WEBAPP_DIR / f"{output_name}.iso" result = subprocess.run( ["genisoimage", "-D", "-V", output_name, "-no-pad", "-r", "-apple", "-o", str(iso_path), str(app_bundle)], capture_output=True, text=True, timeout=30, ) if result.returncode == 0: dmg_result = subprocess.run( ["dmg", "iso", str(iso_path), str(dmg_path)], capture_output=True, text=True, timeout=30, ) if dmg_result.returncode == 0 and dmg_path.exists(): iso_path.unlink(missing_ok=True) return dmg_path except FileNotFoundError: pass except Exception: pass zip_path = WEBAPP_DIR / f"{output_name}.zip" shutil.make_archive( base_name=str(WEBAPP_DIR / output_name), format="zip", root_dir=str(app_bundle.parent), base_dir=app_bundle.name, ) if zip_path.exists(): return zip_path return None @app.post("/api/create-webapp") async def create_webapp(request: Request) -> JSONResponse: """Create a macOS app bundle + DMG from a URL.""" try: data = await request.json() except Exception: return JSONResponse({"error": "Invalid JSON"}, status_code=400) url = data.get("url", "").strip() app_name = data.get("app_name", "WebApp").strip() bundle_id = data.get("bundle_id", "app.localspace.webapp").strip() version = data.get("version", "1.0.0").strip() if not url: return JSONResponse({"error": "URL is required"}, status_code=400) if not app_name: return JSONResponse({"error": "App name is required"}, status_code=400) app_name_safe = re.sub(r'[^a-zA-Z0-9 ]+', '', app_name).strip() if not app_name_safe: app_name_safe = "WebApp" try: bundle = _create_app_bundle(url, app_name_safe, bundle_id, version) pkg = _create_dmg_from_app(bundle, app_name_safe.replace(" ", "-")) if pkg is None: return JSONResponse({"error": "Failed to create package"}, status_code=500) pkg_id = str(uuid.uuid4()) dest = UPLOAD_DIR / f"{pkg_id}{pkg.suffix}" shutil.copy2(pkg, dest) entry = { "app_id": pkg_id, "slug": _slugify(app_name_safe), "filename": dest.name, "file_type": "webapp", "size": dest.stat().st_size, "size_human": _format_bytes(dest.stat().st_size), "sha256": _hash_file(dest), "download_url": f"/api/download/{pkg_id}", "source_url": url, "app_name": app_name_safe, "bundle_id": bundle_id, "version": version, "is_webapp": True, "created_at": time.time(), } _store[pkg_id] = entry _save_db() return JSONResponse({ "app": entry, "message": f"'{app_name_safe}' packaged." if pkg.suffix == ".zip" else f"'{app_name_safe}' DMG created.", }, status_code=201) except Exception as exc: return JSONResponse({"error": str(exc)}, status_code=500) # ─── Tiny Netlify: deploy static sites from ZIP ───────────────────────────── def _find_index_html(root: Path) -> str | None: """Find the entry HTML file in a deployed site.""" candidates = ["index.html", "index.htm"] for c in candidates: if (root / c).exists(): return c # Search one level deep for item in sorted(root.iterdir()): if item.is_dir(): for c in candidates: if (item / c).exists(): return f"{item.name}/{c}" # Fallback: any HTML file for f in root.rglob("*.html"): return str(f.relative_to(root)) return None def _build_site_tree(root: Path, max_depth: int = 3) -> list[dict[str, Any]]: """Build a file tree for a deployed site.""" tree = [] try: for item in sorted(root.iterdir()): entry: dict[str, Any] = { "name": item.name, "type": "directory" if item.is_dir() else "file", "size": item.stat().st_size if item.is_file() else 0, } if item.is_dir() and max_depth > 0: entry["children"] = _build_site_tree(item, max_depth - 1) tree.append(entry) except Exception: pass return tree @app.post("/api/deploy-site") async def deploy_site(file: UploadFile = File(...)) -> JSONResponse: """Deploy a static site from a ZIP file. Returns a hosted URL.""" if not file.filename or not file.filename.lower().endswith(".zip"): return JSONResponse({"error": "Only .zip files are accepted for site deployment."}, status_code=400) site_id = str(uuid.uuid4()) slug = _slugify(file.filename) or f"site-{site_id[:8]}" base_slug = slug counter = 1 while any(a.get("slug") == slug and a.get("file_type") == "site" for a in _store.values()): slug = f"{base_slug}-{counter}" counter += 1 # Save ZIP zip_path = UPLOAD_DIR / f"{site_id}.zip" with open(zip_path, "wb") as f: while True: chunk = await file.read(65536) if not chunk: break f.write(chunk) # Extract site_root = SITES_DIR / site_id site_root.mkdir(parents=True, exist_ok=True) try: with zipfile.ZipFile(zip_path, "r") as zf: zf.extractall(site_root) except zipfile.BadZipFile: shutil.rmtree(site_root) zip_path.unlink(missing_ok=True) return JSONResponse({"error": "Invalid ZIP file."}, status_code=400) # Handle nested root: if ZIP contains a single top-level dir, use that as root top_items = list(site_root.iterdir()) if len(top_items) == 1 and top_items[0].is_dir(): real_root = top_items[0] else: real_root = site_root index_file = _find_index_html(real_root) if not index_file: return JSONResponse({"error": "No HTML file found in ZIP."}, status_code=400) sha256 = _hash_file(zip_path) file_count = sum(1 for _ in real_root.rglob("*") if _.is_file()) total_size = sum(f.stat().st_size for f in real_root.rglob("*") if f.is_file()) tree = _build_site_tree(real_root) entry = { "app_id": site_id, "slug": slug, "filename": file.filename, "file_type": "site", "size": zip_path.stat().st_size, "size_human": _format_bytes(zip_path.stat().st_size), "sha256": sha256, "site_url": f"/site/{site_id}/", "site_preview": f"/site/{site_id}/{index_file}", "index_file": index_file, "file_count": file_count, "total_size": total_size, "total_size_human": _format_bytes(total_size), "tree": tree, "has_preview": True, "preview_entry": index_file, "opened": True, "created_at": time.time(), } _store[site_id] = entry _save_db() return JSONResponse({ "app": entry, "message": f"Site deployed. {file_count} files, {entry['size_human']}.", "site_url": entry["site_url"], }, status_code=201) @app.get("/site/{site_id}/", response_class=HTMLResponse) @app.get("/site/{site_id}", response_class=HTMLResponse) def serve_site_root(site_id: str) -> HTMLResponse: """Serve a deployed static site's index page.""" entry = _store.get(site_id) if not entry or entry.get("file_type") != "site": return HTMLResponse("

Site not found

", status_code=404) site_root = SITES_DIR / site_id if not site_root.exists(): return HTMLResponse("

Site files not found. Storage may have been cleared.

", status_code=404) index_file = entry.get("index_file", "index.html") target = site_root / index_file if not target.exists(): for f in site_root.rglob("*.html"): target = f break if not target.exists(): return HTMLResponse("

No HTML found

", status_code=404) content = target.read_text(errors="replace") rel_dir = str(target.parent.relative_to(site_root)) if "' content = content.replace("", f"{base_tag}", 1) content = content.replace("", f"{base_tag}", 1) return HTMLResponse(content=content) @app.get("/site/{site_id}/{path:path}") def serve_site_file(site_id: str, path: str): """Serve any file from a deployed static site.""" entry = _store.get(site_id) if not entry or entry.get("file_type") != "site": return JSONResponse({"error": "Site not found"}, status_code=404) site_root = SITES_DIR / site_id if not site_root.exists(): return JSONResponse({"error": "Site storage cleared"}, status_code=404) target = site_root / path try: target.resolve().relative_to(site_root.resolve()) except ValueError: return JSONResponse({"error": "Access denied"}, status_code=403) if not target.exists(): return JSONResponse({"error": "File not found"}, status_code=404) if target.is_dir(): idx = target / "index.html" if idx.exists(): content = idx.read_text(errors="replace") rel_dir = str(idx.parent.relative_to(site_root)) if "", f'', 1) return HTMLResponse(content=content) return JSONResponse({"error": "No index.html in directory"}, status_code=404) mime_types = { ".html": "text/html", ".htm": "text/html", ".js": "application/javascript", ".mjs": "application/javascript", ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json", ".xml": "application/xml", ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf", ".otf": "font/otf", ".txt": "text/plain", ".md": "text/markdown", ".wasm": "application/wasm", ".map": "application/json", } media_type = mime_types.get(target.suffix.lower(), "application/octet-stream") return FileResponse(path=target, media_type=media_type) # ─── GGUF Inference ───────────────────────────────────────────────────────── def _get_llm(app_id: str) -> Any | None: """Load a GGUF model into memory, with caching.""" if not HAS_LLAMA: return None if app_id in _llm_cache: return _llm_cache[app_id] entry = _store.get(app_id) if not entry or entry.get("file_type") != "gguf": return None gguf_path = UPLOAD_DIR / f"{app_id}.gguf" if not gguf_path.exists(): return None try: ctx = entry.get("gguf", {}).get("context_length", 2048) n_ctx = min(int(ctx) if ctx else 2048, 4096) llm = _Llama(model_path=str(gguf_path), n_ctx=n_ctx, verbose=False) _llm_cache[app_id] = llm return llm except Exception: return None @app.get("/api/inference/status") def inference_status() -> JSONResponse: """Check if GGUF inference is available.""" loaded = list(_llm_cache.keys()) gguf_apps = [a for a in _store.values() if a.get("file_type") == "gguf"] return JSONResponse({ "available": HAS_LLAMA, "loaded_models": len(loaded), "loaded_ids": loaded, "gguf_apps": [{"app_id": a["app_id"], "filename": a["filename"]} for a in gguf_apps], }) @app.post("/api/inference/load/{app_id}") def load_model(app_id: str) -> JSONResponse: """Load a GGUF model into memory for inference.""" if not HAS_LLAMA: return JSONResponse({ "error": "llama-cpp-python not installed. Inference unavailable.", "hint": "Add llama-cpp-python to requirements.txt to enable inference.", }, status_code=503) entry = _store.get(app_id) if not entry or entry.get("file_type") != "gguf": return JSONResponse({"error": "Not a GGUF artifact."}, status_code=400) llm = _get_llm(app_id) if llm is None: return JSONResponse({"error": "Failed to load model."}, status_code=500) return JSONResponse({ "ok": True, "message": f"Model '{entry['filename']}' loaded.", "context_length": entry.get("gguf", {}).get("context_length", 2048), }) @app.post("/api/inference/chat/{app_id}") async def inference_chat(app_id: str, request: Request) -> JSONResponse: """OpenAI-compatible chat completion endpoint using a loaded GGUF model.""" if not HAS_LLAMA: return JSONResponse({"error": "Inference unavailable. llama-cpp-python not installed."}, status_code=503) llm = _get_llm(app_id) if llm is None: return JSONResponse({"error": "Model not loaded. POST /api/inference/load/{app_id} first."}, status_code=400) try: data = await request.json() except Exception: return JSONResponse({"error": "Invalid JSON"}, status_code=400) messages = data.get("messages", []) if not messages: return JSONResponse({"error": "messages is required"}, status_code=400) max_tokens = min(int(data.get("max_tokens", 512)), 2048) temperature = float(data.get("temperature", 0.7)) stream = bool(data.get("stream", False)) if stream: def gen(): for chunk in llm.create_chat_completion( messages=messages, max_tokens=max_tokens, temperature=temperature, stream=True, ): yield f"data: {json.dumps(chunk)}\n\n" yield "data: [DONE]\n\n" return StreamingResponse(gen(), media_type="text/event-stream") result = llm.create_chat_completion( messages=messages, max_tokens=max_tokens, temperature=temperature, ) return JSONResponse(result) @app.post("/api/inference/completion/{app_id}") async def inference_completion(app_id: str, request: Request) -> JSONResponse: """Text completion endpoint using a loaded GGUF model.""" if not HAS_LLAMA: return JSONResponse({"error": "Inference unavailable. llama-cpp-python not installed."}, status_code=503) llm = _get_llm(app_id) if llm is None: return JSONResponse({"error": "Model not loaded."}, status_code=400) try: data = await request.json() except Exception: return JSONResponse({"error": "Invalid JSON"}, status_code=400) prompt = data.get("prompt", "") if not prompt: return JSONResponse({"error": "prompt is required"}, status_code=400) max_tokens = min(int(data.get("max_tokens", 256)), 2048) temperature = float(data.get("temperature", 0.7)) result = llm(prompt=prompt, max_tokens=max_tokens, temperature=temperature) return JSONResponse(result) # ─── ChatGPT Export → dApp ────────────────────────────────────────────────── def _parse_chatgpt_export(data: Any) -> dict[str, Any]: """Parse a ChatGPT export conversations.json structure.""" conversations = [] raw = data if isinstance(data, list) else data.get("conversations", data) if isinstance(data, dict) else [] for conv in raw: if not isinstance(conv, dict): continue title = conv.get("title", "Untitled") conv_id = conv.get("id", conv.get("uuid", str(uuid.uuid4()))) create_time = conv.get("create_time", 0) update_time = conv.get("update_time", 0) messages = [] mapping = conv.get("mapping", {}) if isinstance(mapping, dict): for node_id, node in mapping.items(): if not isinstance(node, dict): continue msg = node.get("message") if not msg or not isinstance(msg, dict): continue author = msg.get("author", {}) role = author.get("role", "unknown") content = msg.get("content", {}) parts = content.get("parts", []) text = "" for p in parts: if isinstance(p, str): text += p elif isinstance(p, dict): text += p.get("text", str(p)) if text.strip(): messages.append({ "role": role, "text": text[:50000], "create_time": msg.get("create_time", 0), }) messages.sort(key=lambda m: m.get("create_time", 0) or 0) conversations.append({ "id": str(conv_id), "title": title, "message_count": len(messages), "create_time": create_time, "update_time": update_time, "messages": messages, }) conversations.sort(key=lambda c: c.get("create_time", 0) or 0, reverse=True) return {"conversations": conversations, "total": len(conversations)} def _generate_dapp_html(parsed: dict[str, Any], title: str) -> str: """Generate a self-contained dApp HTML for browsing ChatGPT conversations.""" convs = parsed["conversations"] convs_json = json.dumps(convs[:500]) # Limit to 500 conversations return f''' {title} — ChatGPT Archive dApp
💬

ChatGPT Archive dApp

Select a conversation to browse

''' @app.post("/api/import/chatgpt") async def import_chatgpt(file: UploadFile = File(...)) -> JSONResponse: """Import a ChatGPT export (conversations.json) and deploy as a browsable dApp.""" if not file.filename: return JSONResponse({"error": "No filename provided."}, status_code=400) fname = file.filename.lower() if not (fname.endswith(".json") or fname.endswith(".zip")): return JSONResponse({"error": "Only .json or .zip ChatGPT exports are accepted."}, status_code=400) app_id = str(uuid.uuid4()) slug = _slugify(file.filename) or f"chatgpt-archive-{app_id[:8]}" # Save the uploaded file if fname.endswith(".zip"): zip_path = UPLOAD_DIR / f"{app_id}.zip" with open(zip_path, "wb") as f: while True: chunk = await file.read(65536) if not chunk: break f.write(chunk) # Extract and find conversations.json extract_to = EXTRACT_DIR / app_id extract_to.mkdir(parents=True, exist_ok=True) try: with zipfile.ZipFile(zip_path, "r") as zf: zf.extractall(extract_to) except zipfile.BadZipFile: shutil.rmtree(extract_to) zip_path.unlink(missing_ok=True) return JSONResponse({"error": "Invalid ZIP file."}, status_code=400) # Find conversations.json conv_file = None for f in extract_to.rglob("conversations.json"): conv_file = f break if not conv_file: shutil.rmtree(extract_to) return JSONResponse({"error": "No conversations.json found in ZIP export."}, status_code=400) raw_data = json.loads(conv_file.read_text(encoding="utf-8")) sha256 = _hash_file(zip_path) stored_size = zip_path.stat().st_size else: # Direct JSON upload json_path = UPLOAD_DIR / f"{app_id}.json" with open(json_path, "wb") as f: while True: chunk = await file.read(65536) if not chunk: break f.write(chunk) try: raw_data = json.loads(json_path.read_text(encoding="utf-8")) except json.JSONDecodeError: json_path.unlink(missing_ok=True) return JSONResponse({"error": "Invalid JSON file."}, status_code=400) sha256 = _hash_file(json_path) stored_size = json_path.stat().st_size # Parse the ChatGPT export parsed = _parse_chatgpt_export(raw_data) if parsed["total"] == 0: return JSONResponse({"error": "No conversations found in export."}, status_code=400) # Generate dApp HTML title = f"ChatGPT Archive ({parsed['total']} conversations)" dapp_html = _generate_dapp_html(parsed, title) # Deploy as a site dapp_root = DAPPS_DIR / app_id dapp_root.mkdir(parents=True, exist_ok=True) (dapp_root / "index.html").write_text(dapp_html, encoding="utf-8") entry = { "app_id": app_id, "slug": slug, "filename": file.filename, "file_type": "chatgpt", "size": stored_size, "size_human": _format_bytes(stored_size), "sha256": sha256, "dapp_url": f"/dapp/{app_id}/", "conversation_count": parsed["total"], "total_messages": sum(c["message_count"] for c in parsed["conversations"]), "has_preview": True, "preview_entry": "index.html", "opened": True, "created_at": time.time(), } _store[app_id] = entry _save_db() return JSONResponse({ "app": entry, "message": f"ChatGPT archive deployed as dApp. {parsed['total']} conversations, {entry['total_messages']} messages.", "dapp_url": entry["dapp_url"], }, status_code=201) @app.get("/dapp/{app_id}/", response_class=HTMLResponse) @app.get("/dapp/{app_id}", response_class=HTMLResponse) def serve_dapp(app_id: str) -> HTMLResponse: """Serve a deployed ChatGPT dApp.""" entry = _store.get(app_id) if not entry or entry.get("file_type") != "chatgpt": return HTMLResponse("

dApp not found

", status_code=404) dapp_root = DAPPS_DIR / app_id if not dapp_root.exists(): return HTMLResponse("

dApp files not found. Storage may have been cleared.

", status_code=404) index = dapp_root / "index.html" if not index.exists(): return HTMLResponse("

dApp index not found

", status_code=404) return HTMLResponse(content=index.read_text(encoding="utf-8")) @app.get("/dapp/{app_id}/{path:path}") def serve_dapp_file(app_id: str, path: str): """Serve any file from a deployed dApp.""" entry = _store.get(app_id) if not entry or entry.get("file_type") != "chatgpt": return JSONResponse({"error": "dApp not found"}, status_code=404) dapp_root = DAPPS_DIR / app_id if not dapp_root.exists(): return JSONResponse({"error": "dApp storage cleared"}, status_code=404) target = dapp_root / path try: target.resolve().relative_to(dapp_root.resolve()) except ValueError: return JSONResponse({"error": "Access denied"}, status_code=403) if not target.exists(): return JSONResponse({"error": "File not found"}, status_code=404) return FileResponse(path=target) @app.get("/api/storage/status") def storage_status() -> JSONResponse: """Check if persistent storage is attached.""" persistent = str(DATA_DIR).startswith("/data") return JSONResponse({ "persistent": persistent, "data_dir": str(DATA_DIR), "has_uploads": UPLOAD_DIR.exists(), "has_extracted": EXTRACT_DIR.exists(), "has_sites": SITES_DIR.exists(), "has_dapps": DAPPS_DIR.exists(), "inference_available": HAS_LLAMA, "artifact_count": len(_store), }) @app.delete("/api/apps/{app_id}") def delete_app(app_id: str) -> JSONResponse: entry = _store.pop(app_id, None) if entry: # Clean up uploads (all possible extensions) for suffix in [".dmg", ".gguf", ".zip", ".json"]: f = UPLOAD_DIR / f"{app_id}{suffix}" if f.exists(): f.unlink() # Clean up extracted files extracted = EXTRACT_DIR / app_id if extracted.exists(): shutil.rmtree(extracted) # Clean up deployed sites site = SITES_DIR / app_id if site.exists(): shutil.rmtree(site) # Clean up dapps dapp = DAPPS_DIR / app_id if dapp.exists(): shutil.rmtree(dapp) # Unload model from inference cache _llm_cache.pop(app_id, None) _save_db() return JSONResponse({"ok": True})