ssh2-sftp-client使用介绍
最近在做WebShell,除了sz/rz命令方式实现上传下载之外,需要GUI方式实现基本的文件操作,比如拉取文件列表,上传下载。
调研后决定使用基于sftp/ssh实现的ssh2-sftp-client。
这里总结下使用中遇到的问题
服务禁用/开启设定
每个Linux机器默认都有SFTP服务,毕竟底层是SSH协议,算是标配,但用户可以通过在服务端设置来选择开启和关闭sftp服务的。
因此在实际开发中要考虑到服务不可用场景下的处理
# vi /etc/ssh/sshd_config
# override default of no subsystems
Subsystem sftp /usr/libexec/openssh/sftp-server
After
# override default of no subsystems
# Subsystem sftp /usr/libexec/openssh/sftp-server
service sshd restart
如果服务没有正常开启,则异常错误码是ERR_GENERIC_CLIENT
list
如果是想拉取文件列表,使用的是list方法,有几个问题需要注意
不支持
~
remotePath路径必须是相对或者绝对路径,不可以是路径别名,比如
~
async function main() { try { await sftp.connect(config); let fileList = await sftp.list(remotePath); console.log(fileList); await sftp.end(); } catch (e) { console.error(e); } }
文件type
返回结果中有type字段,表示文件类型,其中
d
是文件夹-
是文件l
是软链接
针对链接类型文件,想知道具体是文件还是文件夹,只能再去单独查询判断,比如使用stat
方法,返回值中isDirectory,isFile可以明确具体类别
size单位为
字节
- 这点与一般shell命令显示单位一致
- 文件夹类型也会返回size大小
rights权限
通过list接口可以拿到文件所属user/group,同时文件针对所属user/group/other权限,rights与longname信息等价,只是rights将权限信息结构化显示。owner字段表示归属userID,group表示归属groupID。通过信息还是无法直接判断当前用户对该文件是否有读写权限,为了判断需要SSH单独执行
id username
命令来获取当前用户userID和groupID举个例子如下,注意SSH2Client并不是sftpClient,这个需要使用ssh2下的client单独连接,sftp包不支持
const {Client: SSH2Client} = require('ssh2'); const ssh2Client = new SSH2Client(); await new Promise(resolve => ssh2Client.connect(config).on('ready', () => { return ssh2Client.exec(`id ${config.username}`, (err, stream) => { stream.on('data', (buf) => { const idRes = buf.toString(); console.log(idRes); // uid=0(root) gid=0(root) groups=0(root) const rights = idRes.match(/\d+/g); console.log({ uid: +rights[0], gid: +rights[1], }); return resolve(idRes); }); }) }));
当拿到权限信息后,结合list接口返回的信息即可判断。
const rights = idRes.match(/\d+/g); console.log({ uid: +rights[0], gid: +rights[1], });
filter
list方法list(path, filter) ==> Array[object]
第二个参数filter可以对列表文件进行过滤。比如隐藏文件不显示^[^.]
,但注意Windows文件名并没有规范要求一定是.开头文件,因此Windows下无法通过该方法过滤隐藏文件的显s。解决办法是比如通过执行命令获取文件隐藏属性进一步确定。
返回数据结构
{
"type": "-", // 文件类型
"name": "admin1.pem", // 文件名
"size": 418, // 文件体积,字节数
"modifyTime": 1651807713000,
"accessTime": 1651807715000,
"rights": {
"user": "rw",
"group": "r",
"other": "r"
},
"owner": 0,
"group": 0
}
put/fastPut
上传文件时可以使用这两个方法,区别主要在于是否支持流。比如我这里的设计是用户从网页发送的请求是走了node服务,node服务再发起SSH连接到目标机器,如果使用fastPut方法就一定存在临时文件,这样第一是速度慢,且实现上传进度的话,也并不准确了,毕竟是先分片上传到服务端,然后再分片上传到服务器,两个过程是割裂的。因此像我这里的场景,更适合使用put方法。
put支持的流写法确保是可读流即可
这里举个例子是网页发起的WS传输数据构造的可读流,这样前端发出的数据片,直接流化发送到目标机器,本身在node服务端不做任何其它处理
上传文件默认会有权限,这里建议设置为
0o644
,该值参考的FileZilla-SFTP上传后给予的权限设定。- 644即
rw-r--r--
,用户自己有读写权限,组/其他只有读权限
- 644即
get/fastGet
与上传类似,如果需要中转到前台,可以使用get方法,构建可写流。
限速
有时为了安全,需要对上传下载进行限速,做法如下。注意,限速需要流化
const Throttle = require('throttle');
const throttleStream = new Throttle(1024 * 1024); // 1MB
this.conn
.put(throttleStream.pipe(stream), data.path, {
writeStreamOptions: {
autoClose: false,
mode: 0o644,
},
readStreamOptions: {
autoClose: false,
},
pipeOptions: {
end: false,
},
})
取消
在上传下载过程中,突然想取消了,做法是直接断开SFTP连接,然后重新连接。
断点下载/上传
针对断点下载,远程文件可读流直接控制起始位置即可。
sftp.get(remoteFile, fileWtr, {
readStreamOptions: {
start: 10,
},
})
针对断点上传,可以走append方法即可。
downloadDir/uploadDir
上下载文件夹可以使用该方法,但缺点明显,比如无法流化,这样比如存在WebShell服务端中转,如果使用该方法,中转文件就必然存在,想要的上下载进度就无法准确。
因此可以使用get/put来解决,本质就是递归调用来实现文件夹中所有文件的读写操作,其中创建文件夹的话可以走mkdir解决。
SFTP协议介绍
https://www.ssh.com/academy/ssh/sftp
写在最后
done