BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル SSHとRubyでファイル・アップロードを自動化

SSHとRubyでファイル・アップロードを自動化

ブックマーク

コンピュータスキルがコンピュータそのもののように、急速に進化する様は奇妙な感じがしないか?現在、ハードコアなコンピュータ・プログラミングをしている我々のような人間の中には、初期の頃の心を奪われるようなWebでHTMLやCGIからスタートした人もいるだろう。私もそうした人間の1人だ。「Webデザイン」という擬似コーディングの素晴らしい世界にちょっと手を出してみたことがある読者なら、デザイナーが最近、2種類のグループに分類される、と、絶対に気づいているだろう。最初のグループは、DreamweaverのようなWYSIWYGのエディタを使い、Webページのデザインと発行を行っている。2番目のグループはEmacsやVimといったテキストエディタを使ってHTMLを手ずからコードし、FTPクライアントを用いて完成したページをWebサーバーにアップロードして世界に公開し、できれば世界に高く評価されたい、と思っている。最初のグループは利便性のために柔軟性を犠牲にしており、2番目のグループは、柔軟性のために利便性を犠牲にしている。どちらも正しくないが、完全に間違っているわけでもない。


私の場合、Webデザイン初期の頃は最初のグループに入っていた。最近ではメモ帳を使う運命を受け入れ、「手ずから行う」のを好む人々に仲間入りした。そうすることで可能になった付加的な柔軟性を享受したが、使いやすさへの代償は小さくなかった。ローカルにWebサーバーをインストールし、直接ファイルをエディットするのには時間がかかったし、ポートがうまくいかず、気がつくとページを変え、FTPクライアントに切り替え、ファイルをアップロードし、ブラウザに切り替え、アップデートしたページをビューしているのであった。すばやくできるものではないし、しばらく時間が経つと、ひどく時代遅れになっていた。「自動化してよ!」と声高に叫んでいるも同然のプロセスだった。だから昨夏、お気に入りのテキストエディタを起動し、自動化することに決めた。


私が欲していたのは、手動でやっていた全てを、ずっと高速かつ正確に行うプログラムだった。Rubyを使って自動化スクリプトを書くことに決めた。後で追加機能を付け足すだろうから、コードは短く、メンテナンスしやすいものにしたかった。(動的型付けの)Rubyを使えば、最低限の手間で拡張できるコンパクトなコードを簡単に書ける。Rubyはスクリプティング言語でありながら、オブジェクト指向である。それにより、手順言語を使うよりも手際よく、コードの重複を回避できた。Rubyではまた、それなりのオープンソースSFTPライブラリが利用可能であるため(Net::SFTP[0])、自分で書く必要に迫られることはなかった。(SFTPはファイルをセキュアに転送するネットワークプロトコルである。)


この記事では、読者が自分バージョンのプログラムを作成できるよう、ステップごとに案内する。ソースコード例の完全版も入れ、コードが何をしているのかを1行ずつ分析する。仕事日にはいつも決まって行う作業を、Rubyを使えばとても簡単に自動化できることをお見せしよう。

要件

このプログラムには基本的な要件が1つ存在する。それは、リモートのSFTPサーバーに接続し、ファイルをアップロードできなければならないということである。しかし、ローカルで変更が加えられたファイルのみをアップロードし、アップロードするファイルをチェックしながら自動的にサブディレクトリに再帰するようにしたいのである。

このスクリプトのフローは以下のとおりである。

  1. リモートサーバーとSFTPコネクションを確立する。
  2. ローカルディレクトリ内で全ファイルとサブディレクトリをリストする。
  3. ローカルディレクトリにあるファイルのタイムスタンプとリモートディレクトリにあるファイルのタイムスタンプを比較し、ローカルで変更したファイルのみアップロードする。
  4. 任意のローカルサブディレクトリに戻り、ステップ2から繰り返し、必要に応じてリモートのサブディレクトリを作成する。

すぐに分かるのは、ステップ1から3は、Rubyの組み込みオブジェクトとNet::SFTPで簡単に処理できることだ。ステップ4は非常に興味深い。RubyのDirクラスではサブディレクトリに再帰する方法を提供しているが、期待するほど理解しやすいものではない。Rubyでは言語の拡張が簡単なため、我々独自のメソッドを書けばよいのではなかろうか。とても楽しめるだけでなく、Rubyの拡張しやすさも同時に学べる。

依存性

