dsh-remote 0.8.72 → 0.8.73
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/LICENSE +21 -21
- package/README.en.md +389 -389
- package/README.md +462 -462
- package/cordis.patch.yml +9 -9
- package/lib/binding.js +172 -172
- package/lib/client.js +4022 -4022
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -141
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4550 -4540
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -893
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/package.json +100 -99
package/lib/sftp-surface.js
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
// dsh-remote — the promisified SFTP surface every backend presents.
|
|
2
|
-
//
|
|
3
|
-
// Both SSH transports (the native ssh2 pool in lib/pool.js and the
|
|
4
|
-
// system-OpenSSH pool in lib/openssh.js) must expose the SAME object to
|
|
5
|
-
// callers: lib/sync.js, lib/search.js, lib/routes-fs.js, lib/remote-fs.js and
|
|
6
|
-
// the rw_* tools all consume this exact shape. Keeping one implementation
|
|
7
|
-
// matters more than it looks: the timeout, dead-channel and range-read rules
|
|
8
|
-
// below encode failures that were expensive to find (issue #44), and a second
|
|
9
|
-
// hand-copied version would drift away from the fixed one.
|
|
10
|
-
//
|
|
11
|
-
// The two rules that are easy to get wrong:
|
|
12
|
-
// • A dead channel must fail FAST. ssh2's own EOF handling only rejects the
|
|
13
|
-
// requests pending at that moment; every later request is registered and
|
|
14
|
-
// never answered, so each waits out the full commandTimeoutMs. Over a tree
|
|
15
|
-
// walk that is N × 2 × timeout — days on a large directory.
|
|
16
|
-
// • `readPartial` exists so an oversized preview never downloads the whole
|
|
17
|
-
// file to show its head.
|
|
18
|
-
import { toSftpPath } from './paths.js'
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Wrap a live SFTP channel (ssh2's SFTP instance) in the promisified surface
|
|
22
|
-
* the plugin's callers expect.
|
|
23
|
-
*
|
|
24
|
-
* @param {object} sftp - an ssh2 SFTP channel (already negotiated/ready).
|
|
25
|
-
* @param {object} opts
|
|
26
|
-
* @param {number} opts.commandTimeoutMs - per-operation timeout.
|
|
27
|
-
* @returns {object} promisified sftp surface.
|
|
28
|
-
*/
|
|
29
|
-
export function buildSftpSurface(sftp, { commandTimeoutMs, onTimeout }) {
|
|
30
|
-
// A dead channel must fail fast (see the header). ssh2's SFTP instance and the
|
|
31
|
-
// system-OpenSSH engine both emit these, so one hook covers both backends.
|
|
32
|
-
let dead = false
|
|
33
|
-
const markDead = () => { dead = true }
|
|
34
|
-
if (typeof sftp.on === 'function') {
|
|
35
|
-
sftp.on('end', markDead)
|
|
36
|
-
sftp.on('close', markDead)
|
|
37
|
-
sftp.on('error', markDead)
|
|
38
|
-
}
|
|
39
|
-
const withTimeout = (fn) => (...args) =>
|
|
40
|
-
new Promise((resolve, reject) => {
|
|
41
|
-
if (dead) return reject(new Error('sftp channel closed'))
|
|
42
|
-
let timedOut = false
|
|
43
|
-
const timer = setTimeout(() => {
|
|
44
|
-
timedOut = true
|
|
45
|
-
markDead()
|
|
46
|
-
if (typeof onTimeout === 'function') { try { onTimeout() } catch { /* best effort */ } }
|
|
47
|
-
reject(new Error('sftp operation timed out'))
|
|
48
|
-
}, commandTimeoutMs)
|
|
49
|
-
const finish = (err, value) => {
|
|
50
|
-
clearTimeout(timer)
|
|
51
|
-
if (timedOut) return
|
|
52
|
-
err ? reject(err) : resolve(value)
|
|
53
|
-
}
|
|
54
|
-
try { fn(...args, finish) } catch (err) { clearTimeout(timer); if (!timedOut) reject(err) }
|
|
55
|
-
})
|
|
56
|
-
const P = (p) => toSftpPath(p)
|
|
57
|
-
return {
|
|
58
|
-
readdir: (dir) => withTimeout((d, cb) => sftp.readdir(d, cb))(P(dir)),
|
|
59
|
-
stat: (p) => withTimeout((d, cb) => sftp.stat(d, cb))(P(p)),
|
|
60
|
-
lstat: (p) => withTimeout((d, cb) => sftp.lstat(d, cb))(P(p)),
|
|
61
|
-
mkdir: (dir) => withTimeout((d, cb) => sftp.mkdir(d, cb))(P(dir)),
|
|
62
|
-
rmdir: (dir) => withTimeout((d, cb) => sftp.rmdir(d, cb))(P(dir)),
|
|
63
|
-
unlink: (p) => withTimeout((d, cb) => sftp.unlink(d, cb))(P(p)),
|
|
64
|
-
rename: (p, d) => withTimeout((a, b, cb) => sftp.rename(a, b, cb))(P(p), P(d)),
|
|
65
|
-
// POSIX rename(2) semantics: atomically REPLACE an existing destination.
|
|
66
|
-
// Plain SSH_FXP_RENAME fails when the target exists (OpenSSH's sftp-server
|
|
67
|
-
// enforces that even though it advertises posix-rename@openssh.com — ssh2
|
|
68
|
-
// only sends the extension when asked explicitly, verified against
|
|
69
|
-
// OpenSSH_9.6 in issue #65). Servers with the extension get one atomic
|
|
70
|
-
// request; servers without it get the move-aside dance, whose every hop
|
|
71
|
-
// targets a path that does not exist, which plain RENAME always permits.
|
|
72
|
-
// rw_edit's atomic write depends on this — without it every edit of an
|
|
73
|
-
// existing file fails with a bare "Failure".
|
|
74
|
-
renameOverwrite: (p, d) => withTimeout((a, b, cb) => {
|
|
75
|
-
const ext = sftp._extensions && sftp._extensions['posix-rename@openssh.com']
|
|
76
|
-
if (ext === '1' && typeof sftp.ext_openssh_rename === 'function') {
|
|
77
|
-
return sftp.ext_openssh_rename(a, b, cb)
|
|
78
|
-
}
|
|
79
|
-
const bak = `${b}.dsh-remote-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`
|
|
80
|
-
sftp.rename(b, bak, (e1) => {
|
|
81
|
-
if (e1) {
|
|
82
|
-
// Destination absent → a plain rename is already overwrite-safe.
|
|
83
|
-
if (/no such/i.test(String((e1 && e1.message) || e1))) return sftp.rename(a, b, cb)
|
|
84
|
-
return cb(e1)
|
|
85
|
-
}
|
|
86
|
-
sftp.rename(a, b, (e2) => {
|
|
87
|
-
if (e2) {
|
|
88
|
-
// Roll the original back into place before reporting the failure.
|
|
89
|
-
return sftp.rename(bak, b, () => cb(e2))
|
|
90
|
-
}
|
|
91
|
-
// Best effort: a leftover backup must not fail the operation.
|
|
92
|
-
sftp.unlink(bak, () => cb(null))
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
})(P(p), P(d)),
|
|
96
|
-
realpath: (p) => withTimeout((d, cb) => sftp.realpath(d, cb))(P(p)),
|
|
97
|
-
// Attribute write (chmod semantics live in setstat at the protocol level,
|
|
98
|
-
// so BOTH backends — ssh2's SFTPWrapper and its raw protocol SFTP — answer
|
|
99
|
-
// this one shape). Used by rw_edit to restore the original mode after an
|
|
100
|
-
// atomic rename replace (issue #65).
|
|
101
|
-
setstat: (p, attrs) => withTimeout((d, a, cb) => sftp.setstat(d, a, cb))(P(p), attrs),
|
|
102
|
-
readFile: (p) => withTimeout((d, cb) => sftp.readFile(d, cb))(P(p)),
|
|
103
|
-
writeFile: (p, data) => withTimeout((d, data2, cb) => sftp.writeFile(d, data2, cb))(P(p), data),
|
|
104
|
-
fastGet: (p, lp) => withTimeout((d, l, cb) => sftp.fastGet(d, l, cb))(P(p), lp),
|
|
105
|
-
fastPut: (lp, p) => withTimeout((l, d, cb) => sftp.fastPut(l, d, cb))(lp, P(p)),
|
|
106
|
-
// Range read for oversized previews: never downloads the whole file.
|
|
107
|
-
readPartial: (p, offset, length) => withTimeout((d, off, len, cb) => {
|
|
108
|
-
sftp.open(d, 'r', (e, handle) => {
|
|
109
|
-
if (e) return cb(e)
|
|
110
|
-
const buf = Buffer.alloc(len)
|
|
111
|
-
sftp.read(handle, buf, 0, len, off, (e2, bytesRead, data) => {
|
|
112
|
-
sftp.close(handle, () => {})
|
|
113
|
-
if (e2) return cb(e2)
|
|
114
|
-
cb(null, Buffer.isBuffer(data) ? data.subarray(0, bytesRead) : buf.subarray(0, bytesRead))
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
})(P(p), offset, length),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
1
|
+
// dsh-remote — the promisified SFTP surface every backend presents.
|
|
2
|
+
//
|
|
3
|
+
// Both SSH transports (the native ssh2 pool in lib/pool.js and the
|
|
4
|
+
// system-OpenSSH pool in lib/openssh.js) must expose the SAME object to
|
|
5
|
+
// callers: lib/sync.js, lib/search.js, lib/routes-fs.js, lib/remote-fs.js and
|
|
6
|
+
// the rw_* tools all consume this exact shape. Keeping one implementation
|
|
7
|
+
// matters more than it looks: the timeout, dead-channel and range-read rules
|
|
8
|
+
// below encode failures that were expensive to find (issue #44), and a second
|
|
9
|
+
// hand-copied version would drift away from the fixed one.
|
|
10
|
+
//
|
|
11
|
+
// The two rules that are easy to get wrong:
|
|
12
|
+
// • A dead channel must fail FAST. ssh2's own EOF handling only rejects the
|
|
13
|
+
// requests pending at that moment; every later request is registered and
|
|
14
|
+
// never answered, so each waits out the full commandTimeoutMs. Over a tree
|
|
15
|
+
// walk that is N × 2 × timeout — days on a large directory.
|
|
16
|
+
// • `readPartial` exists so an oversized preview never downloads the whole
|
|
17
|
+
// file to show its head.
|
|
18
|
+
import { toSftpPath } from './paths.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Wrap a live SFTP channel (ssh2's SFTP instance) in the promisified surface
|
|
22
|
+
* the plugin's callers expect.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} sftp - an ssh2 SFTP channel (already negotiated/ready).
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {number} opts.commandTimeoutMs - per-operation timeout.
|
|
27
|
+
* @returns {object} promisified sftp surface.
|
|
28
|
+
*/
|
|
29
|
+
export function buildSftpSurface(sftp, { commandTimeoutMs, onTimeout }) {
|
|
30
|
+
// A dead channel must fail fast (see the header). ssh2's SFTP instance and the
|
|
31
|
+
// system-OpenSSH engine both emit these, so one hook covers both backends.
|
|
32
|
+
let dead = false
|
|
33
|
+
const markDead = () => { dead = true }
|
|
34
|
+
if (typeof sftp.on === 'function') {
|
|
35
|
+
sftp.on('end', markDead)
|
|
36
|
+
sftp.on('close', markDead)
|
|
37
|
+
sftp.on('error', markDead)
|
|
38
|
+
}
|
|
39
|
+
const withTimeout = (fn) => (...args) =>
|
|
40
|
+
new Promise((resolve, reject) => {
|
|
41
|
+
if (dead) return reject(new Error('sftp channel closed'))
|
|
42
|
+
let timedOut = false
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true
|
|
45
|
+
markDead()
|
|
46
|
+
if (typeof onTimeout === 'function') { try { onTimeout() } catch { /* best effort */ } }
|
|
47
|
+
reject(new Error('sftp operation timed out'))
|
|
48
|
+
}, commandTimeoutMs)
|
|
49
|
+
const finish = (err, value) => {
|
|
50
|
+
clearTimeout(timer)
|
|
51
|
+
if (timedOut) return
|
|
52
|
+
err ? reject(err) : resolve(value)
|
|
53
|
+
}
|
|
54
|
+
try { fn(...args, finish) } catch (err) { clearTimeout(timer); if (!timedOut) reject(err) }
|
|
55
|
+
})
|
|
56
|
+
const P = (p) => toSftpPath(p)
|
|
57
|
+
return {
|
|
58
|
+
readdir: (dir) => withTimeout((d, cb) => sftp.readdir(d, cb))(P(dir)),
|
|
59
|
+
stat: (p) => withTimeout((d, cb) => sftp.stat(d, cb))(P(p)),
|
|
60
|
+
lstat: (p) => withTimeout((d, cb) => sftp.lstat(d, cb))(P(p)),
|
|
61
|
+
mkdir: (dir) => withTimeout((d, cb) => sftp.mkdir(d, cb))(P(dir)),
|
|
62
|
+
rmdir: (dir) => withTimeout((d, cb) => sftp.rmdir(d, cb))(P(dir)),
|
|
63
|
+
unlink: (p) => withTimeout((d, cb) => sftp.unlink(d, cb))(P(p)),
|
|
64
|
+
rename: (p, d) => withTimeout((a, b, cb) => sftp.rename(a, b, cb))(P(p), P(d)),
|
|
65
|
+
// POSIX rename(2) semantics: atomically REPLACE an existing destination.
|
|
66
|
+
// Plain SSH_FXP_RENAME fails when the target exists (OpenSSH's sftp-server
|
|
67
|
+
// enforces that even though it advertises posix-rename@openssh.com — ssh2
|
|
68
|
+
// only sends the extension when asked explicitly, verified against
|
|
69
|
+
// OpenSSH_9.6 in issue #65). Servers with the extension get one atomic
|
|
70
|
+
// request; servers without it get the move-aside dance, whose every hop
|
|
71
|
+
// targets a path that does not exist, which plain RENAME always permits.
|
|
72
|
+
// rw_edit's atomic write depends on this — without it every edit of an
|
|
73
|
+
// existing file fails with a bare "Failure".
|
|
74
|
+
renameOverwrite: (p, d) => withTimeout((a, b, cb) => {
|
|
75
|
+
const ext = sftp._extensions && sftp._extensions['posix-rename@openssh.com']
|
|
76
|
+
if (ext === '1' && typeof sftp.ext_openssh_rename === 'function') {
|
|
77
|
+
return sftp.ext_openssh_rename(a, b, cb)
|
|
78
|
+
}
|
|
79
|
+
const bak = `${b}.dsh-remote-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`
|
|
80
|
+
sftp.rename(b, bak, (e1) => {
|
|
81
|
+
if (e1) {
|
|
82
|
+
// Destination absent → a plain rename is already overwrite-safe.
|
|
83
|
+
if (/no such/i.test(String((e1 && e1.message) || e1))) return sftp.rename(a, b, cb)
|
|
84
|
+
return cb(e1)
|
|
85
|
+
}
|
|
86
|
+
sftp.rename(a, b, (e2) => {
|
|
87
|
+
if (e2) {
|
|
88
|
+
// Roll the original back into place before reporting the failure.
|
|
89
|
+
return sftp.rename(bak, b, () => cb(e2))
|
|
90
|
+
}
|
|
91
|
+
// Best effort: a leftover backup must not fail the operation.
|
|
92
|
+
sftp.unlink(bak, () => cb(null))
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})(P(p), P(d)),
|
|
96
|
+
realpath: (p) => withTimeout((d, cb) => sftp.realpath(d, cb))(P(p)),
|
|
97
|
+
// Attribute write (chmod semantics live in setstat at the protocol level,
|
|
98
|
+
// so BOTH backends — ssh2's SFTPWrapper and its raw protocol SFTP — answer
|
|
99
|
+
// this one shape). Used by rw_edit to restore the original mode after an
|
|
100
|
+
// atomic rename replace (issue #65).
|
|
101
|
+
setstat: (p, attrs) => withTimeout((d, a, cb) => sftp.setstat(d, a, cb))(P(p), attrs),
|
|
102
|
+
readFile: (p) => withTimeout((d, cb) => sftp.readFile(d, cb))(P(p)),
|
|
103
|
+
writeFile: (p, data) => withTimeout((d, data2, cb) => sftp.writeFile(d, data2, cb))(P(p), data),
|
|
104
|
+
fastGet: (p, lp) => withTimeout((d, l, cb) => sftp.fastGet(d, l, cb))(P(p), lp),
|
|
105
|
+
fastPut: (lp, p) => withTimeout((l, d, cb) => sftp.fastPut(l, d, cb))(lp, P(p)),
|
|
106
|
+
// Range read for oversized previews: never downloads the whole file.
|
|
107
|
+
readPartial: (p, offset, length) => withTimeout((d, off, len, cb) => {
|
|
108
|
+
sftp.open(d, 'r', (e, handle) => {
|
|
109
|
+
if (e) return cb(e)
|
|
110
|
+
const buf = Buffer.alloc(len)
|
|
111
|
+
sftp.read(handle, buf, 0, len, off, (e2, bytesRead, data) => {
|
|
112
|
+
sftp.close(handle, () => {})
|
|
113
|
+
if (e2) return cb(e2)
|
|
114
|
+
cb(null, Buffer.isBuffer(data) ? data.subarray(0, bytesRead) : buf.subarray(0, bytesRead))
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})(P(p), offset, length),
|
|
118
|
+
}
|
|
119
|
+
}
|