From 3b1c1aeda12a4d3fc693fc42b3a4ccf99da38b97 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Mar 2026 04:11:45 -0400 Subject: [PATCH] feat(sftp): add download save-to-disk + upload support, remove debug banner --- backend/src/terminal/sftp.gateway.ts | 10 +++++++ frontend/components/sftp/SftpSidebar.vue | 33 +++++++++++++++++++---- frontend/composables/useSftp.ts | 34 +++++++++++++++++++++--- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/backend/src/terminal/sftp.gateway.ts b/backend/src/terminal/sftp.gateway.ts index c6115ac..2dfc657 100644 --- a/backend/src/terminal/sftp.gateway.ts +++ b/backend/src/terminal/sftp.gateway.ts @@ -109,6 +109,16 @@ export class SftpGateway { stream.on('error', (e: any) => this.send(client, { type: 'error', message: e.message })); break; } + case 'upload': { + const buf = Buffer.from(msg.data, 'base64'); + const stream = sftp.createWriteStream(msg.path); + stream.end(buf, () => { + this.logger.log(`[SFTP] Upload complete: ${msg.path} (${buf.length} bytes)`); + this.send(client, { type: 'uploaded', path: msg.path, size: buf.length }); + }); + stream.on('error', (e: any) => this.send(client, { type: 'error', message: e.message })); + break; + } case 'mkdir': { sftp.mkdir(msg.path, (err: any) => { if (err) return this.send(client, { type: 'error', message: err.message }); diff --git a/frontend/components/sftp/SftpSidebar.vue b/frontend/components/sftp/SftpSidebar.vue index 872b18b..e785313 100644 --- a/frontend/components/sftp/SftpSidebar.vue +++ b/frontend/components/sftp/SftpSidebar.vue @@ -9,7 +9,7 @@ const props = defineProps<{ const sessionIdRef = computed(() => props.sessionId) const { entries, currentPath, fileContent, - connect, disconnect, list, readFile, writeFile, mkdir, rename, remove, download, + connect, disconnect, list, readFile, writeFile, mkdir, rename, remove, download, upload, } = useSftp(sessionIdRef) const width = ref(260) // resizable sidebar width in px @@ -18,6 +18,27 @@ const newFolderName = ref('') const showNewFolderInput = ref(false) const renameTarget = ref(null) const renameTo = ref('') +const fileInput = ref(null) + +function triggerUpload() { + fileInput.value?.click() +} + +function handleFileSelected(event: Event) { + const input = event.target as HTMLInputElement + const files = input.files + if (!files?.length) return + for (const file of files) { + const reader = new FileReader() + reader.onload = () => { + const base64 = (reader.result as string).split(',')[1] + const destPath = `${currentPath.value === '/' ? '' : currentPath.value}/${file.name}` + upload(destPath, base64) + } + reader.readAsDataURL(file) + } + input.value = '' // reset so same file can be re-uploaded +} onMounted(() => { connect() @@ -103,10 +124,6 @@ const breadcrumbs = computed(() => {