1、使用ssh的好处

通过使用SSH,你可以把所有传输的数据进行加密,这样"中间人"这种攻击方式就不可能实现了,而且也能够防止DNS欺骗和IP欺骗。使用SSH,还有一个额外的好处就是传输的数据是经过压缩的,所以可以加快传输的速度。
所谓“中间人”的攻击方式, 就是“中间人”冒充真正的服务器接收你传给服务器的数据,然后再冒充你把数据传给真正的服务器。服务器和你之间的数据传送被“中间人”一转手做了手脚之后,就会出现很严重的问题。

2、上传和下载要解决的两个问题:

  • 上传(下载)的实现过程
  • 在上传(下载)的过程中,前端需要知道上传的进度,更新进度条;在上传(下载)结束后,前端需要知道上传已经结束,更新上传数量

3、上传(下载)过程:

1)上传前的操作

在上传之前,我们要获取这几个参数:

  • 被上传的文件在本地电脑的存储路径(绝对路径,包含文件名及后缀);
  • 要上传的目的地的路径(远程端的路径,包含文件名及后缀),存到哪里;
  • 一个用于上传的server对象,其中包括ip、port、username、password

2)调用服务端上传方法

在前端调用 Meteor.call,通知服务端相应的上传方法,并把上述参数传给服务端,去做上传

3)服务端执行上传(下载)

1、实现上传(下载)我们需要借助一个依赖包ssh2-sftp-client。
所以先下载它。

npm install ssh2-sftp-client

2、引入

let sftpClient = require('ssh2-sftp-client');

3、创建一个sftp对象

let sftp = new sftpClient();

4、上传的过程

sftp.connect({
  host: '127.0.0.1',
  port: '8080',
  username: 'username',
  password: '******'
}).then(() => {
    return sftp.fastPut(localPath,remotePath, {step: (total_transferred, chunk, total) => {
      //每小段上传完成后的回调
    }})
    })
    .then(data => {
  console.log( '上传结束',data);
  sftp.end();
}).catch(err => {
  console.log('上传出现错误',err);
  sftp.end();
});
  • 1)建立ssh连接,调用sftp.connect(server)方法,并传入server对象
  • 2)执行上传,sftp.connect()返回一个Promise对象,在第一个then中,调用sftp.fastPut执行上传文件的操作
    fastPut方法只是其中的一种上传方法,ssh包还提供有其它的上传方法。
fastPut(destFile, srcFile, options) ==> string

fastPut方法共有三个参数

  • destFile为远程文件的路径(尾)
  • srcFile为本地的路径(源)
  • options为配置对象
    options共有四个属性,其中step属性的值是一个方法。文件上传操作并不是一次性完成的,而是将一个大文件分成一小块一小块上传,所以每一小块上传完成后就调用一次这个方法。

在上传过程中会多次调用这个step方法。
step方法共有三个参数
- total_transferred,已经上传了多少k(包含本次上传的)
- chunk,本次上传了多少k
- total,此文件一共有多少k

存在的bug
上传过程中多次调用的step方法,其参数total_transferred,有时并不是按照递增执行的,
例如:按照正常情况,total_transferred的值应该是这样:…0.7,0.8,0.9,1
但有时它会是这样:…0.7,1,0.8,0.9,不靠谱。

  • 3)当全部上传完成后,就执行第二个then中的回调方法。
  • 回调的参数data是一个字符串,告诉你上传的结果,文件从哪上传到哪成功了。例如‘ E:\work\sg2148_MPanelCtl\img\Diagonal-45FoV-left.bmp was successfully uploaded to /opt/seeing/app/rgb_config/images/Diagonal-45FoV-left.bmp!’
    在里面调用end方法,结束ssh
  • 4)当catch到error时也end结束ssh。

5、下载的过程
下载的过程同上传的过程类似

let sftp = new sftpClient();
  sftp.connect(server).then(() => {
    return sftp.fastGet(srcFile, destFile,{step: (total_transferred, chunk, total,) => {
     //每小段下载完成后的回调
    }});
    }).then(() =>{
      console.log("下载结束");
      sftp.end();
    }).catch((err) => {
      console.log('下载出现错误',err);
      sftp.end();
  });

下载使用的是sftp.fastGet方法,并且其参数的顺序与上传不同
第一个参数是远程的路径(源)
第二个参数是本地的路径(尾)
总之无论上传还是下载方法,其第一个参数都是从哪里的地址,第二个参数是到哪里的地址。

4、前端如何知道每个文件上传的进度

上传一个文件是分多块上传的,每上传一小块就调用一次step函数。并且step函数的参数中含有已经上传完成的体积大小。

1、第一种方法,前端轮询数据库

在服务端每次调用step函数时,都将上传的进度更新到数据库;然后在前端向数据库轮询,当上传完成时resolve出去。
但我在项目中发现,服务端更新数据库有时会存在bug,数据更新不上,即使改成递归的更新数据库(即更新数据库不成功,就再次更新,直到更新成功为之),但还是偶尔有问题。
轮询函数

  • 查询一次数据库获取传输情况的数据
  • 没查到递归调用
  • 根据状态做不同操作
  • 如果还是正在传输,那么递归调用
async sftpSchedule(server, src, dest, successCb, failCb) {
    let sftpInfo = await CommonMethods.meteorCall(
      "querySftpInfo",
      server,
      src,
      dest
    );
    let status = sftpInfo.status;
    if (status === 1) {//传输完成
      successCb();
    } else if (status === -1) {//传输失败
      failCb();
    } else if (status === 0) {//传输正在进行
      let percentComplete =
      sftpInfo.total > 0 ? sftpInfo.total_transferred / sftpInfo.total : 0;
      let progress = parseInt(percentComplete * 100);//进度
      setTimeout(() => {
        this.sftpSchedule(server, src, dest, successCb, failCb);
      }, 500);
    } else {
      setTimeout(() => {
        this.sftpSchedule(server, src, dest, successCb, failCb);
      }, 500);
    }
  }

2、第二种,使用socket通知

服务端在step函数中向前端发消息,在传输完成时向前端发消息,在传输遇到错误时向前端发消息;前端根据这些消息做相应的处理。具体操作已在另一篇文章详述。