gulpとWinSCPでローカルとFTPサーバのディレクトリ同期（＋セキュリティも担保）

ICSさんの「[JenkinsでCI環境構築チュートリアル ～GitHubからWebサーバーへのデプロイ～](http://ics-web.jp/lab/archives/3283)」の記事が素敵過ぎて導入してみたかったんですけど、Jenkinsのためにサーバ借りるのがちょっと厳しいので断念。

上記の記事でも書かれていますが、**WinSCPはコマンドラインから実行できる**ので、これをベースに上手いことgulpから叩いてFTPサーバに同期させてみる方法を試してみました。

<br>

## やりたいこと
* WinSCPのディレクトリ同期が良さげなので、コレを使ってローカルからFTPサーバへディレクトリ同期をしたい
* パスフレーズ付きの秘密鍵を使った公開鍵暗号方式でセキュリティも担保したい
* gulpのファイル監視を起点にして、良い感じのタイミングで実行したい

<br>

## 動作環境
* ローカル: Windows 7 64bit
* Webサーバー: heteml

今回はWindows限定の話ですが、Macだとrsyncとkeychainコマンドを使って同じような事が恐らくできるんじゃないでしょうか。

Webサーバは今回は[heteml](http://heteml.jp/)を使っています。
あらかじめ公開鍵をサーバに置いておけば、公開鍵暗号方式でSFTP接続できるようになります。

<br>

## 必要なソフトのインストール

### WinSCP
今回の肝。
ディレクトリ同期の機能があり、コマンドラインでも実行できます。

[WinSCP](http://winscp.net/eng/docs/lang:jp)をインストールしたらコマンド実行を楽にするために環境変数にパスを通しておきます。
デフォルトでは以下になるかと思います。

```
C:\Program Files (x86)\WinSCP
```

<br>

### PuTTY (PuTTYgen + Pageant)
PuTTYgenは公開鍵と秘密鍵(Putty形式)を作成するのに使います。
Pageantは秘密鍵のパスフレーズを入力するダイアログを表示したり、2回目以降の入力を省略するのに使います。

[TortoiseGit](https://code.google.com/p/tortoisegit/)をインストールするともれなく付いてくるので、自分はそれを使っています。
入ってない人は別途[PuTTY](http://www.putty.org/)をインストールしてもらえればと思います。

環境変数のパスはTortoiseGitが勝手に通してくれてたようですが、通ってなかったら通しておいてください。
Pageantをコマンドラインで実行する時に使います。

<br>

### Node.js + gulp + gulp-run
gulp周りの環境構築手順は省略しますが、gulpからコマンドラインを利用するのにgulp-runが必要になります。
あらかじめnpmでインストールしておいてください。

<br>

## 秘密鍵と公開鍵の作成

「[PuTTYgenの利用](http://www.tempest.jp/security/keygen.html)」を参考に、公開鍵と秘密鍵のペアを作成します。

それぞれの鍵の名前はとりあえず下記の通りで説明を進めます。
保存先は説明を簡略化するために`C:\.ssh\`としました。

| 鍵の種類 | 保存ファイルパス |
|:---|:---|
| 公開鍵  | c:\\.ssh\\hoge.pub |
| 秘密鍵  | c:\\.ssh\\hoge.ppk |

秘密鍵にはパスフレーズを設定しておきます。


<br>

## 公開鍵をhetemlサーバーに設置

### hetemlでSSHを利用できるようにする
hetemlでSSHが使えるようになってなければ、hetemlのサポートサイト内の「[SSH のご利用方法](http://heteml.jp/support/manual/ssh/)」を参考に設定しておいてください。

<br>

### FTPサーバに公開鍵を設置

先ほどの「[PuTTYgenの利用](http://www.tempest.jp/security/keygen.html)」でも同様の事が書かれていますが、「[hetemlのSSHの認証にRSA鍵を利用する方法 とおまけ](http://d.hatena.ne.jp/love0hate/20111028/1319794243)」も参考になります。


要はホームディレクトリから見て、`./.ssh/authorized_keys`というパスとファイル名に公開鍵の内容が入っていれば良いので、`.ssh`ディレクトリを作って`hoge.pub`を入れてから`authorized_keys`にファイル名を変えればOKです。

ちなみに、公開鍵を複数設定したい場合は、公開鍵`xxx.pub`の中身を一行ずつ`authorized_keys`に追加しておけば大丈夫です。

<br>

### 公開鍵のパーミッション
hetemlのサポートサイト「[SSH の 公開鍵のパーミッション設定について知りたい](http://heteml.jp/support/faq/1441.html)」にパーミッションの推奨設定が載ってるので、その通りに設定します。

| ファイル/ディレクトリ | パーミッション |
|:---|:---|
| ./ssh | 700 |
| ./ssh/authorized_keys | 600 |
<br>

## WinSCPで試しに接続してみる

まずはWinSCPを普通に起動してこれまでの設定が上手くいってるか確認しておくと良いと思います。
「[WinSCPを使って公開鍵認証でログインする方法（PuTTYgenで鍵生成）](http://www.koikikukan.com/archives/2013/01/15-000300.php)」を参考に接続確認をしておきます。

<br>

## コマンドラインでWinSCPを実行する

コマンドラインの使い方は「[コマンドラインオプション - WinSCP Wiki](http://sourceforge.jp/projects/winscp/wiki/commandline)」がとても参考になります。

コマンドラインだけでも色々できますが、

```bat
$ winscp.com /script=winscp.txt
```

上記のように`/scriptオプション`を使うと、**細かい処理はテキストファイルに記述できる**ので、今回はこの方法を使いました。

最終的にはgulpから呼び出すので、gulpの作業ディレクトリにテキストファイル`winscp.txt`を入れておきます。

<br>

## WinSCPのコマンドでSFTP接続

テキストファイルに記述するWinSCPのコマンドは「[コマンドリファレンス - WinSCP Wiki](http://sourceforge.jp/projects/winscp/wiki/script_commands)」を見ればやれる事が一通り書いてあります。

以降はこのリファレンスを元に進めていきます。

<br>

### とりあえず接続してみるが・・・

まずは接続を行うために以下のコマンドを`winscp.txt`に記述して、コマンドラインから実行します。

```bat:winscp.txt
# エラーが発生しても処理は継続
option batch continue
# ファイルの上書きなどの確認ダイアログを表示しないようにする
option confirm off

# SFTPで接続
open sftp://[ユーザー名]@sshXX.heteml.jp:2222 -privatekey="C:\.ssh\hoge.ppk"

# リモートのカレントディレクトリのファイル一覧を表示
ls
```
最初の`option`コマンドはgulpから実行した時にエラーなどでgulpごと止まってしまうのを防ぐために入れておきます。

[ユーザー名]の部分はhetemlで契約しているユーザー名が入り、接続先のサブドメインXXの部分はhetemlから指定の数字が入ります。

また、通常のパスワード認証は使わないのでパスワードは省略し、代わりに秘密鍵のファイルパスを`-privatekey`オプションで渡します。

<br>

### ・・・上手くいかない

残念ながら上記コマンドを実行してもパスフレーズを入力するタイミングが無いので上手く接続できません。

プロンプトで聞かれたりするのかなぁと思いましたが、ダメっぽいです。
また、WinSCPにパスフレーズを渡すようなオプションも無いようです。
（あったとしてもパスフレーズをファイルに記述しておくのは避けたいですが・・・）

<br>

### 救いの手Pageantでパスフレーズを入力

ここで、Pageantの出番です。
「[Pageant でパスフレーズの入力を省く - WebOS Goodies](http://webos-goodies.jp/archives/50723253.html)」がとても参考になりました。

コマンドラインから

```bat
$ pageant c:\.ssh\hoge.ppk
```

のように秘密鍵のパスを渡して実行すると、パスフレーズの入力を促すダイアログが表示されます。

**ここでパスフレーズを入力するとPageantに秘密鍵とパスフレーズの組み合わせが登録されて次回からの入力を省略してくれます。**
**しかもこれはWinSCPから実行する場合にも有効になります。**

めちゃくちゃ便利ですね。

<br>

pageantコマンドは単独で何度も実行すると「既に立ち上がってるよ」とアラートを出してきます。
しかし、秘密鍵のパスを渡しておけば、

* Pageantが起動してなければ起動する
* パスフレーズが入力されていなければダイアログを表示
* 既にパスフレーズが入力されていれば何もしない

といった具合で、とても都合の良い処理をしてくれます。

<br>

## WinSCPのコマンドでディレクトリ同期

パスフレーズの問題が解決したので、いよいよディレクトリ同期を行います。

```bat:winscp.txt
# エラーが発生しても処理は継続
option batch continue
# ファイルの上書きなどの確認ダイアログを表示しないようにする
option confirm off

# SFTP接続
open sftp://[ユーザー名]@sshXX.heteml.jp:2222 -privatekey="C:\.ssh\hoge.ppk"

# リモートのカレントディレクトリを同期させたいディレクトリに変更
cd "/home/sites/heteml/...[省略].../hoge/"

# ローカルのカレントディレクトリを同期させたいディレクトリに変更
lcd "c:\project\hoge\release\"

# ローカルからリモートへディレクトリ同期
synchronize remote -delete

```

処理の流れとしては、SFTP接続をした後にローカルとリモートのカレントディレクトリを同期させたいディレクトリに変更します。

その後に、`synchronize`コマンドでディレクトリ同期を実行します。
パスの指定がなければ、ローカル/リモート共にカレントディレクトリが使用されます。

<br>

`synchronize`コマンドに渡した`remote`オプションはローカルからリモートへのみ同期するという意味です。
また、`-delete`オプションを指定すると同期元（ローカル）に無いファイルは同期先（リモート）から削除してくれます。

詳しいオプションの内容は先程のリファレンスを参考にしてみてください。

<br>

この方法では、更新日時を元にしてリモートよりローカルの方が新しいファイルだけを転送します。
なので、何らかのファイルが更新された時にgulpから逐一実行していても、**更新されたファイルだけを転送する**ので効率の良い同期を実現してくれます。

<br>

## gulpに組み込んで実行する

ラストはgulpに組み込んで実行してみます。

とりあえず、次の環境を前提に組んでみます。

* `c:\project\hoge\` をプロジェクトフォルダとし、
* `c:\project\hoge\gulp\` をgulpの作業ディレクトリとする<br>
→　WinSCPコマンドのテキストファイルもこの中に入れておく
* `c:\project\hoge\release\` を同期したいローカルのディレクトリとする

<br>

gulpの記述は以下の通りです。

gulp-runの使い方は上記を見れば一目瞭然かと思います。
コマンドを文字列で渡してから`exec`メソッドで実行するだけです。

```js:gulpfile.js
var gulp = require('gulp');
var run = require('gulp-run');

gulp.task('watch', function (callback) {

  // 最初にPageantを起動してパスフレーズを入力
  // \\は2つ並べないとダメ
  run('pageant C:\\.ssh\\hoge.ppk').exec();

  // releaseフォルダ内の全ファイルを対象
  var glob = "../release/**/*";
  
  // 監視を始める
  gulp.watch(glob, function (e) {
    // ファイルが更新されたらWinSCPのコマンドを実行
    run('winscp.com /script=winscp.txt').exec();
  });

});
```

まず、ファイル監視を始めると同時にPageantを起動して、パスフレーズがまだ入力されてなかったらダイアログから入力します。

続いて、`release`フォルダ以下の全ファイルを対象に監視し、何かしらの変更があればWinSCPのコマンドを叩いてディレクトリ同期を実行します。

gulp.watchはファイルの追加/更新だけでなく削除にも反応するので、`synchronize`コマンドに`-delete`オプションを入れておけばファイルの削除時にもちゃんと同期してくれます。

<br>

ちなみに、「[gulp.watchの注意点メモ](http://qiita.com/yuichiroharai/items/3eb9aef9dea7b5eb5090)」でも書いたのですが、この場合は`release`フォルダの中に何かしらのファイル/フォルダが入ってる状態で監視をスタートしないと監視が上手くいかないので注意してください。

<br>

## あとがき
gulpとWinSCPでディレクトリ同期を行う手順は以上です。

とりあえずはこの形でやりたい事は実現できましたが、ちょっと何とかしたいなぁと思っているのが、**同期させるたびにSFTP認証と接続を行っている**所です。
[sessionコマンド](http://sourceforge.jp/projects/winscp/wiki/script_commands#session)辺りを使えばもしかしたら認証/接続したセッションを引き継げるかも？
という気もしてますが、この辺りは今後試してみようと思います。