Architect's Log

I'm a Cloud Architect. I'm highly motivated to reduce toils with driving DevOps.

WININET.DLLの外部メソッド呼び出しを使用してFTPのクライアントを実装する

どういうこと?

WININET.DLLの外部メソッド呼び出しを使用してFTPのクライアントを実装します。

どうして?

.NET Compact Frameworkでは、FtpWebRequestクラスやFtpWebResponseクラスが提供されていません。そのためFTPのクライアントを実装するにはWININET.DLLの外部メソッド呼び出しが必要です。

どうすれば?

ソースコードを記載します。InternetHandleクラスについては 前回 を参照してください。

using System;
using System.IO;
using System.Runtime.InteropServices;
using Shared.Win32;

namespace Shared.Net {

    /// <summary>
    /// FTP接続のクライアント機能を提供します。
    /// </summary>
    public class FtpClient : IDisposable {

        #region アンマネージAPI

        [DllImport("WININET", EntryPoint = "InternetOpen", SetLastError = true, CharSet = CharSet.Auto)]
        static extern IntPtr InternetOpen(
            string lpszAgent, 
            int dwAccessType, 
            string lpszProxyName, 
            string lpszProxyBypass, 
            int dwFlags);

        [DllImport("WININET", EntryPoint = "InternetConnect", SetLastError = true, CharSet = CharSet.Auto)]
        static extern IntPtr InternetConnect(
            IntPtr hInternet, 
            string lpszServerName, 
            int nServerPort, 
            string lpszUsername, 
            string lpszPassword, 
            int dwService, 
            int dwFlags, 
            int dwContext);

        [DllImport("WININET", EntryPoint = "FtpSetCurrentDirectory", SetLastError = true, CharSet = CharSet.Auto)]
        static extern bool FtpSetCurrentDirectory(IntPtr hConnect, string lpszDirectory);

        [DllImport("WININET", EntryPoint = "FtpGetFile", SetLastError = true, CharSet = CharSet.Auto)]
        static extern bool FtpGetFile(
            IntPtr hConnect, 
            string lpszRemoteFile, 
            string lpszNewFile, 
            bool fFailIfExists, 
            FileAttributes dwFlagsAndAttributes,
            int dwFlags, int dwContext);

        #endregion

        /// <summary>
        /// インターネットセッションハンドル
        /// </summary>
        private InternetHandle internetHandle;

        /// <summary>
        /// FTP接続を識別するハンドル
        /// </summary>
        private InternetHandle ftpConnectHandle;

        #region 終了処理

        /// <summary>
        /// ガベージ コレクションによってクリアされる前に、アンマネージ リソースを解放し、
        /// その他のクリーンアップ操作を実行します。 
        /// </summary>
        ~FtpClient() {
            Dispose();
        }

        /// <summary>
        /// 既にDisposeが呼ばれたかどうかを示します。
        /// </summary>
        /// <value>既にDisoseが呼ばれた場合はtrue。まだ呼ばれていない場合はfalse。</value>
        public bool Disposed {
            get { return this.disposed; }
        }
        private bool disposed = false;

        /// <summary>
        /// アンマネージ リソースの解放および終了処理を実行します。
        /// </summary>
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// アンマネージ リソースの解放および終了処理を実行します。
        /// </summary>
        /// <param name="disposing">
        /// マネージ リソースとアンマネージ リソースの両方を解放する場合は true。
        /// アンマネージ リソースだけを解放する場合は false。
        /// </param>
        private void Dispose(bool disposing) {
            if (disposed) return;

            // アンマネージリソースを解放する。
            if (this.ftpConnectHandle != null) this.ftpConnectHandle.Dispose();
            if (this.internetHandle != null) this.internetHandle.Dispose();

            disposed = true;
        }

        #endregion 終了処理

        #region 接続

        /// <summary>
        /// FTPサーバーに接続します。
        /// </summary>
        /// <param name="hostName">接続先のFTPサーバー。ホスト名またはIPアドレスで指定します。</param>
        /// <param name="userName">接続に使用するユーザー名</param>
        /// <param name="password">接続に使用するパスワード</param>
        public void ConnectServer(string hostName, string userName, string password) {
            this.ConnectServer(hostName, userName, password, "FtpClient");
        }

        /// <summary>
        /// エージェント名を指定してFTPサーバーに接続します。
        /// </summary>
        /// <param name="hostName">接続先のFTPサーバー。ホスト名またはIPアドレスで指定します。</param>
        /// <param name="userName">接続に使用するユーザー名</param>
        /// <param name="password">接続に使用するパスワード</param>
        /// <param name="agentName">接続に使用するエージェント名</param>
        public void ConnectServer(string hostName, string userName, string password, string agentName) {
            this.internetHandle = this.Initialize(agentName);
            this.ftpConnectHandle = this.InternalConnectServer(this.internetHandle, hostName, userName, password);
        }

