Add upload progress bar with SSE extraction status
ISO uploads now show a progress bar during file transfer (via XHR upload.onprogress) and real-time extraction status (via SSE events through the existing Hub). Falls back to plain form POST if JS is disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -160,7 +160,8 @@ func imageUploadForm(errMsg string) string {
|
||||
return layout("Upload Image", fmt.Sprintf(`
|
||||
<h2>Upload Boot Image</h2>
|
||||
%s
|
||||
<form method="POST" action="/images/upload" enctype="multipart/form-data" class="form">
|
||||
<form id="upload-form" method="POST" action="/images/upload" enctype="multipart/form-data" class="form">
|
||||
<input type="hidden" name="upload_id" id="upload-id" value="">
|
||||
<label>Name<input type="text" name="name" placeholder="proxmox-8.2" required pattern="[a-z0-9][a-z0-9.\-]*"></label>
|
||||
<label>Version<input type="text" name="version" placeholder="8.2-1" required></label>
|
||||
<label>Kind
|
||||
@@ -169,9 +170,16 @@ func imageUploadForm(errMsg string) string {
|
||||
</select>
|
||||
</label>
|
||||
<label>ISO File<input type="file" name="iso" accept=".iso" required></label>
|
||||
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">Upload may take several minutes for large ISOs. The kernel and initrd will be extracted automatically.</p>
|
||||
<button type="submit" class="btn">Upload & Extract</button>
|
||||
</form>
|
||||
<div id="upload-progress" style="display:none" class="upload-progress">
|
||||
<h3 id="progress-title">Uploading ISO...</h3>
|
||||
<div class="progress-bar-track">
|
||||
<div class="progress-bar-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progress-text">Preparing upload...</div>
|
||||
<div class="progress-detail" id="progress-detail"></div>
|
||||
</div>
|
||||
`, errHTML))
|
||||
}
|
||||
|
||||
|
||||
+38
-8
@@ -167,34 +167,64 @@ func (u *UI) NewImageForm(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (u *UI) UploadImage(w http.ResponseWriter, r *http.Request) {
|
||||
isXHR := r.Header.Get("X-Requested-With") == "XMLHttpRequest"
|
||||
|
||||
if err := r.ParseMultipartForm(0); err != nil {
|
||||
renderHTML(w, imageUploadForm("Invalid form submission"))
|
||||
if isXHR {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"ok": false, "error": "Invalid form submission"})
|
||||
} else {
|
||||
renderHTML(w, imageUploadForm("Invalid form submission"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
version := strings.TrimSpace(r.FormValue("version"))
|
||||
kind := strings.TrimSpace(r.FormValue("kind"))
|
||||
uploadID := strings.TrimSpace(r.FormValue("upload_id"))
|
||||
|
||||
file, _, err := r.FormFile("iso")
|
||||
if err != nil {
|
||||
renderHTML(w, imageUploadForm("ISO file is required"))
|
||||
if isXHR {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"ok": false, "error": "ISO file is required"})
|
||||
} else {
|
||||
renderHTML(w, imageUploadForm("ISO file is required"))
|
||||
}
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var progressFn image.ProgressFunc
|
||||
if uploadID != "" {
|
||||
progressFn = func(stage, detail string) {
|
||||
u.Hub.Publish(events.Event{
|
||||
Name: "image.upload_progress",
|
||||
Payload: fmt.Sprintf(`{"upload_id":%q,"stage":%q,"detail":%q}`, uploadID, stage, detail),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, err = u.ImageSvc.Upload(r.Context(), image.UploadParams{
|
||||
Name: name,
|
||||
Kind: kind,
|
||||
Version: version,
|
||||
ISO: file,
|
||||
Name: name,
|
||||
Kind: kind,
|
||||
Version: version,
|
||||
ISO: file,
|
||||
OnProgress: progressFn,
|
||||
})
|
||||
if err != nil {
|
||||
renderHTML(w, imageUploadForm(err.Error()))
|
||||
if isXHR {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"ok": false, "error": err.Error()})
|
||||
} else {
|
||||
renderHTML(w, imageUploadForm(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/images", http.StatusSeeOther)
|
||||
if isXHR {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
} else {
|
||||
http.Redirect(w, r, "/images", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UI) SetDefaultImage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user