BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

通过SSH和Ruby实现自动化文件上载

| 作者 Matthew Bass 关注 0 他的粉丝 ,译者 李明(nasi) 关注 0 他的粉丝 发布于 2008年2月19日. 估计阅读时间: 22 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

计算机技术就像计算机本身一样飞速发展,这难道不有趣吗?一些做硬编码计算机编程的人们如今在早期Web迷人的时光里开始接触HTML和CGI。我就是其中之一。如果你也涉猎了叫做“Web设计”的伪码梦幻世界的话,你无疑会发现在这个时代绝大多数设计师会分属于两个阵营之一。第一阵营是采用一个所见即所得(WYSIWYG)的编辑器比如Dreamweaver来设计和发布网页。第二阵营使用诸如Emacs或Vim的文本编辑器来手工编码HTML,然后通过一个FTP客户端将完成的网页上传到一台Web服务器让世界看见并欣赏它,希望如此。第一阵营牺牲灵活性换取便利性,而第二阵营正好相反。没有哪一种方法是错的,但是也没有哪一种是完全正确的。

在我早年的Web设计中我属于第一阵营。最近我拥抱了使用写字板的命运并加入了那些“手工制作”的队伍。我喜欢这样做所带来的灵活性上的收益,但是也付出了损失易用性的代价。在本地安装Web服务器并直接编辑文件不但浪费时间而且不具移植性,所以我通常修改一个页面、转换到FTP客户端、上载文件、转换到浏览器并刷新以查看上载后的文件。这事做起来不算快,而且还需要反复做。这就是一个高喊着“把我自动化”的过程。所以在上个夏天,我启动了最喜欢的文本编辑器并决定开始动手。 

我试图写一个程序来做所有这些我需要手工来做的事情,但是更快更准确。我决定使用Ruby来编写自动化脚本。我试图使代码短小并具可维护性,因为稍后我还要加入一些其他特性。Ruby(作为动态类型的语言)让编写紧凑的代码变得简单,将困扰最小化。它虽是脚本语言,但是也面向对象。这使得我可以避免代码重复,从而比使用过程语言更为优雅。Ruby还拥有一个相当出色的开源SFTP库可供使用(Net::SFTP[0]),因此我不必自己动手编写。(SFTP是一个安全传输文件的网络协议。)

在这篇文章中,我将指导你逐步完成整个流程,创建这个程序自己的版本。文章中包含了完整的示例源代码,并带有逐行的代码分析。我邀请你参与进来并体验Ruby是如何能够轻易的将你的工作自动例行化的。

需求

我们的程序有一个基本需求:它要连接到一台远程的SFTP服务器并上载我们的文件。不过我们也希望它可以做到仅上载那些本地修改过的文件,并能够在判断文件上载的时候可以递归的检查其子目录。

脚本预想的流程应该是:

  1. 和远程服务器建立起一个SFTP连接
  2. 列出本地目录下所有的文件和子目录
  3. 比较本地文件的时间戳和远程目录下文件的时间戳,仅上载本地修改过的文件
  4. 递归访问所有本地子目录并重复步骤2,必要时创建远程子目录

显然步骤1到3可以通过Ruby的内建对象和Net::SFTP轻松搞定。步骤4很有趣。尽管Ruby的Dir类提供了一种递归访问子目录的方法,但是和我们需要的还有所不同。既然Ruby对语言的扩展非常简单,为什么我们不自己写一个方法?不仅仅是因为这样做会很有趣,而且我们还将学到扩展Ruby是多么的容易。

依赖

除了Ruby本身以外,我们依赖的仅仅是Net::SFTP和Net::SSH[1]这两个库了。幸运的是,这两个软件包都是可以通过Gem[2]来安装的。假定你已经在本地机器上安装了Ruby,并正确设置了Gem的路径,然后在命令提示符前输入:

gem install net-ssh --include-dependencies
gem install net-sftp --include-dependencies

编码开始!


现在我们准备开始写点儿代码。我们先来尝试连接远程服务器、列出一个给定本地目录的所有文件并关闭连接。我们将会使用Net::SSH和Net::SFTP接口以及Ruby的Dir类来实现。

1: require 'net/ssh'
2: require 'net/sftp'
3: Net::SSH.start('server', 'username', 'password') do |ssh|
4: ssh.sftp.connect do |sftp|
5: Dir.foreach('.') do |file|
6: puts file
7: end
8: end
9: end

让我们逐行看一下:

  1. 加载Net::SSH库。
  2. 加载Net::SFTP库,因为我们这两个库都会用到。
  3. 通过一个给定的用户名和密码建立一个SSH会话(诸如代理服务器之类的附加参数也可以在这里给定。更多信息参见API文档)。
  4. 打开对远程服务器的SFTP连接。
  5. 用Dir类列出当前工作目录的所有文件。
  6. 打印出每个文件名。
  7. 退出文件列表循环。
  8. -
  9. 关闭SFTP和SSH连接。

在我的系统上执行了脚本以后,产生的输出如下:

 .
..
cgi-bin
etc
logs
public_html
temp