Ruby自体はさておき、我々の依存性はNet::SFTPとNet::SSH[1]ライブラリだけである。幸運にも、両方ともGems[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行ずつ細かくみてみよう。

  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

元の要件リストを見ると、ステップ1と2は9行のコードで終えることができた。

ステップ3に進み、リストアップされたファイルのタイムスタンプをリモートのタイムスタンプと比較し、変更されたファイルのみアップロードする(この目的では、ローカルのタイムスタンプがリモートサーバーにあるタイムスタンプと同じか大きい場合に、ファイルが変更されたと定義する)。Rubyを使ってタイムスタンプを比較するのは非常に簡単である。実際、比較そのものはたった1行のコードで済む。

ここでスクリプトを見てみよう。

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行ずつ見ていこう。

1. - 2. Net::SSHとNet::SFTPを要求、
3. - 4. SSHセッションとSFTP接続を確立。
5. 現在の作業ディレクトリ内でファイルをループ処理。
6. ディレクトリへの再帰はまだ処理できないため、現在のファイルが実はディレクトリかチェックする。もしそうなら、ループの次の繰り返しにスキップする。
7. - 11. リモートのファイルがまだ存在していない可能性があるため、タイムスタンプの決定を試みる際にNet::SFTPが投げる例外をキャッチする必要がある。フラグを2つ設定する。1つは、ローカルファイルが変更されて、アップロードが必要と指し示すフラグで、もう1つはリモートファイルがまだ存在しないと示すフラグである。
12.- 13. ローカルファイルがまだアップロードされていないか、リモートファイルより新しい場合、そのファイルをアップロード中と示す行をプリントする。
14. ローカルファイルをリモートサーバーにアップロードする。
15.- 18. ifステートメント、ファイルのループ、SFTP接続、SSHセッションを閉じる。

これで要件3を完了した。リモートサーバーにログインし、ローカルシステム上で変更された全ファイルをアップロードするスクリプトができたが、このスクリプトでは単一のディレクトリにしか働かない。サブディレクトリにナビゲートしていき、アップロードする追加ファイルを探してはくれない。また、リモートサーバー上にはないディレクトリを作成するという処理もできない。この2つの状況も対象に入れない限り、スクリプトが完成したとは言えない。

再帰

サブディレクトリに降りていくことと、転送中のファイルが含まれるディレクトリがリモートサーバー上には存在しないかもしれないという状況を処理することで、スクリプトを完成させよう。

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行ずつ詳しく見ていこう。

1. - 4. Net::SSHとNet::SFTPを要求。
5. - 6. 比較とアップロードを行うローカルディレクトリとリモートディレクトリ向けの変数を定義。
7. - 8. リモートサーバー上にまだ存在しないファイルとディレクトリに割当が必要なデフォルトファイル、およびディレクトリ許可用の変数を定義。
9. - 13. リモートサーバーにSSHセッションとSFTP接続を確立。
14. 各サブディレクトリへの下降を開始。
15. カレントディレクトリ内の各アイテムでループオーバー。
14.カレントアイテムがファイルではなくディレクトリの場合は、次の繰り返しへスキップ。
15.現在位置のカレントディレクトリと相対するように、ループオーバーしているローカルファイルへのパスと等しくなるようlocal_file変数を設定。
16.リモートサーバー上のディレクトリ/ファイルと等しくなるようにremote_file変数を設定し、接頭部にはremote_dir値をつけて、アップロード中のファイルを、ユーザーの単なるホームディレクトリではなく、正しい位置に置くようにする。
17.- 26. Net::SFTPがもう少し頭の良いファイル処理をしてくれれば不要になるであろう、意地の悪いコードがここにも少し登場する。アップロード先のリモートディレクトリがすでに存在することをチェックする必要がある。そうするには、sftp.stat(..)を呼び出し、チェックするディレクトリの名称を渡す。プロパティコード=2の例外をstatが投げたら、リモートディレクトリが存在しないということなので、正しい許可を割り当ててリモートディレクトリを作成する。
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プログラムを介してディレクトリに常に変更を加えたりするために、何十時間も費やしていたであろう。このスクリプトが私の場合と同様に、読者の役にも立ってくれればと願っている。もし役立ったら、私のWebサイト(www.matthewbass.com)から私に知らせてくれないか。また、強化についての追加提案がある場合や、コードを1、2行排除できる素晴らしいリファクタリングを見つけたら、連絡してくれると嬉しい。楽しいRubyライフを!

脚注

1. SFTPはRuby APIで、SFTP上でファイルをセキュアに転送するためのものである。Net::SSHの一部をなす。

2. Net::SSHはRuby APIで、セキュアシェルを介してリソースにアクセスするためのものである。http://rubyforge.org/projects/net-ssh/

3. GemはRubyのパッケージマネージャである。http://www.rubygems.org

4. Rubyを使ったアプリケーションデプロイメントの自動化。http://manuals.rubyonrails.com/read/book/17

原文はこちらです:http://www.infoq.com/articles/ruby-file-upload-ssh-intro

この記事に星をつける

おすすめ度
スタイル

BT