From 0d98e16f29673a1165e85a3c15353d7da03acd45 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 26 Jun 2026 18:36:04 +0100 Subject: [PATCH] feat: implement SFTP file upload progress tracking using XHR in API and UI components --- frontend/src/api.ts | 37 ++++++++++++++++++++++----- frontend/src/components/SftpPanel.vue | 22 ++++++++++++++-- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 39fd84c..b074cec 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -318,16 +318,41 @@ export const api = { await handle(res); }, - async sftpUpload(connId: string, path: string, file: File): Promise { + async sftpUpload( + connId: string, + path: string, + file: File, + onProgress?: (loaded: number, total: number) => void + ): Promise { const fd = new FormData(); fd.set("path", path); fd.set("file", file); - const res = await fetch(`/api/sftp/${connId}/upload`, { - method: "POST", - credentials: "include", - body: fd, + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", `/api/sftp/${connId}/upload`); + xhr.withCredentials = true; + if (onProgress) { + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + onProgress(e.loaded, e.total); + } + }; + } + xhr.onload = () => { + if (xhr.status === 401) return reject(new Error("unauthorized")); + let data: any = {}; + try { + data = JSON.parse(xhr.responseText); + } catch (e) {} + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(data.error || xhr.statusText)); + } + }; + xhr.onerror = () => reject(new Error("Network Error")); + xhr.send(fd); }); - await handle(res); }, sftpDownloadUrl(connId: string, path: string): string { diff --git a/frontend/src/components/SftpPanel.vue b/frontend/src/components/SftpPanel.vue index 9de810a..b3dead7 100644 --- a/frontend/src/components/SftpPanel.vue +++ b/frontend/src/components/SftpPanel.vue @@ -11,6 +11,7 @@ const err = ref(""); const busy = ref(false); const renameTarget = ref(null); const newName = ref(""); +const uploadProgress = ref(null); function isDir(m: number): boolean { return (m & 0o170000) === 0o040000; @@ -66,11 +67,16 @@ async function onUpload(ev: Event) { input.value = ""; if (!file) return; err.value = ""; + uploadProgress.value = 0; try { - await api.sftpUpload(props.connId, path.value, file); + await api.sftpUpload(props.connId, path.value, file, (loaded, total) => { + uploadProgress.value = Math.round((loaded / total) * 100); + }); await load(); } catch (e) { err.value = e instanceof Error ? e.message : "Upload failed"; + } finally { + uploadProgress.value = null; } } @@ -186,7 +192,19 @@ function fmtSize(n: number): string {

{{ err }}

-

Loading…

+
+
+ Uploading... + {{ uploadProgress }}% +
+
+
+
+
+

Loading…