        /// <summary>
        /// 初期化します。
        /// </summary>
        /// <param name="agentName">接続に使用するエージェント名</param>
        /// <returns></returns>
        private InternetHandle Initialize(string agentName) {
            const int INTERNET_OPEN_TYPE_DIRECT = 1;

            InternetHandle h = new InternetHandle(InternetOpen(agentName, INTERNET_OPEN_TYPE_DIRECT, null, null, 0));
            if (h.IsZero) {
                throw new Exception(string.Format("FTP 接続の初期化に失敗しました。{0}", LastError.Text));
            }

            return h; 
        }

        /// <summary>
        /// FTPサーバーに接続します。
        /// </summary>
        /// <param name="internetHandle"><see cref="Initialize"/>で取得したハンドル</param>
        /// <param name="hostName">接続先のFTPサーバー。ホスト名またはIPアドレスで指定します。</param>
        /// <param name="userName">接続に使用するユーザー名</param>
        /// <param name="password">接続に使用するパスワード</param>
        /// <returns>FTP接続を識別するハンドル</returns>
        /// <remarks>Passiveモードには対応していません。</remarks>
        private InternetHandle InternalConnectServer(
            InternetHandle internetHandle, 
            string hostName, 
            string userName, 
            string password) {
            const int INTERNET_SERVICE_FTP = 1;
            const int INTERNET_DEFAULT_FTP_PORT = 21;
//            Passiveモードの場合は、引数dwFlagsに設定する(未検証)
//            const int INTERNET_CONNECT_FLAG_PASSIVE = 0x08000000;

            // FTP サーバに接続する
            InternetHandle h = new InternetHandle(InternetConnect(
                                                        internetHandle.Pointer, 
                                                        hostName,
                                                        INTERNET_DEFAULT_FTP_PORT,
                                                        userName,
                                                        password,
                                                        INTERNET_SERVICE_FTP,
                                                        0,
                                                        0));

            if (h.IsZero) {
                string s = string.Format("FTP サーバーに接続できません。{0}", LastError.Text);
#if DEBUG
                s = string.Format("{0},HostName={1},UserName={2},Password={3}", s, hostName, userName, password);
#endif
                throw new Exception(s);
            }

            return h;
        }

        #endregion 接続

        /// <summary>
        /// 接続先のカレントディレクトリを変更します。
        /// </summary>
        /// <param name="directory">変更後のカレントディレクトリを示す相対URL</param>
        public void ChangeCurrectDirectory(string directory) {
            if (!FtpClient.FtpSetCurrentDirectory(this.ftpConnectHandle.Pointer, directory)) {
                throw new Exception(
                    string.Format("カレントディレクトリの変更に失敗しました。{0},directory={1}",
                    LastError.Text, directory));
            }
        }

        #region ファイル取得
        
        /// <summary>
        /// ファイルをダウンロードします。
        /// </summary>
        /// <param name="sourceFileUrl">
        /// ダウンロードするファイルをUrlで指定します。相対Urlでの指定も可能です。
        /// </param>
        /// <param name="destinationFileFullName">
        /// ダウンロードしたファイルの保存先をファイル名付きの絶対パスで指定します。
        /// </param>
        public void DownloadFile(string sourceFileUrl, string destinationFileFullName) {
            this.InternalDownloadFile(this.ftpConnectHandle, sourceFileUrl, destinationFileFullName);
        }

        /// <summary>
        /// ファイルダウンロードの内部実装です。
        /// </summary>
        /// <param name="ftpConnectHandle">
        /// <see cref="InternalConnectServer"/>で取得したハンドルを指定します。
        /// </param>
        /// <param name="sourceFileUrl">ダウンロードするファイルをUrlで指定します。</param>
        /// <param name="destinationFileFullName">
        /// ダウンロードしたファイルの保存先をファイル名付きの絶対パスで指定します。
        /// </param>
        private void InternalDownloadFile(
            InternetHandle ftpHandle, 
            string sourceFileUrl, 
            string destinationFileFullName) {
            if (!FtpGetFile(this.ftpConnectHandle.Pointer, sourceFileUrl, destinationFileFullName, false, 0, 0, 0)) {
                throw new Exception(string.Format(
                    "ダウンロードに失敗しました。{0},sourceFileUrl={1},destinationFileFullName={2}",
                    LastError.Text, sourceFileUrl, destinationFileFullName));
            }
        }

        /// <summary>
        /// 複数のファイルをダウンロードします。
        /// </summary>
        /// <param name="sourceFileUrl">
        /// ダウンロードするファイルをUrlで指定します。相対Urlでの指定も可能です。
        /// </param>
        /// <param name="destinationDirectory">
        /// ダウンロードしたファイルの保存先ディレクトリを絶対パスで指定します。
        /// </param>
        public void DownloadFiles(string[] sourceFileUrl, string destinationDirectory) {
            foreach (string file in sourceFileUrl) {
                this.DownloadFile(file, Path.Combine(destinationDirectory, file));
            }
        }

        #endregion ファイル取得

    }

}