feat: add fork functionality to create a copy of a run

main
Antonio De Lucreziis 6 months ago
parent 0d1806738c
commit 7dbab98466

@ -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)

@ -86,6 +86,15 @@
<i class="fa-solid fa-trash-can icon" aria-hidden="true"></i>
<span>Delete Diagram</span>
</button>
<button
class="btn"
type="submit"
formaction="{{ base_path }}/d/{{ run_id }}/fork"
title="Create a copy of this run"
>
<i class="fa-solid fa-code-branch icon" aria-hidden="true"></i>
<span>Fork</span>
</button>
</div>
</section>
</form>
@ -143,13 +152,14 @@
const currentTex = latexArea ? latexArea.value : ""
let historyEntries = []
// Validate edit/compile/delete actions.
// Validate edit/compile/delete/fork actions.
if (editForm && instructionsInput) {
editForm.addEventListener("submit", e => {
const submitter = e.submitter
const action = (submitter && submitter.getAttribute("formaction")) || ""
const isAlternateAction = !!action
const isDeleteAction = action.endsWith("/delete")
const isForkAction = action.endsWith("/fork")
const val = (instructionsInput.value || "").trim()
if (isDeleteAction) {
@ -159,6 +169,13 @@
return
}
if (isForkAction) {
if (!confirm("Create a fork (copy) of this run?")) {
e.preventDefault()
}
return
}
if (!isAlternateAction && !val) {
e.preventDefault()
}

Loading…
Cancel
Save