回头看看我们最初的需求列表,我们只用了9行代码就完成了步骤1和步骤2。
下面来到步骤3,比较列出文件的时间戳和其所对应的远程时间戳,仅仅上载那些修改过的文件。(为了实现这个目的,我们定义一个文件是否修改过为本地时间戳大于等于远程服务器上的时间戳。)使用Ruby比较时间戳想当容易。实际上,比较本身只用一行代码就能够做到。

现在让我们看看脚本:

1: require 'net/ssh'
2: require 'net/sftp'
3: Net::SSH.start('server', 'username', 'password') do |ssh|
4: ssh.sftp.connect do |sftp|
5: Dir.foreach('.') do |file|
6: next if File.stat(file).directory?
7: begin
8: local_file_changed = File.stat(file).mtime > Time.at(sftp.stat(file).mtime)
9: rescue Net::SFTP::Operations::StatusException
10: not_uploaded = true
11: end
12: if not_uploaded or local_file_changed
13: puts "#{file} has changed and will be uploaded"
14: sftp.put_file(file, file)
15: end
16: end
17: end
18: end

让我们逐行来看一下:

1. - 2. 加载Net::SSH和Net::SFTP。
3. - 4. 建立SSH会话和SFTP连接。
5. 遍历当前工作目录下的所有文件。
6. 由于当前我们还不能递归访问目录,所以需要查看当前文件是否是一个目录。如果是的话则跳过到循环的下个迭代。
7. - 11. 由于远程文件可能不存在, 所以我们需要捕捉当我们试图去查看一个不存在的文件的时间戳的时候Net::SFTP抛出的异常。我们设置了两个标志,一个用于标记本地文件是否修改过并需要上载,另一个用于标记远程文件是否存在。
12. - 13. 如果本地文件没有被上载过或者比远程文件要新的话,打印一行文字说明文件将被上载。
14. 上载本地文件到远程服务器。
15. - 18. 结束if语句、文件循环、SFTP连接和SSH会话。

现在我们已经完成了三个需求。我们现在的脚本可以登录到远程服务器并上载所有本地系统中修改过的文件,但是脚本还只是仅仅能处理单一目录。它还不能尽如子目录去查看需要上载的文件。它也不能创建远程服务器上没有的目录。我们会在在我们宣布脚本完成以前解决这两个问题。

递归

让我们完善脚本,实现递归访问子目录并处理包含需要上载文件的目录在远程服务器不存在的情况:

1: require 'net/ssh'
2: require 'net/sftp'
3: require 'dir'
4:
5: local_path = 'C:\public_html'
6: remote_path = '/usr/jsmith/public_html'
7: file_perm = 0644
8: dir_perm = 0755
9:
10: puts 'Connecting to remote server'
11: Net::SSH.start('server', 'username', 'password') do |ssh|
12: ssh.sftp.connect do |sftp|
13: puts 'Checking for files which need updating'
14: Find.find(local_path) do |file|
15: next if File.stat(file).directory?
16: local_file = "#{dir}/#{file}"
17: remote_file = remote_path + local_file.sub(local_path, '')
18:
19: begin
20: remote_dir = File.dirname(remote_file)
21: sftp.stat(remote_dir)
22: rescue Net::SFTP::Operations::StatusException => e
23: raise unless e.code == 2
24: sftp.mkdir(remote_dir, :permissions => dir_perm)
25: end
26:
27: begin
28: rstat = sftp.stat(remote_file)
29: rescue Net::SFTP::Operations::StatusException => e
30: raise unless e.code == 2
31: sftp.put_file(local_file, remote_file)
32: sftp.setstat(remote_file, :permissions => file_perm)
33: next
34: end
35:
36: if File.stat(local_file).mtime > Time.at(rstat.mtime)
37: puts "Copying #{local_file} to #{remote_file}"
38: sftp.put_file(local_file, remote_file)
39: end
40: end
41: end
42:
43: puts ‘Disconnecting from remote server'
44: end
45: end
46:
47: puts 'File transfer complete'

哇呜!这个要比我们之前的版本长很多,但这主要是处理远程目录不存在的时候必须要做的异常检查造成的。这是Net::SFTP库的一个限制。当我们试图上载一个并不存在的远程目录的时候,put_file方法将会抛出一个令人讨厌的异常。put_file方法理应通过自动创建文件目录树中的缺失部分来处理这种情况。然而修改这个方法并不在本文讨论的范围以内,因此我把这个留给你作为练习。

让我们逐行来看一下新代码:

