Add file download for job models

- GET /files/{filename}: serves file from STL_UPLOAD_DIR with path-traversal guard
- Download button in File & Geometry card header (always visible when file attached)
- Download button also appears at the top of the Start Job modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-18 22:52:26 +02:00
parent 51a7791461
commit bef57027f2
2 changed files with 33 additions and 7 deletions

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from pathlib import Path
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import case, or_
from sqlalchemy.orm import Session
@@ -297,6 +297,18 @@ def edit_job(
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
@router.get("/files/{filename}")
def download_file(filename: str):
from fastapi import HTTPException
stl_dir = get_stl_dir()
path = (stl_dir / filename).resolve()
if not str(path).startswith(str(stl_dir.resolve())):
raise HTTPException(400, "Invalid filename")
if not path.exists():
raise HTTPException(404, "File not found")
return FileResponse(path, filename=filename, media_type="application/octet-stream")
@router.post("/jobs/{job_id}/start")
def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(get_db)):
from fastapi import HTTPException

View File

@@ -98,12 +98,19 @@
<div class="card h-100">
<div class="card-header border-secondary d-flex align-items-center justify-content-between" style="border-color:#21262d">
<span class="text-secondary small text-uppercase fw-normal">File &amp; Geometry</span>
{% if job.file_name and not job.file_name.endswith('.gcode') %}
<form method="POST" action="/jobs/{{ job.id }}/analyse">
<button type="submit" class="btn btn-outline-secondary btn-sm py-0 px-2" style="font-size:.75rem">
<i class="bi bi-cpu me-1"></i>Compute now
</button>
</form>
{% if job.file_name %}
<div class="d-flex gap-2">
<a href="/files/{{ job.file_name }}" class="btn btn-outline-primary btn-sm py-0 px-2" style="font-size:.75rem" download>
<i class="bi bi-download me-1"></i>Download
</a>
{% if not job.file_name.endswith('.gcode') %}
<form method="POST" action="/jobs/{{ job.id }}/analyse">
<button type="submit" class="btn btn-outline-secondary btn-sm py-0 px-2" style="font-size:.75rem">
<i class="bi bi-cpu me-1"></i>Compute now
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
<div class="card-body">
@@ -350,6 +357,13 @@
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{% if job.file_name %}
<div class="mb-3">
<a href="/files/{{ job.file_name }}" class="btn btn-outline-primary btn-sm w-100" download>
<i class="bi bi-download me-1"></i>Download {{ job.file_name }}
</a>
</div>
{% endif %}
{% if available_printers %}
<label class="form-label text-secondary small">Select printer <span class="text-danger">*</span></label>
<select name="printer_id" class="form-select bg-dark border-secondary text-white" required>