feat: implement SFTP file upload progress tracking using XHR in API and UI components
This commit is contained in:
+31
-6
@@ -318,16 +318,41 @@ export const api = {
|
|||||||
await handle(res);
|
await handle(res);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sftpUpload(connId: string, path: string, file: File): Promise<void> {
|
async sftpUpload(
|
||||||
|
connId: string,
|
||||||
|
path: string,
|
||||||
|
file: File,
|
||||||
|
onProgress?: (loaded: number, total: number) => void
|
||||||
|
): Promise<void> {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.set("path", path);
|
fd.set("path", path);
|
||||||
fd.set("file", file);
|
fd.set("file", file);
|
||||||
const res = await fetch(`/api/sftp/${connId}/upload`, {
|
return new Promise((resolve, reject) => {
|
||||||
method: "POST",
|
const xhr = new XMLHttpRequest();
|
||||||
credentials: "include",
|
xhr.open("POST", `/api/sftp/${connId}/upload`);
|
||||||
body: fd,
|
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 {
|
sftpDownloadUrl(connId: string, path: string): string {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const err = ref("");
|
|||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const renameTarget = ref<SftpEntry | null>(null);
|
const renameTarget = ref<SftpEntry | null>(null);
|
||||||
const newName = ref("");
|
const newName = ref("");
|
||||||
|
const uploadProgress = ref<number | null>(null);
|
||||||
|
|
||||||
function isDir(m: number): boolean {
|
function isDir(m: number): boolean {
|
||||||
return (m & 0o170000) === 0o040000;
|
return (m & 0o170000) === 0o040000;
|
||||||
@@ -66,11 +67,16 @@ async function onUpload(ev: Event) {
|
|||||||
input.value = "";
|
input.value = "";
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
err.value = "";
|
err.value = "";
|
||||||
|
uploadProgress.value = 0;
|
||||||
try {
|
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();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err.value = e instanceof Error ? e.message : "Upload failed";
|
err.value = e instanceof Error ? e.message : "Upload failed";
|
||||||
|
} finally {
|
||||||
|
uploadProgress.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +192,19 @@ function fmtSize(n: number): string {
|
|||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1 overflow-auto p-2">
|
<div class="min-h-0 flex-1 overflow-auto p-2">
|
||||||
<p v-if="err" class="mb-2 text-xs text-red-400">{{ err }}</p>
|
<p v-if="err" class="mb-2 text-xs text-red-400">{{ err }}</p>
|
||||||
<p v-if="busy" class="text-xs text-slate-500">Loading…</p>
|
<div v-if="uploadProgress !== null" class="mb-2 space-y-1">
|
||||||
|
<div class="flex justify-between text-[10px] text-slate-400">
|
||||||
|
<span>Uploading...</span>
|
||||||
|
<span>{{ uploadProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full overflow-hidden rounded-full bg-slate-800">
|
||||||
|
<div
|
||||||
|
class="h-full bg-accent transition-all duration-200"
|
||||||
|
:style="{ width: uploadProgress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else-if="busy" class="text-xs text-slate-500">Loading…</p>
|
||||||
<ul v-else class="space-y-0.5">
|
<ul v-else class="space-y-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="e in entries"
|
v-for="e in entries"
|
||||||
|
|||||||
Reference in New Issue
Block a user