feat(sftp): add download save-to-disk + upload support, remove debug banner
This commit is contained in:
parent
a9702795a4
commit
3b1c1aeda1
@ -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 });
|
||||
|
||||
@ -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<string | null>(null)
|
||||
const renameTo = ref('')
|
||||
const fileInput = ref<HTMLInputElement | null>(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(() => {
|
||||
<template>
|
||||
<div class="flex h-full" :style="{ width: width + 'px' }">
|
||||
<div class="flex flex-col h-full bg-gray-900 border-r border-gray-800 overflow-hidden flex-1">
|
||||
<!-- Debug banner — remove once SFTP is working -->
|
||||
<div class="px-2 py-1 bg-yellow-900/50 text-yellow-300 text-xs border-b border-yellow-800">
|
||||
SFTP: {{ entries.length }} entries | path: {{ currentPath }} | sid: {{ sessionId?.substring(0, 8) }}
|
||||
</div>
|
||||
<!-- Toolbar -->
|
||||
<div class="px-2 py-1.5 border-b border-gray-800 shrink-0">
|
||||
<!-- Breadcrumbs -->
|
||||
@ -139,6 +156,12 @@ const breadcrumbs = computed(() => {
|
||||
class="text-xs text-gray-500 hover:text-wraith-400 px-1.5 py-0.5 rounded hover:bg-gray-800"
|
||||
title="New folder"
|
||||
>+ Folder</button>
|
||||
<button
|
||||
@click="triggerUpload"
|
||||
class="text-xs text-gray-500 hover:text-wraith-400 px-1.5 py-0.5 rounded hover:bg-gray-800"
|
||||
title="Upload file"
|
||||
>↑ Upload</button>
|
||||
<input ref="fileInput" type="file" multiple class="hidden" @change="handleFileSelected" />
|
||||
</div>
|
||||
<!-- New folder input -->
|
||||
<div v-if="showNewFolderInput" class="flex gap-1 mt-1">
|
||||
|
||||
@ -8,6 +8,7 @@ export function useSftp(sessionId: Ref<string | null>) {
|
||||
const currentPath = ref('/')
|
||||
const fileContent = ref<{ path: string; content: string } | null>(null)
|
||||
const transfers = ref<any[]>([])
|
||||
const activeDownloads = new Map<string, { path: string; chunks: string[]; total: number }>()
|
||||
|
||||
let pendingList: string | null = null
|
||||
|
||||
@ -50,9 +51,35 @@ export function useSftp(sessionId: Ref<string | null>) {
|
||||
fileContent.value = null
|
||||
list(currentPath.value)
|
||||
break
|
||||
case 'progress':
|
||||
// Update transfer progress
|
||||
case 'uploaded':
|
||||
list(currentPath.value)
|
||||
break
|
||||
case 'downloadStart':
|
||||
activeDownloads.set(msg.transferId, { path: msg.path, chunks: [], total: msg.total })
|
||||
break
|
||||
case 'downloadChunk':
|
||||
const dl = activeDownloads.get(msg.transferId)
|
||||
if (dl) dl.chunks.push(msg.data)
|
||||
break
|
||||
case 'downloadComplete': {
|
||||
const transfer = activeDownloads.get(msg.transferId)
|
||||
if (transfer) {
|
||||
const binary = atob(transfer.chunks.join(''))
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
const blob = new Blob([bytes])
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = transfer.path.split('/').pop() || 'download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
activeDownloads.delete(msg.transferId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'error':
|
||||
console.error('SFTP error:', msg.message)
|
||||
break
|
||||
@ -90,6 +117,7 @@ export function useSftp(sessionId: Ref<string | null>) {
|
||||
function remove(path: string) { send({ type: 'delete', path }) }
|
||||
function chmod(path: string, mode: string) { send({ type: 'chmod', path, mode }) }
|
||||
function download(path: string) { send({ type: 'download', path }) }
|
||||
function upload(path: string, dataBase64: string) { send({ type: 'upload', path, data: dataBase64 }) }
|
||||
|
||||
// If sessionId arrives after WS is already open, send any pending list
|
||||
watch(sessionId, (newId) => {
|
||||
@ -107,6 +135,6 @@ export function useSftp(sessionId: Ref<string | null>) {
|
||||
|
||||
return {
|
||||
entries, currentPath, fileContent, transfers,
|
||||
connect, disconnect, list, readFile, writeFile, mkdir, rename, remove, chmod, download,
|
||||
connect, disconnect, list, readFile, writeFile, mkdir, rename, remove, chmod, download, upload,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user