1. - 4. 加载Net::SSH和Net::SFTP。
5. - 6. 定义变量用于设定将会被比较和上载的本地和远程的目录。
7. - 8. 定义变量用于设定当远程服务器不存在的时候我们需要设定的文件和目录的默认权限。
9. - 13. 建立到远程服务器的SSH会话和SFTP连接。
14. 开始递归访问每个子目录。
15. 循环当前目录下的每个条目。
15. 如果当前条目是目录而非文件则跳过至下一条目。
16. 设置local_file变量为我们遍历到的本地文件,相对于我们的当前目录。
17. 设置remote_file变量为远程服务器的目标路径,并前缀remote_dir路径以便我们可以上传至正确的地址而不是用户的home目录。
19. - 26. 这是另一段惹人厌的代码,如果Net::SFTP能在文件处理方面再智能些这些代码就不会出现在这里。我们需要检查我们试图上载的远程目录是否已经存在。为了实现这个功能,我们调用sftp.stat(..)并传入待检查的目录名。如果stat抛出一个异常且属性码是2,则说明远程目录不存在。我们则需要创建它并设置正确的权限。
27. - 35. 更令人厌恶的代码,用于检查即将上载的远程文件是否存在。然后其实我们可以不必做这个检查就创建远程文件,因为当我们上载文件的时候会自动创建。但是我们需要做这个检查,因为当上传一个新文件时我们要给远程文件设置适当的权限。如果我们不这么做的话则会使用默认的UNIX权限,这样做可能会禁止我们稍后的文件上载。
36. - 40. 最后,如果到了这里,说明将要上载的远程目录和文件都存在。我们比较本地文件和远程文件的修改时间。如果本地文件更新,则上载它。
40. - 48. 所有我们开启的循环的结束,关闭和远程服务器的SFTP连接和SSH会话。

这是脚本执行的时候可能产生的输出:

Connecting to remote server
Checking for files which need updating
Copying D:/html/index.php to /home/public_html/index.php
Copying D:/html/media.php to /home/public_html/media.php
Copying D:/html/contact.php to /home/public_html/contact.php
Copying D:/html/images/go.gif to /home/public_html/images/go.gif
Copying D:/html/images/stop.gif to /home/public_html/images/stop.gif
Copying D:/html/include/menu.php to /home/public_html/include/menu.php
Disconnecting from remote server
File transfer complete

我们成功了!现在我们有了一个从任意深度的本地目录树上载文件到远程服务器上的快速、方便的方法。脚本相当的聪明,当目录不存在的时候会创建目录,而且只上载那些实际修改过的文件。

改进的方向

尽管我们开发的“骨架”级脚本已经相当有用,但是依然存在一些我们仅通过少许工作就可以实现的改进:

  • 处理本地删除的文件和目录。我们当前的脚本当本地的目录被删除的时候还不能从远程服务器移除。在移除远程文件之前可能需要提示用户以免重要的文件被误删除。
  • 为判断文件修改增加其他检查。比较时间戳的方法的确很好,但是为什么不同时比较一下文件的大小?当所连接的服务器并没有设置准确时间的时候这种检查是很有用的。
  • 记录上载文件的日志。我增加了打印到标准输出的语句,但是为什么不采用一个更加成熟的日志机制来记录文件上载的时间和地点?(如果你决定每天或每周运行脚本的话这将是很重要的。)
  • 采用Capistrano[4]重写脚本。Jamis Buck用于编写部署脚本的优秀框架是一个理想的选择,可作为一个永久解决方案在项目之间使用。

同时,算不得是改进的是,Net::SSH库支持公钥认证。运行PuTTY的Pageant应用程序(请看http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)并添加你的密钥,然后从我们的脚本的Net::SSH.start语句中移除你的密码。现在你可以在不将密码明文储存的情况下上载文件,也不必每次连接远程服务器的时候输入密码了。真好!

总结

随着时间的推移, 这个脚本已经为我节省了数十个小时,我不再需要手工使用FTP GUI,也不需要通过一个恼人的FTP命令行程序频繁的变换目录。我希望这个脚本对你的工作也起到同样的作用。如果是这样的话,我邀请你通过我的网站(www.matthewbass.com)联系我并让我知晓。我也同样有兴趣听到你对改进此脚本的建议,或者你完成了一个漂亮的重构可以精简一到两行代码。尽情的使用Ruby吧!

注脚

1. Net::SFTP是一个基于SFTP安全传送文件的Ruby API。它是Net::SSH的一部分。

2. Net::SSH是一个通过secure shell访问资源的Ruby API。http://rubyforge.org/projects/net-ssh/

3. Gem是Ruby的包管理器。http://www.rubygems.org

4. 用Ruby实现自动应用程序部署。http://manuals.rubyonrails.com/read/book/17

查看英文原文:Automating File Uploads with SSH and Ruby

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

实在很慢~ by @1sters! IceskYsl

这个思路我早就实现过,差不多的方式,也是用的SFTP,但是对于一个目录来说,逐一对比文件和目录,且都要等到服务器返回,其很慢的。

我记得当时我跑完一次大概用了2个小时,效率实在是~

本地打包再ssh 上传,然后服务器上解压缩 by WEN Shaohua

没必要每次比较每个文件的Timestamp。时间大多浪费在这里了。

Re: 实在很慢~ by Xie Fengbo

还是要加入版本控制以便增量修改, 本地svn diff | gzip >diff.gz, 上传diff 包, 到服务器上patch一下就OK了.

直接rsync不就行了? by Lee John

也是支持ssh的,而且还可以用自己的公钥

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

4 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT