|
|
|
|
@ -1266,6 +1266,56 @@ async def delete_run(request: Request, run_id: str):
|
|
|
|
|
return RedirectResponse(url=f"{BASE_PATH}/", status_code=303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/d/{run_id}/fork", response_class=HTMLResponse)
|
|
|
|
|
async def fork_run(request: Request, run_id: str):
|
|
|
|
|
src_dir = _run_dir_for_id(run_id)
|
|
|
|
|
if src_dir is None:
|
|
|
|
|
return RedirectResponse(url=f"{BASE_PATH}/?error=Invalid%20run%20id", status_code=303)
|
|
|
|
|
|
|
|
|
|
client_ip = request.client.host if request.client else "unknown"
|
|
|
|
|
|
|
|
|
|
# Forking counts as a convert: enforce daily convert quota
|
|
|
|
|
if not _consume_rate_limit(client_ip, key="convert", limit=CONVERT_DAILY_LIMIT):
|
|
|
|
|
logger.info("fork.rate_limit_exceeded from=%s ip=%s", run_id, client_ip)
|
|
|
|
|
return RedirectResponse(url=f"{BASE_PATH}/?error=Rate%20limit%20exceeded", status_code=303)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
new_id = _new_run_id()
|
|
|
|
|
new_dir = RUNS_DIR / new_id
|
|
|
|
|
new_dir.mkdir(parents=True, exist_ok=False)
|
|
|
|
|
|
|
|
|
|
# Copy a selected set of files so the fork is usable: source .tex, original upload, and common artifacts
|
|
|
|
|
candidates = list(src_dir.glob("original.*")) + [
|
|
|
|
|
src_dir / "diagram.tex",
|
|
|
|
|
src_dir / "diagram.pdf",
|
|
|
|
|
src_dir / "diagram.png",
|
|
|
|
|
src_dir / "diagram.svg",
|
|
|
|
|
src_dir / "preview.png",
|
|
|
|
|
src_dir / "run.log.txt",
|
|
|
|
|
src_dir / "meta.json",
|
|
|
|
|
]
|
|
|
|
|
for p in candidates:
|
|
|
|
|
if p.exists() and p.is_file():
|
|
|
|
|
shutil.copy2(p, new_dir / p.name)
|
|
|
|
|
|
|
|
|
|
# Update or create meta for the new run
|
|
|
|
|
meta = _read_run_meta(new_dir)
|
|
|
|
|
meta["forked_from"] = run_id
|
|
|
|
|
meta["created_at"] = time.time()
|
|
|
|
|
meta["updated_at"] = time.time()
|
|
|
|
|
_write_run_meta(new_dir, meta)
|
|
|
|
|
|
|
|
|
|
logger.info("fork.ok from=%s to=%s ip=%s", run_id, new_id, client_ip)
|
|
|
|
|
run_log = RunLogger(new_dir, logger)
|
|
|
|
|
run_log.line(f"fork.from={run_id} by_ip={client_ip}")
|
|
|
|
|
run_log.line("rate.consumed key=convert")
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("fork.error from=%s", run_id)
|
|
|
|
|
return RedirectResponse(url=f"{BASE_PATH}/d/{run_id}?error=Fork%20failed", status_code=303)
|
|
|
|
|
|
|
|
|
|
return RedirectResponse(url=f"{BASE_PATH}/d/{new_id}", status_code=303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/d/{run_id}/files/{filename}")
|
|
|
|
|
async def get_run_file(run_id: str, filename: str):
|
|
|
|
|
path = _safe_join_run_file(run_id, filename)
|
|
|
|
|
|