diff --git a/lib/agent/handlers.go b/lib/agent/handlers.go index e3c68a7..06390e4 100644 --- a/lib/agent/handlers.go +++ b/lib/agent/handlers.go @@ -172,7 +172,11 @@ func (a *Agent) handleDownload(w http.ResponseWriter, r *http.Request) { _, _ = f.Seek(0, io.SeekStart) } w.Header().Set("Content-Type", contentType) - w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(name, `"`, "'")+`"`) + disp := "attachment" + if r.URL.Query().Get("inline") == "1" { + disp = "inline" + } + w.Header().Set("Content-Disposition", disp+`; filename="`+strings.ReplaceAll(name, `"`, "'")+`"`) w.Header().Set("Accept-Ranges", "bytes") http.ServeContent(w, r, name, info.ModTime(), f) } diff --git a/lib/agent/web/app.js b/lib/agent/web/app.js index 1b24512..a943a88 100644 --- a/lib/agent/web/app.js +++ b/lib/agent/web/app.js @@ -61,6 +61,20 @@ return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[i]}`; } + // ponytail: extension allowlist; browsers that can't render still get a tab (download/blank) + const previewExt = new Set([ + "png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", + "pdf", + "txt", "log", "csv", "json", "xml", "html", "htm", "css", "js", "md", + "mp4", "webm", "ogg", "mp3", "wav", + ]); + + function isPreviewable(filename) { + const i = String(filename).lastIndexOf("."); + if (i < 0) return false; + return previewExt.has(String(filename).slice(i + 1).toLowerCase()); + } + function joinPath(base, name) { if (!base) return name; if (/[\\/]$/.test(base)) return base + name; @@ -178,15 +192,27 @@ const modified = document.createElement("td"); modified.textContent = entry.modified_time ? new Date(entry.modified_time).toLocaleString() : ""; const action = document.createElement("td"); + action.className = "actions"; + const full = joinPath(data.path, entry.name); const del = document.createElement("button"); del.type = "button"; del.textContent = "Delete"; - const full = joinPath(data.path, entry.name); del.addEventListener("click", (event) => { event.stopPropagation(); deleteFile(full, entry.name).catch((err) => showError(err.message)); }); action.append(del); + if (entry.type === "file" && isPreviewable(entry.name)) { + const preview = document.createElement("button"); + preview.type = "button"; + preview.textContent = "Preview"; + preview.addEventListener("click", (event) => { + event.stopPropagation(); + const url = `/api/v1/download?path=${encodeURIComponent(full)}&inline=1`; + window.open(url, "_blank", "noopener"); + }); + action.append(preview); + } tr.append(name, type, size, modified, action); fileRows.append(tr); } diff --git a/lib/agent/web/index.html b/lib/agent/web/index.html index 86694d8..5867051 100644 --- a/lib/agent/web/index.html +++ b/lib/agent/web/index.html @@ -101,6 +101,16 @@ th { color: var(--muted); font-weight: 600; font-size: 0.8rem; } td.name { cursor: pointer; color: var(--accent); } td.name.file { color: var(--text); } + td.actions { white-space: nowrap; text-align: right; } + td.actions button { + font: inherit; + border: 1px solid var(--line); + background: var(--card); + border-radius: 6px; + padding: 0.25rem 0.55rem; + cursor: pointer; + } + td.actions button + button { margin-left: 0.35rem; } pre { margin: 0.5rem 0 0; padding: 0.75rem;