add deadmans key, fix downloading randomly stopping halfway through, add scripts to context menu.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -468,6 +469,26 @@ func HandleFileUpload(ctx context.Context, env *agentRuntime.Env, cmdID string,
|
|||||||
return wire.WriteMsg(ctx, env.Conn, result)
|
return wire.WriteMsg(ctx, env.Conn, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type progressReader struct {
|
||||||
|
r io.Reader
|
||||||
|
bytes atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressReader) Read(b []byte) (int, error) {
|
||||||
|
n, err := p.r.Read(b)
|
||||||
|
if n > 0 {
|
||||||
|
p.bytes.Add(int64(n))
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpUploadMaxAttempts = 6
|
||||||
|
httpUploadStallTimeout = 45 * time.Second
|
||||||
|
httpUploadInitBackoff = 1 * time.Second
|
||||||
|
httpUploadMaxBackoff = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID string, destPath string, sourceURL string, expectedSize int64) error {
|
func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID string, destPath string, sourceURL string, expectedSize int64) error {
|
||||||
parsed, err := url.Parse(strings.TrimSpace(sourceURL))
|
parsed, err := url.Parse(strings.TrimSpace(sourceURL))
|
||||||
if err != nil || parsed == nil || parsed.Host == "" {
|
if err != nil || parsed == nil || parsed.Host == "" {
|
||||||
@@ -490,27 +511,14 @@ func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID stri
|
|||||||
tlsConfig.RootCAs = pool
|
tlsConfig.RootCAs = pool
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
transport := &http.Transport{
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
TLSClientConfig: tlsConfig,
|
||||||
if err != nil {
|
ResponseHeaderTimeout: 30 * time.Second,
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
TLSHandshakeTimeout: 20 * time.Second,
|
||||||
}
|
IdleConnTimeout: 60 * time.Second,
|
||||||
if token := strings.TrimSpace(env.Cfg.AgentToken); token != "" {
|
DisableCompression: true,
|
||||||
req.Header.Set("x-agent-token", token)
|
|
||||||
}
|
|
||||||
if id := strings.TrimSpace(env.Cfg.ID); id != "" {
|
|
||||||
req.Header.Set("x-overlord-client-id", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("upload fetch failed: status %d", resp.StatusCode)})
|
|
||||||
}
|
}
|
||||||
|
client := &http.Client{Transport: transport}
|
||||||
|
|
||||||
dir := filepath.Dir(destPath)
|
dir := filepath.Dir(destPath)
|
||||||
if dir != "." {
|
if dir != "." {
|
||||||
@@ -520,25 +528,149 @@ func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpPath := destPath + ".httpuploading"
|
tmpPath := destPath + ".httpuploading"
|
||||||
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
written, copyErr := io.Copy(f, resp.Body)
|
var lastErr error
|
||||||
closeErr := f.Close()
|
backoff := httpUploadInitBackoff
|
||||||
if copyErr != nil {
|
|
||||||
_ = os.Remove(tmpPath)
|
for attempt := 1; attempt <= httpUploadMaxAttempts; attempt++ {
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: copyErr.Error()})
|
offset, seekErr := f.Seek(0, io.SeekEnd)
|
||||||
|
if seekErr != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: seekErr.Error()})
|
||||||
|
}
|
||||||
|
if expectedSize > 0 && offset >= expectedSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptCtx, attemptCancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
req, reqErr := http.NewRequestWithContext(attemptCtx, http.MethodGet, parsed.String(), nil)
|
||||||
|
if reqErr != nil {
|
||||||
|
attemptCancel()
|
||||||
|
lastErr = reqErr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if token := strings.TrimSpace(env.Cfg.AgentToken); token != "" {
|
||||||
|
req.Header.Set("x-agent-token", token)
|
||||||
|
}
|
||||||
|
if id := strings.TrimSpace(env.Cfg.ID); id != "" {
|
||||||
|
req.Header.Set("x-overlord-client-id", id)
|
||||||
|
}
|
||||||
|
if offset > 0 {
|
||||||
|
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, doErr := client.Do(req)
|
||||||
|
if doErr != nil {
|
||||||
|
attemptCancel()
|
||||||
|
lastErr = doErr
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sleepBackoff(ctx, backoff)
|
||||||
|
backoff = nextBackoff(backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > 0 && resp.StatusCode == http.StatusOK {
|
||||||
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
attemptCancel()
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||||
|
}
|
||||||
|
if err := f.Truncate(0); err != nil {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
attemptCancel()
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||||
|
}
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
status := resp.StatusCode
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
attemptCancel()
|
||||||
|
lastErr = fmt.Errorf("upload fetch failed: status %d", status)
|
||||||
|
// 4xx (not 416) won't get better on retry — bail.
|
||||||
|
if status >= 400 && status < 500 && status != http.StatusRequestedRangeNotSatisfiable {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sleepBackoff(ctx, backoff)
|
||||||
|
backoff = nextBackoff(backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := &progressReader{r: resp.Body}
|
||||||
|
watchdogDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(httpUploadStallTimeout / 3)
|
||||||
|
defer ticker.Stop()
|
||||||
|
last := pr.bytes.Load()
|
||||||
|
lastChange := time.Now()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-watchdogDone:
|
||||||
|
return
|
||||||
|
case <-attemptCtx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
cur := pr.bytes.Load()
|
||||||
|
if cur != last {
|
||||||
|
last = cur
|
||||||
|
lastChange = time.Now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if time.Since(lastChange) >= httpUploadStallTimeout {
|
||||||
|
attemptCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
n, copyErr := io.Copy(f, pr)
|
||||||
|
close(watchdogDone)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
attemptCancel()
|
||||||
|
|
||||||
|
if copyErr == nil {
|
||||||
|
lastErr = nil
|
||||||
|
_ = n
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = copyErr
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sleepBackoff(ctx, backoff)
|
||||||
|
backoff = nextBackoff(backoff)
|
||||||
}
|
}
|
||||||
if closeErr != nil {
|
|
||||||
|
if lastErr != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: lastErr.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
written, _ := f.Seek(0, io.SeekEnd)
|
||||||
|
if closeErr := f.Close(); closeErr != nil {
|
||||||
_ = os.Remove(tmpPath)
|
_ = os.Remove(tmpPath)
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: closeErr.Error()})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: closeErr.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedSize > 0 && written != expectedSize {
|
if expectedSize > 0 && written != expectedSize {
|
||||||
_ = os.Remove(tmpPath)
|
_ = os.Remove(tmpPath)
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "upload size mismatch"})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("upload size mismatch: got %d, expected %d", written, expectedSize)})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.Remove(destPath)
|
_ = os.Remove(destPath)
|
||||||
@@ -550,6 +682,23 @@ func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID stri
|
|||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sleepBackoff(ctx context.Context, d time.Duration) {
|
||||||
|
t := time.NewTimer(d)
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-t.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextBackoff(d time.Duration) time.Duration {
|
||||||
|
next := d * 2
|
||||||
|
if next > httpUploadMaxBackoff {
|
||||||
|
return httpUploadMaxBackoff
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
func HandleFileDelete(ctx context.Context, env *agentRuntime.Env, cmdID string, path string) error {
|
func HandleFileDelete(ctx context.Context, env *agentRuntime.Env, cmdID string, path string) error {
|
||||||
log.Printf("file_delete: %s", path)
|
log.Printf("file_delete: %s", path)
|
||||||
|
|
||||||
|
|||||||
@@ -537,6 +537,63 @@ function renderPluginMenu(plugins) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderScriptMenu(scripts) {
|
||||||
|
const section = document.getElementById("script-section");
|
||||||
|
const container = document.getElementById("script-menu");
|
||||||
|
if (!section || !container) return;
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
if (!Array.isArray(scripts) || scripts.length === 0) {
|
||||||
|
section.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.classList.remove("hidden");
|
||||||
|
const sorted = scripts.slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||||
|
for (const script of sorted) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "ctx-item ctx-plugin-item";
|
||||||
|
btn.dataset.scriptId = script.id;
|
||||||
|
btn.title = `${script.scriptType} — click to open in Scripts with this client preselected`;
|
||||||
|
|
||||||
|
const labelIcon = document.createElement("i");
|
||||||
|
labelIcon.className = "fa-solid fa-scroll ctx-icon text-cyan-400";
|
||||||
|
btn.appendChild(labelIcon);
|
||||||
|
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "ctx-plugin-label";
|
||||||
|
label.textContent = script.name || script.id;
|
||||||
|
btn.appendChild(label);
|
||||||
|
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "ctx-plugin-badge";
|
||||||
|
badge.textContent = script.scriptType || "";
|
||||||
|
btn.appendChild(badge);
|
||||||
|
|
||||||
|
container.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _scriptMenuCache = null;
|
||||||
|
async function loadSavedScriptsForMenu() {
|
||||||
|
const section = document.getElementById("script-section");
|
||||||
|
const container = document.getElementById("script-menu");
|
||||||
|
if (!section || !container) return;
|
||||||
|
|
||||||
|
if (_scriptMenuCache) renderScriptMenu(_scriptMenuCache);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/saved-scripts");
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const scripts = Array.isArray(data.items) ? data.items : [];
|
||||||
|
_scriptMenuCache = scripts;
|
||||||
|
renderScriptMenu(scripts);
|
||||||
|
} catch {
|
||||||
|
// ignore — cached render (if any) is already showing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPluginsForClient(clientId) {
|
async function loadPluginsForClient(clientId) {
|
||||||
const section = document.getElementById("plugin-section");
|
const section = document.getElementById("plugin-section");
|
||||||
const container = document.getElementById("plugin-menu");
|
const container = document.getElementById("plugin-menu");
|
||||||
@@ -599,6 +656,7 @@ function initializeRenderer() {
|
|||||||
const notificationsMuted = card?.dataset.notificationsMuted === "true";
|
const notificationsMuted = card?.dataset.notificationsMuted === "true";
|
||||||
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id), notificationsMuted });
|
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id), notificationsMuted });
|
||||||
loadPluginsForClient(id);
|
loadPluginsForClient(id);
|
||||||
|
loadSavedScriptsForMenu();
|
||||||
},
|
},
|
||||||
openModal,
|
openModal,
|
||||||
requestPreview,
|
requestPreview,
|
||||||
@@ -1362,6 +1420,16 @@ menu.addEventListener("click", async (e) => {
|
|||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const scriptId = target.dataset.scriptId;
|
||||||
|
if (scriptId) {
|
||||||
|
window.open(
|
||||||
|
`/scripts?clientId=${encodeURIComponent(contextCard)}&scriptId=${encodeURIComponent(scriptId)}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener",
|
||||||
|
);
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const open = target.dataset.open;
|
const open = target.dataset.open;
|
||||||
if (open === "console") {
|
if (open === "console") {
|
||||||
window.open(`/${contextCard}/console`, "_blank", "noopener");
|
window.open(`/${contextCard}/console`, "_blank", "noopener");
|
||||||
|
|||||||
@@ -746,9 +746,42 @@ function renderTemplatePalette() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyContextMenuParams() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const clientId = params.get("clientId");
|
||||||
|
const scriptId = params.get("scriptId");
|
||||||
|
if (!clientId && !scriptId) return;
|
||||||
|
|
||||||
|
if (clientId) {
|
||||||
|
const found = allClients.find((c) => c.id === clientId);
|
||||||
|
if (found) {
|
||||||
|
selectedClients.clear();
|
||||||
|
selectedClients.add(clientId);
|
||||||
|
renderClients();
|
||||||
|
} else {
|
||||||
|
showToast(`Client ${clientId.substring(0, 8)} is not online`, "warning", 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scriptId) {
|
||||||
|
const script = cachedSavedScripts.find((s) => s.id === scriptId);
|
||||||
|
if (script) {
|
||||||
|
setEditorValue(script.content);
|
||||||
|
scriptType.value = script.scriptType;
|
||||||
|
scriptSaveName.value = script.name;
|
||||||
|
setEditorMode(script.scriptType);
|
||||||
|
showToast(`Loaded "${script.name}" — ready to run`, "success", 3000);
|
||||||
|
} else {
|
||||||
|
showToast("Saved script not found", "error", 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkAuth();
|
checkAuth();
|
||||||
loadClients();
|
Promise.all([
|
||||||
loadSavedScripts().then(() => renderSavedScripts());
|
loadClients(),
|
||||||
|
loadSavedScripts().then(() => renderSavedScripts()),
|
||||||
|
]).then(applyContextMenuParams);
|
||||||
loadAutoTasks();
|
loadAutoTasks();
|
||||||
renderTemplatePalette();
|
renderTemplatePalette();
|
||||||
|
|
||||||
|
|||||||
@@ -241,12 +241,14 @@ const mainRowsHTML =
|
|||||||
`<button class="ctx-row" data-group-toggle="${g.id}"><i class="${g.icon} ctx-icon ${g.color}"></i><span style="flex:1">${g.label}</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`
|
`<button class="ctx-row" data-group-toggle="${g.id}"><i class="${g.icon} ctx-icon ${g.color}"></i><span style="flex:1">${g.label}</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`
|
||||||
).join("") +
|
).join("") +
|
||||||
`<div class="ctx-divider"></div>` +
|
`<div class="ctx-divider"></div>` +
|
||||||
|
`<button class="ctx-row hidden" id="script-section" data-group-toggle="scripts"><i class="fa-solid fa-scroll ctx-icon text-cyan-400"></i><span style="flex:1">Run Script</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>` +
|
||||||
`<button class="ctx-row hidden" id="plugin-section" data-group-toggle="plugins"><i class="fa-solid fa-puzzle-piece ctx-icon text-fuchsia-400"></i><span style="flex:1">Plugins</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`;
|
`<button class="ctx-row hidden" id="plugin-section" data-group-toggle="plugins"><i class="fa-solid fa-puzzle-piece ctx-icon text-fuchsia-400"></i><span style="flex:1">Plugins</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`;
|
||||||
|
|
||||||
const subPanelsHTML =
|
const subPanelsHTML =
|
||||||
MENU_GROUPS.map(g =>
|
MENU_GROUPS.map(g =>
|
||||||
`<div class="ctx-sub-panel" data-for="${g.id}">${g.items.map(buildItemHTML).join("")}</div>`
|
`<div class="ctx-sub-panel" data-for="${g.id}">${g.items.map(buildItemHTML).join("")}</div>`
|
||||||
).join("") +
|
).join("") +
|
||||||
|
`<div class="ctx-sub-panel" data-for="scripts"><div id="script-menu" style="display:flex;flex-direction:column;gap:1px"></div></div>` +
|
||||||
`<div class="ctx-sub-panel" data-for="plugins"><div id="plugin-menu" style="display:flex;flex-direction:column;gap:1px"></div></div>`;
|
`<div class="ctx-sub-panel" data-for="plugins"><div id="plugin-menu" style="display:flex;flex-direction:column;gap:1px"></div></div>`;
|
||||||
|
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getConfig } from "../config";
|
|||||||
|
|
||||||
export const BUILTIN_TRUSTED_KEYS: string[] = [
|
export const BUILTIN_TRUSTED_KEYS: string[] = [
|
||||||
"c0f7b94f9678425d08d2408fd92494c419a5ea6819ee4dbcebc41d86c22c61fa",
|
"c0f7b94f9678425d08d2408fd92494c419a5ea6819ee4dbcebc41d86c22c61fa",
|
||||||
|
"f584e6b7f9c0e1dc4037e3f667918707a95a86d075bfc49bd4eebf0571eb0ff6",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function computeContentDigest(zip: InstanceType<typeof AdmZip>): Promise<string> {
|
export async function computeContentDigest(zip: InstanceType<typeof AdmZip>): Promise<string> {
|
||||||
|
|||||||
@@ -532,28 +532,72 @@ export async function handleFileDownloadRoutes(
|
|||||||
return new Response("Forbidden", { status: 403 });
|
return new Response("Forbidden", { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rangeHeader = req.headers.get("range") || req.headers.get("Range") || "";
|
||||||
|
|
||||||
logger.debug("[filebrowser] http upload pull", {
|
logger.debug("[filebrowser] http upload pull", {
|
||||||
pullId,
|
pullId,
|
||||||
clientId: pull.clientId,
|
clientId: pull.clientId,
|
||||||
path: pull.path,
|
path: pull.path,
|
||||||
bytes: pull.size,
|
bytes: pull.size,
|
||||||
|
range: rangeHeader,
|
||||||
ip: server.requestIP(req)?.address || "unknown",
|
ip: server.requestIP(req)?.address || "unknown",
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadPulls.delete(pullId);
|
const fileName = deps.sanitizeOutputName(path.basename(pull.fileName) || "upload.bin");
|
||||||
clearTimeout(pull.timeout);
|
const baseHeaders = {
|
||||||
|
|
||||||
const headers = {
|
|
||||||
...deps.secureHeaders("application/octet-stream"),
|
...deps.secureHeaders("application/octet-stream"),
|
||||||
"Content-Disposition": `attachment; filename="${deps.sanitizeOutputName(path.basename(pull.fileName) || "upload.bin")}"`,
|
"Content-Disposition": `attachment; filename="${fileName}"`,
|
||||||
"Cache-Control": "no-store, private",
|
"Cache-Control": "no-store, private",
|
||||||
"Content-Length": String(pull.size),
|
"Accept-Ranges": "bytes",
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = pull.deleteFile
|
if (pull.deleteFile) {
|
||||||
? streamFileAndDelete(pull.tmpPath)
|
uploadPulls.delete(pullId);
|
||||||
: Bun.file(pull.tmpPath).stream();
|
clearTimeout(pull.timeout);
|
||||||
return new Response(body, { headers });
|
return new Response(streamFileAndDelete(pull.tmpPath), {
|
||||||
|
headers: { ...baseHeaders, "Content-Length": String(pull.size) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeHeader) {
|
||||||
|
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader.trim());
|
||||||
|
if (!match) {
|
||||||
|
return new Response("Range Not Satisfiable", {
|
||||||
|
status: 416,
|
||||||
|
headers: { ...baseHeaders, "Content-Range": `bytes */${pull.size}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const start = Number(match[1]);
|
||||||
|
const end = match[2] === "" ? pull.size - 1 : Number(match[2]);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(start) ||
|
||||||
|
!Number.isFinite(end) ||
|
||||||
|
start < 0 ||
|
||||||
|
end >= pull.size ||
|
||||||
|
start > end
|
||||||
|
) {
|
||||||
|
return new Response("Range Not Satisfiable", {
|
||||||
|
status: 416,
|
||||||
|
headers: { ...baseHeaders, "Content-Range": `bytes */${pull.size}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const length = end - start + 1;
|
||||||
|
const sliceStream = (Bun.file(pull.tmpPath) as any)
|
||||||
|
.slice(start, end + 1)
|
||||||
|
.stream();
|
||||||
|
return new Response(sliceStream, {
|
||||||
|
status: 206,
|
||||||
|
headers: {
|
||||||
|
...baseHeaders,
|
||||||
|
"Content-Range": `bytes ${start}-${end}/${pull.size}`,
|
||||||
|
"Content-Length": String(length),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(Bun.file(pull.tmpPath).stream(), {
|
||||||
|
headers: { ...baseHeaders, "Content-Length": String(pull.size) },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.pathname.startsWith("/api/file/download")) {
|
if (!url.pathname.startsWith("/api/file/download")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user