mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-21 20:22:22 +02:00
131 lines
4.1 KiB
Python
131 lines
4.1 KiB
Python
"""SubApp for .swarm sharing. Three endpoints: export (returns the bundle bytes
|
|
as a download), import/preflight (parse + stage in a sandbox, no writes), and
|
|
import/commit (write the staged entities with fresh ids). Staging is in-process
|
|
with a TTL; a lost token just means re-open the file."""
|
|
import logging
|
|
import shutil
|
|
import time
|
|
import uuid
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import File, HTTPException, Response, UploadFile
|
|
|
|
from backend.config.Apps import SubApp
|
|
|
|
from backend.apps.swarm import closure
|
|
from backend.apps.swarm.models import (
|
|
ExportPreflightResponse,
|
|
ExportRequest,
|
|
ImportCommitRequest,
|
|
ImportCommitResponse,
|
|
ImportPreflightResponse,
|
|
RequirementView,
|
|
)
|
|
from backend.apps.swarm.ziputil import MAX_TOTAL_BYTES, BundleError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
P_STAGING: dict[str, dict] = {}
|
|
P_STAGING_TTL = 30 * 60 # 30 minutes
|
|
|
|
|
|
def p_gc_staging() -> None:
|
|
now = time.time()
|
|
for token in list(P_STAGING):
|
|
if now - P_STAGING[token]["created_at"] > P_STAGING_TTL:
|
|
p_discard(token)
|
|
|
|
|
|
def p_discard(token: str) -> None:
|
|
entry = P_STAGING.pop(token, None)
|
|
if entry:
|
|
shutil.rmtree(entry["sandbox"], ignore_errors=True)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def swarm_lifespan():
|
|
p_gc_staging()
|
|
try:
|
|
yield
|
|
finally:
|
|
for token in list(P_STAGING):
|
|
p_discard(token)
|
|
|
|
|
|
swarm = SubApp("swarm", swarm_lifespan)
|
|
|
|
|
|
@swarm.router.post("/export/preflight")
|
|
async def export_preflight(body: ExportRequest) -> ExportPreflightResponse:
|
|
try:
|
|
manifest = closure.build_manifest(body.type, body.id)
|
|
except BundleError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
return ExportPreflightResponse(
|
|
summary=closure.summarize(manifest),
|
|
filename=closure.swarm_filename(manifest.root.name),
|
|
link_supported=False,
|
|
)
|
|
|
|
|
|
@swarm.router.post("/export")
|
|
async def export_bundle(body: ExportRequest) -> Response:
|
|
try:
|
|
raw, name = closure.build_bundle(body.type, body.id)
|
|
except BundleError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
fname = closure.swarm_filename(name)
|
|
return Response(
|
|
content=raw,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
|
)
|
|
|
|
|
|
@swarm.router.post("/import/preflight")
|
|
async def import_preflight(file: UploadFile = File(...)) -> ImportPreflightResponse:
|
|
raw = await file.read()
|
|
if len(raw) > MAX_TOTAL_BYTES:
|
|
raise HTTPException(status_code=400, detail="file is too large")
|
|
try:
|
|
sandbox, manifest, warnings = closure.stage_upload(raw, file.filename or "")
|
|
conflicts = closure.detect_conflicts(sandbox, manifest)
|
|
review = closure.review_bundle(sandbox, manifest)
|
|
except BundleError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
p_gc_staging()
|
|
token = uuid.uuid4().hex
|
|
P_STAGING[token] = {"sandbox": sandbox, "manifest": manifest, "created_at": time.time()}
|
|
return ImportPreflightResponse(
|
|
summary=closure.summarize(manifest),
|
|
staging_token=token,
|
|
conflicts=conflicts,
|
|
review=review,
|
|
warnings=warnings,
|
|
)
|
|
|
|
|
|
@swarm.router.post("/import/commit")
|
|
async def import_commit(body: ImportCommitRequest) -> ImportCommitResponse:
|
|
entry = P_STAGING.get(body.staging_token)
|
|
if not entry:
|
|
raise HTTPException(status_code=404, detail="import session expired; please re-open the file")
|
|
try:
|
|
root_type, root_id, created, unresolved = closure.commit(
|
|
entry["sandbox"], entry["manifest"], body.accept_requirements
|
|
)
|
|
except BundleError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
finally:
|
|
p_discard(body.staging_token)
|
|
if root_id is None:
|
|
raise HTTPException(status_code=400, detail="bundle has no root entity")
|
|
return ImportCommitResponse(
|
|
root_type=root_type,
|
|
root_id=root_id,
|
|
created=created,
|
|
unresolved_requirements=[
|
|
RequirementView(kind=r.kind, key=r.key, label=r.label, detail=r.detail) for r in unresolved
|
|
],
|
|
)
|