C#でSSH.NETを使ってsftpサーバとやりとりをする

# 要約
C# SSH.NET の使用例です。
sshでsftpサーバに接続してファイルリストを取得します。

GitHubにサンプルコードを置いてあります。(https://github.com/unknown-ds/csharp_sftp)

### 環境
Windows 10
Visual Studio Express 2017 for Windows Desktop
Docker Desktop Community ver.2.0.0.3(31259)

### 参考
テスト用に以下のdocker-composeファイルを使わせて頂きました。ありがとうございます。:grinning:
[テスト用途のSFTP/FTPS/FTPサーバーを単独で起動するdocker-compose.ymlの例](https://qiita.com/miyanaga/items/37d4e1632e575c83a745)

# 実装
## フォルダ構成
プロジェクトフォルダ以下にsftpサーバ用`docker`フォルダを作っています。この下にアクセステスト用フォルダ`remote`と鍵が入っているフォルダ`ssh`があります。

```
└─[projectroot]
　　├─[docker]
　　│　├─[remote]
　　│　│　└─test.txt　　 // テスト用テキストファイル
　　│　└─[ssh]
　　│　　　└─id_rsa      // 秘密鍵
　　└─docker-compose.yml
```

## ライブラリ
NugetでSSH.NETをインストールします。

![lib.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/864725c1-0444-d951-2542-17530871f55a.png)


## テストクラス説明
コンストラクタで接続情報を用意し、`Execute()`でテストをしています。
基本的な流れとしては
1. Renci.SshNet.ConnectionInfoクラスを生成し、接続情報を用意する。
2. Renci.SshNet.SftpClientのインスタンスを生成して接続/切断を行う。
3. ファイルリストを取得する場合はListDirectoryで取得して処理。
4. ファイルのアップロード/ダウンロードはUploadFile/DownloadFileメソッドにストリームを渡して処理する。
となります。

```C#
// SFTP接続クラス
    public class CSftp
    {
        // 接続情報
        public ConnectionInfo ConnNfo { private set; get; }
        // 接続ホスト名
        public string HostName { private set; get; }
        // ポート
        public Int32 Port { private set; get; }
        // ユーザー名
        public string UserName { private set; get; }
        // パスワード
        public string Password { private set; get; }

        // コンストラクタ
        public CSftp()
        {
            HostName = "localhost";             // 接続先ホスト名
            Port = 10221;                       // ポート
            UserName = "sftp-with-rsa-key";     // ユーザー名
            Password = "1001";                  // パスワード

            string KeyFile = @"..\..\docker\ssh\id_rsa";     // 秘密鍵
            string PassPhrase = "";                          // パスフレーズ


            // パスワード認証
            var _PassAuth = new PasswordAuthenticationMethod(UserName, Password);

            // 秘密鍵認証
            var _PrivateKey = new PrivateKeyAuthenticationMethod(UserName, new PrivateKeyFile[]{
                        new PrivateKeyFile(KeyFile, PassPhrase)
                    });

            // 接続情報の生成
            ConnNfo = new ConnectionInfo(HostName, Port, UserName,
                new AuthenticationMethod[]{
                    _PassAuth,          // パスワード認証
                    _PrivateKey,        // 秘密鍵認証
                }
            );

        }

        // 実行
        public void Execute()
        {

            using (var sftp = new SftpClient(ConnNfo))
            {
                // 接続
                sftp.Connect();
                // 確認
                if (sftp.IsConnected)
                {
                    // 接続に成功
                    Console.WriteLine("Connection success!!\n");
                }
                else
                {
                    // 接続に失敗
                    Console.WriteLine("Connection failed!!\n");
                    return;
                }


                // ファイルリスト表示
                printFiles(sftp, "/remote");

                // ファイル内容表示
                printTxtFile(sftp, "/remote/test.txt");

                // ファイルアップロード
                uploadFile(sftp, "/remote", "../../Program.cs");


                // 切断
                sftp.Disconnect();
            }

        }

        // ファイル表示
        private void printFiles(
            SftpClient _sftp,   // sftpクライアント
            string _Path        // パス
            )
        {
            // 指定パスを調べる
            foreach (var file in _sftp.ListDirectory(_Path))
            {
                if (file.Name.StartsWith(".")) continue;

                if (file.IsDirectory)
                {
                    // ディレクトリなら再帰して調べる
                    printFiles(_sftp, file.FullName);
                }
                else
                {
                    // 表示
                    Console.WriteLine($"{file.FullName}\t\t{file.LastAccessTime}\t{file.LastWriteTime}");
                }
            }
        }

        // 指定テキストファイルの表示
        private void printTxtFile(
            SftpClient _sftp,       // sftpクライアント
            string _FilePath        // ファイルパス
            )
        {
            var _CurDir = Path.GetDirectoryName(_FilePath).Substring(1);
            var _FileName = Path.GetFileName(_FilePath);

            // カレントディレクトリ変更
            _sftp.ChangeDirectory(_CurDir);
            
            foreach (var file in _sftp.ListDirectory("./"))
            {
                if (file.IsDirectory) continue;
                if (file.Name != _FileName) continue;

                // 読み込み
                Int64 _Size = file.Length;
                var _Buf = new byte[_Size];
                using (var _St = new MemoryStream(_Buf, 0, (int)_Size))
                {
                    _sftp.DownloadFile(file.FullName, _St);
                }

                // SJIS変換
                string _str = Encoding.GetEncoding(932).GetString(_Buf);
                // 内容表示
                Console.WriteLine();
                Console.WriteLine($"------------------{file.Name}");
                Console.WriteLine($"{_str}");
                Console.WriteLine("------------------");
            }
        }

        // ファイルのアップロード
        private void uploadFile(
            SftpClient _sftp,       // sftpクライアント
            string  _UploadPath,    // アップロードパス
            string  _UploadFile     // アップロードファイル名
            )
        {
            // カレントディレクトリ変更
            _sftp.ChangeDirectory(_UploadPath);
            // アップロード先パス
            var _RemotePath = _UploadPath + "/" + Path.GetFileName(_UploadFile);

            using (var _uploadStream = File.OpenRead(_UploadFile))
            {
                _sftp.UploadFile(_uploadStream, _RemotePath, true);
            }
        }        
    }
```

## 注意点
日本語ファイル名を扱う際は、ConnectionInfoクラスのEncodingプロパティにエンコード情報を設定する。

## 実行例
`docker-compose up -d`でsftpサーバを立ち上げます。
テストコードを実行し接続に成功すると以下のような出力になります。
ファイルリストの表示と`test.txt`の内容表示、Program.csのアップロードが行われます。

```
Connection success!!

/remote/test.txt                2019/04/16 11:17:52     2019/04/16 15:29:18
/remote/Program.cs              2019/04/16 16:40:02     2019/04/16 16:40:02

------------------test.txt
テスト用のテキストです。
------------------
```

# SSH接続してコマンドで行う
シェルコマンドでやりとりする場合は、SshClientクラスインスタンスを生成してやり取りをすることができます。

```C#
using (var sshclient = new SshClient(ConnNfo))
{
    sshclient.Connect();

    using (var cmd = sshclient.CreateCommand("ls -lah"))
    {
        Console.WriteLine(cmd.Execute());
        Console.WriteLine($"ExitStatus:{cmd.ExitStatus}");
    }
        sshclient.Disconnect();
}
```

シェル実行できない場合は（サーバ側で制限されている場合は）
`This service allows sftp connections only.`
という風にレスポンスが返ってきます。（ExistStatus は 1 です。）