dsh-ssh-workbench 0.0.0-stage → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +552 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +178 -2
- package/SECURITY.md +27 -0
- package/cordis.patch.yml +5 -0
- package/docs/architecture.md +146 -0
- package/docs/execution-model.md +83 -0
- package/docs/images/chat-at-resource.png +0 -0
- package/docs/images/plugin.png +0 -0
- package/docs/images/remote-workbench.png +0 -0
- package/docs/images/workbench.png +0 -0
- package/docs/quickstart.md +106 -0
- package/lib/client.js +4991 -0
- package/lib/core/activity.js +88 -0
- package/lib/core/background.js +69 -0
- package/lib/core/broker.js +802 -0
- package/lib/core/dialect.js +28 -0
- package/lib/core/errors.js +138 -0
- package/lib/core/execution-stats.js +88 -0
- package/lib/core/gate-pwsh.js +88 -0
- package/lib/core/gate.js +572 -0
- package/lib/core/guard.js +123 -0
- package/lib/core/known-hosts.js +74 -0
- package/lib/core/live-executions.js +101 -0
- package/lib/core/local-file-api.js +174 -0
- package/lib/core/localfs.js +381 -0
- package/lib/core/managed-context.js +276 -0
- package/lib/core/mentions.js +216 -0
- package/lib/core/paths.js +104 -0
- package/lib/core/posix-quote.js +4 -0
- package/lib/core/posix-shell.js +89 -0
- package/lib/core/prefs.js +38 -0
- package/lib/core/remote-exec-tool.js +394 -0
- package/lib/core/remote-file-tools.js +559 -0
- package/lib/core/remote-shell-tool.js +310 -0
- package/lib/core/secrets.js +95 -0
- package/lib/core/server-ref.js +61 -0
- package/lib/core/shell-adapters.js +48 -0
- package/lib/core/ssh.js +761 -0
- package/lib/core/store.js +83 -0
- package/lib/core/terminals.js +299 -0
- package/lib/core/win32-adapter.js +141 -0
- package/lib/index.js +409 -0
- package/lib/routes.js +759 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +89 -4
- package/vendor/NOTICE.md +50 -0
- package/vendor/XTERM-LICENSE +21 -0
- package/vendor/node_modules/asn1/LICENSE +19 -0
- package/vendor/node_modules/asn1/README.md +50 -0
- package/vendor/node_modules/asn1/lib/ber/errors.js +13 -0
- package/vendor/node_modules/asn1/lib/ber/index.js +27 -0
- package/vendor/node_modules/asn1/lib/ber/reader.js +262 -0
- package/vendor/node_modules/asn1/lib/ber/types.js +36 -0
- package/vendor/node_modules/asn1/lib/ber/writer.js +317 -0
- package/vendor/node_modules/asn1/lib/index.js +20 -0
- package/vendor/node_modules/asn1/package.json +31 -0
- package/vendor/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
- package/vendor/node_modules/bcrypt-pbkdf/LICENSE +66 -0
- package/vendor/node_modules/bcrypt-pbkdf/README.md +45 -0
- package/vendor/node_modules/bcrypt-pbkdf/index.js +556 -0
- package/vendor/node_modules/bcrypt-pbkdf/package.json +15 -0
- package/vendor/node_modules/safer-buffer/LICENSE +21 -0
- package/vendor/node_modules/safer-buffer/Readme.md +156 -0
- package/vendor/node_modules/safer-buffer/dangerous.js +58 -0
- package/vendor/node_modules/safer-buffer/package.json +34 -0
- package/vendor/node_modules/safer-buffer/safer.js +77 -0
- package/vendor/node_modules/tweetnacl/AUTHORS.md +28 -0
- package/vendor/node_modules/tweetnacl/LICENSE +24 -0
- package/vendor/node_modules/tweetnacl/README.md +459 -0
- package/vendor/node_modules/tweetnacl/nacl-fast.js +2388 -0
- package/vendor/node_modules/tweetnacl/nacl.js +1175 -0
- package/vendor/node_modules/tweetnacl/package.json +58 -0
- package/vendor/ssh2/LICENSE +19 -0
- package/vendor/ssh2/lib/Channel.js +295 -0
- package/vendor/ssh2/lib/agent.js +1123 -0
- package/vendor/ssh2/lib/client.js +2176 -0
- package/vendor/ssh2/lib/http-agents.js +84 -0
- package/vendor/ssh2/lib/index.js +44 -0
- package/vendor/ssh2/lib/keygen.js +582 -0
- package/vendor/ssh2/lib/protocol/Protocol.js +2136 -0
- package/vendor/ssh2/lib/protocol/SFTP.js +4052 -0
- package/vendor/ssh2/lib/protocol/constants.js +356 -0
- package/vendor/ssh2/lib/protocol/crypto/poly1305.js +43 -0
- package/vendor/ssh2/lib/protocol/crypto.js +1602 -0
- package/vendor/ssh2/lib/protocol/handlers.js +16 -0
- package/vendor/ssh2/lib/protocol/handlers.misc.js +1285 -0
- package/vendor/ssh2/lib/protocol/kex.js +1908 -0
- package/vendor/ssh2/lib/protocol/keyParser.js +1484 -0
- package/vendor/ssh2/lib/protocol/node-fs-compat.js +115 -0
- package/vendor/ssh2/lib/protocol/utils.js +356 -0
- package/vendor/ssh2/lib/protocol/zlib.js +255 -0
- package/vendor/ssh2/lib/server.js +1380 -0
- package/vendor/ssh2/lib/utils.js +336 -0
- package/vendor/ssh2/package.json +34 -0
- package/vendor/xterm.js +53 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-local known_hosts with strict-by-default trust.
|
|
3
|
+
*
|
|
4
|
+
* ssh2 verifies the host key with `hostHash: 'sha256'`; the verifier receives
|
|
5
|
+
* the hex digest, so this store keeps `<address> sha256:<hex>` lines. Unknown
|
|
6
|
+
* hosts are rejected unless the user explicitly trusts the fingerprint.
|
|
7
|
+
*/
|
|
8
|
+
import { knownHostsPath, readTextFile, writePrivateFile } from './paths.js'
|
|
9
|
+
|
|
10
|
+
function addressOf(server) {
|
|
11
|
+
return `${server.host}:${server.port}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeFingerprint(fingerprint) {
|
|
15
|
+
const value = String(fingerprint ?? '').trim().toLowerCase()
|
|
16
|
+
if (!value) return ''
|
|
17
|
+
return value.startsWith('sha256:') ? value : `sha256:${value}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parse(text) {
|
|
21
|
+
const map = new Map()
|
|
22
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
23
|
+
const line = rawLine.trim()
|
|
24
|
+
if (!line || line.startsWith('#')) continue
|
|
25
|
+
const match = /^(\S+)\s+(sha256:[0-9a-f]+)$/i.exec(line)
|
|
26
|
+
if (match) map.set(match[1], match[2].toLowerCase())
|
|
27
|
+
}
|
|
28
|
+
return map
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createKnownHosts({ file = knownHostsPath() } = {}) {
|
|
32
|
+
const load = () => parse(readTextFile(file))
|
|
33
|
+
const save = (map) => {
|
|
34
|
+
const lines = [...map.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([address, fp]) => `${address} ${fp}`)
|
|
35
|
+
writePrivateFile(file, `${lines.join('\n')}${lines.length ? '\n' : ''}`)
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
addressOf,
|
|
39
|
+
/** `mode: 'strict'` rejects unknown hosts; `'accept-new'` records them (TOFU). */
|
|
40
|
+
verify(server, fingerprint, mode = 'strict') {
|
|
41
|
+
const address = addressOf(server)
|
|
42
|
+
const normalized = normalizeFingerprint(fingerprint)
|
|
43
|
+
if (!normalized) return false
|
|
44
|
+
const map = load()
|
|
45
|
+
const known = map.get(address)
|
|
46
|
+
if (known) return known === normalized
|
|
47
|
+
if (mode === 'accept-new') {
|
|
48
|
+
map.set(address, normalized)
|
|
49
|
+
save(map)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
},
|
|
54
|
+
trust(server, fingerprint) {
|
|
55
|
+
const normalized = normalizeFingerprint(fingerprint)
|
|
56
|
+
if (!normalized) throw new Error('a fingerprint is required')
|
|
57
|
+
const map = load()
|
|
58
|
+
map.set(addressOf(server), normalized)
|
|
59
|
+
save(map)
|
|
60
|
+
},
|
|
61
|
+
revoke(server) {
|
|
62
|
+
const map = load()
|
|
63
|
+
const deleted = map.delete(addressOf(server))
|
|
64
|
+
save(map)
|
|
65
|
+
return deleted
|
|
66
|
+
},
|
|
67
|
+
list() {
|
|
68
|
+
return [...load().entries()].map(([address, fingerprint]) => ({ address, fingerprint }))
|
|
69
|
+
},
|
|
70
|
+
has(server) {
|
|
71
|
+
return load().has(addressOf(server))
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight remote executions, for the composer's "current target" indicator.
|
|
3
|
+
*
|
|
4
|
+
* This is a display-only projection. It never selects a target, never reaches
|
|
5
|
+
* a tool, and disappears as soon as the dispatch settles. Every remote tool is
|
|
6
|
+
* explicit-server, so this map is the only place where the UI can learn what a
|
|
7
|
+
* running tool is touching.
|
|
8
|
+
*/
|
|
9
|
+
import { sessionKeyOf } from './remote-shell-tool.js'
|
|
10
|
+
import {
|
|
11
|
+
REMOTE_EDIT_TOOL_NAME,
|
|
12
|
+
REMOTE_GLOB_TOOL_NAME,
|
|
13
|
+
REMOTE_GREP_TOOL_NAME,
|
|
14
|
+
REMOTE_READ_TOOL_NAME,
|
|
15
|
+
REMOTE_WRITE_TOOL_NAME,
|
|
16
|
+
} from './remote-file-tools.js'
|
|
17
|
+
import { REMOTE_EXEC_TOOL_NAME } from './remote-exec-tool.js'
|
|
18
|
+
|
|
19
|
+
const SINGLE_SERVER_TOOLS = new Set([
|
|
20
|
+
'remote_bash',
|
|
21
|
+
'remote_pwsh',
|
|
22
|
+
REMOTE_READ_TOOL_NAME,
|
|
23
|
+
REMOTE_WRITE_TOOL_NAME,
|
|
24
|
+
REMOTE_EDIT_TOOL_NAME,
|
|
25
|
+
REMOTE_GLOB_TOOL_NAME,
|
|
26
|
+
REMOTE_GREP_TOOL_NAME,
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** Extract the explicit server names a remote tool call will touch. */
|
|
30
|
+
export function serversOf(exec) {
|
|
31
|
+
const name = exec?.name
|
|
32
|
+
const args = exec?.arguments
|
|
33
|
+
if (!name || !args || typeof args !== 'object') return []
|
|
34
|
+
if (name === REMOTE_EXEC_TOOL_NAME) {
|
|
35
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks : []
|
|
36
|
+
const names = []
|
|
37
|
+
for (const task of tasks) {
|
|
38
|
+
if (task && typeof task.server === 'string' && task.server.length > 0 && !names.includes(task.server)) {
|
|
39
|
+
names.push(task.server)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return names
|
|
43
|
+
}
|
|
44
|
+
if (!SINGLE_SERVER_TOOLS.has(name)) return []
|
|
45
|
+
return typeof args.server === 'string' && args.server.length > 0 ? [args.server] : []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {{ now?: () => number }} [options] */
|
|
49
|
+
export function createLiveExecutions(options = {}) {
|
|
50
|
+
const now = options.now ?? Date.now
|
|
51
|
+
let nextToken = 0
|
|
52
|
+
/** @type {Map<number, { sessionId: string, tool: string, servers: string[], startedAt: number }>} */
|
|
53
|
+
const active = new Map()
|
|
54
|
+
|
|
55
|
+
/** Register one dispatch; returns undefined when this is not a remote call. */
|
|
56
|
+
function begin(exec) {
|
|
57
|
+
const servers = serversOf(exec)
|
|
58
|
+
if (servers.length === 0) return undefined
|
|
59
|
+
const sessionId = sessionKeyOf(exec)
|
|
60
|
+
if (!sessionId) return undefined
|
|
61
|
+
const token = ++nextToken
|
|
62
|
+
active.set(token, {
|
|
63
|
+
sessionId,
|
|
64
|
+
tool: typeof exec.name === 'string' ? exec.name : '',
|
|
65
|
+
servers,
|
|
66
|
+
startedAt: now(),
|
|
67
|
+
})
|
|
68
|
+
return token
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function end(token) {
|
|
72
|
+
if (token !== undefined) active.delete(token)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @returns {{ count: number, servers: { name: string, count: number, startedAt: number }[] }} */
|
|
76
|
+
function snapshot(sessionId) {
|
|
77
|
+
const byName = new Map()
|
|
78
|
+
let calls = 0
|
|
79
|
+
for (const entry of active.values()) {
|
|
80
|
+
if (entry.sessionId !== sessionId) continue
|
|
81
|
+
calls += 1
|
|
82
|
+
for (const name of entry.servers) {
|
|
83
|
+
const previous = byName.get(name)
|
|
84
|
+
byName.set(name, {
|
|
85
|
+
name,
|
|
86
|
+
count: (previous?.count ?? 0) + 1,
|
|
87
|
+
startedAt: previous ? Math.min(previous.startedAt, entry.startedAt) : entry.startedAt,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { count: calls, servers: [...byName.values()] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function clear(sessionId) {
|
|
95
|
+
for (const [token, entry] of active) {
|
|
96
|
+
if (sessionId === undefined || entry.sessionId === sessionId) active.delete(token)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { begin, end, snapshot, clear, size: () => active.size }
|
|
101
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-filesystem implementation of the files-panel API.
|
|
3
|
+
*
|
|
4
|
+
* The workbench reuses the very same files component for remote SFTP and the
|
|
5
|
+
* local workspace. This adapter gives the local side the same method surface
|
|
6
|
+
* as the SSH runtime (`sftpHome`, `sftpList`, `sftpRead`, chunked open/read/
|
|
7
|
+
* write sessions, mkdir/remove/rename), so the panel and the file viewer stay
|
|
8
|
+
* provider-blind. Every path is resolved through the scoped local filesystem
|
|
9
|
+
* browser.
|
|
10
|
+
*/
|
|
11
|
+
import fsp from 'node:fs/promises'
|
|
12
|
+
import { randomUUID } from 'node:crypto'
|
|
13
|
+
|
|
14
|
+
export const LOCAL_FILE_REF = '__local__'
|
|
15
|
+
|
|
16
|
+
const SYMLINK_WRITE_ERROR = 'refusing to write through a symlink'
|
|
17
|
+
|
|
18
|
+
export function createLocalFileApi(localfs) {
|
|
19
|
+
const server = { id: LOCAL_FILE_REF, name: LOCAL_FILE_REF, platform: 'local' }
|
|
20
|
+
const sessions = new Map()
|
|
21
|
+
|
|
22
|
+
const sessionFor = (id, kind) => {
|
|
23
|
+
const session = sessions.get(id)
|
|
24
|
+
if (!session || session.kind !== kind) throw new Error('local transfer session not found')
|
|
25
|
+
return session
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function lstatOrNull(target) {
|
|
29
|
+
try {
|
|
30
|
+
return await fsp.lstat(target)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Open a local path for a transfer session.
|
|
39
|
+
*
|
|
40
|
+
* Write sessions resolve the path as a directory entry (parents followed and
|
|
41
|
+
* bounded by the root, final component not followed) and refuse to write
|
|
42
|
+
* through a final symlink, so an uploaded file cannot escape via a link.
|
|
43
|
+
* Read sessions keep follow semantics (a symlink inside the root is readable;
|
|
44
|
+
* one leaving the root is rejected by `localfs.resolve`).
|
|
45
|
+
*/
|
|
46
|
+
async function openLocal(target, flags, { noFollowFinal = false } = {}) {
|
|
47
|
+
const resolved = noFollowFinal
|
|
48
|
+
? await localfs.resolveEntry(target)
|
|
49
|
+
: await localfs.resolve(target)
|
|
50
|
+
if (noFollowFinal) {
|
|
51
|
+
const info = await lstatOrNull(resolved)
|
|
52
|
+
if (info && info.isSymbolicLink()) throw new Error(SYMLINK_WRITE_ERROR)
|
|
53
|
+
}
|
|
54
|
+
const handle = await fsp.open(resolved, flags)
|
|
55
|
+
return { resolved, handle }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Canonical local workspace root, mirroring the remote login home. */
|
|
59
|
+
async function sftpHome(_server) {
|
|
60
|
+
return await localfs.rootReal()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function sftpList(_server, remotePath) {
|
|
64
|
+
const target = typeof remotePath === 'string' && remotePath.trim() ? remotePath : await localfs.rootReal()
|
|
65
|
+
const result = await localfs.list(target)
|
|
66
|
+
return result.entries
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function sftpRead(_server, remotePath, options = {}) {
|
|
70
|
+
const result = await localfs.read(remotePath, options.maxBytes)
|
|
71
|
+
const buffer = Buffer.from(result.base64, 'base64')
|
|
72
|
+
const binary = buffer.includes(0)
|
|
73
|
+
const formatted = {
|
|
74
|
+
path: result.path,
|
|
75
|
+
size: result.size,
|
|
76
|
+
bytesRead: buffer.length,
|
|
77
|
+
truncated: result.truncated,
|
|
78
|
+
binary,
|
|
79
|
+
content: binary ? undefined : buffer.toString('utf8'),
|
|
80
|
+
}
|
|
81
|
+
if (options.base64 === true) formatted.base64 = result.base64
|
|
82
|
+
return formatted
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function sftpOpenWrite(_server, remotePath, truncate = true) {
|
|
86
|
+
let opened
|
|
87
|
+
try {
|
|
88
|
+
opened = await openLocal(remotePath, truncate === false ? 'r+' : 'w', { noFollowFinal: true })
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (truncate === false && error && error.code === 'ENOENT') {
|
|
91
|
+
opened = await openLocal(remotePath, 'w', { noFollowFinal: true })
|
|
92
|
+
} else throw error
|
|
93
|
+
}
|
|
94
|
+
const id = `locw_${randomUUID()}`
|
|
95
|
+
sessions.set(id, { kind: 'write', path: opened.resolved, handle: opened.handle })
|
|
96
|
+
return { session: id, path: opened.resolved }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function sftpWriteChunk(sessionId, offset, buffer) {
|
|
100
|
+
const session = sessionFor(sessionId, 'write')
|
|
101
|
+
await session.handle.write(buffer, 0, buffer.length, offset)
|
|
102
|
+
return { session: sessionId, bytes: buffer.length, offset }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function sftpOpenRead(_server, remotePath) {
|
|
106
|
+
const opened = await openLocal(remotePath, 'r')
|
|
107
|
+
const info = await opened.handle.stat()
|
|
108
|
+
if (!info.isFile()) {
|
|
109
|
+
await opened.handle.close()
|
|
110
|
+
throw new Error('not a file')
|
|
111
|
+
}
|
|
112
|
+
const id = `locr_${randomUUID()}`
|
|
113
|
+
sessions.set(id, { kind: 'read', path: opened.resolved, handle: opened.handle, size: info.size })
|
|
114
|
+
return { session: id, path: opened.resolved, size: info.size }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function sftpReadChunk(sessionId, offset, length) {
|
|
118
|
+
const session = sessionFor(sessionId, 'read')
|
|
119
|
+
const buffer = Buffer.alloc(length)
|
|
120
|
+
const { bytesRead } = await session.handle.read(buffer, 0, length, offset)
|
|
121
|
+
return { session: sessionId, bytesRead, base64: buffer.subarray(0, bytesRead).toString('base64') }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function sftpCloseTransfer(sessionId) {
|
|
125
|
+
const session = sessions.get(sessionId)
|
|
126
|
+
// The server ref lets routes invalidate the local directory cache and log
|
|
127
|
+
// local activity under the right resource instead of "unknown" (B5/B8).
|
|
128
|
+
if (!session) return { session: sessionId, path: null, server: LOCAL_FILE_REF }
|
|
129
|
+
sessions.delete(sessionId)
|
|
130
|
+
let bytes
|
|
131
|
+
try {
|
|
132
|
+
if (session.kind === 'write') bytes = (await session.handle.stat()).size
|
|
133
|
+
} catch {
|
|
134
|
+
// A failed stat still closes the descriptor below.
|
|
135
|
+
}
|
|
136
|
+
await session.handle.close()
|
|
137
|
+
return { session: sessionId, path: session.path, bytes, server: LOCAL_FILE_REF }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sftpMkdir(_server, remotePath) {
|
|
141
|
+
return await localfs.mkdir(remotePath)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function sftpRemove(_server, remotePath) {
|
|
145
|
+
return await localfs.remove(remotePath)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function sftpRename(_server, fromPath, toPath) {
|
|
149
|
+
return await localfs.rename(fromPath, toPath)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function dispose() {
|
|
153
|
+
for (const session of [...sessions.values()]) {
|
|
154
|
+
try { await session.handle.close() } catch { /* already closed */ }
|
|
155
|
+
}
|
|
156
|
+
sessions.clear()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
server,
|
|
161
|
+
sftpHome,
|
|
162
|
+
sftpList,
|
|
163
|
+
sftpRead,
|
|
164
|
+
sftpOpenWrite,
|
|
165
|
+
sftpWriteChunk,
|
|
166
|
+
sftpOpenRead,
|
|
167
|
+
sftpReadChunk,
|
|
168
|
+
sftpCloseTransfer,
|
|
169
|
+
sftpMkdir,
|
|
170
|
+
sftpRemove,
|
|
171
|
+
sftpRename,
|
|
172
|
+
dispose,
|
|
173
|
+
}
|
|
174
|
+
}
|