Paradigm Shift Design

ISHITOYA Kentaro's blog.

C#とPHPでファイルアップロード

C#でのファイルアップロードには、いくつかの方法がある。

  1. WebClientを用いた方法
  2. WebRequestを用いた方法

ほかにもSocketを生で使うとかあるだろうけれど、代表的にはこの二つ。


でWebClientを用いた方法がとても簡単で、

Encoding enc = System.Text.Encoding.GetEncoding("utf-8");
WebClient client = new WebClient();
byte[] bytes = client.UploadFile(url, filename);
result = enc.GetString(bytes);

とかかけちゃうわけですが、WebClientには重大な欠点があって、それは「TimeOut値」を設定できない、という点。
そして、デフォルトは30秒なので、ちょっと大きなファイルをUp回線の細いサーバーにアップロードしようと思うと、すぐにWebExceptionが出る。

System.Net.WebException: 要求は中止されました: 要求がキャンセルされました

このメッセージ、「クライアントによって要求がタイムアウトされました」とか、出してくれればいいのだけれど、サーバーがタイムアウト要求を出しているのか、クライアントが出しているのかが分からず、解決に時間がかかってしまった。


結局のところ、タイムアウト時間を変更できない所為で、WebClientを使って20MB以上のファイルを転送することができないので、WebRequestを使うことにする。
それにWebClientだとプログレスを表示することもできないし。

public ErrorResponse UploadFile(String filename)
{
    String result = null;
    String filepath = Path.GetFullPath(filename);
    String url = this.endpoint + "?metamovics.movieFile";
    String boundary = System.Environment.TickCount.ToString();
    Encoding enc = System.Text.Encoding.GetEncoding("utf-8");

    try
    {
        //リクエストの設定
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        request.Timeout = 600 * 100 * IniFile.getInt("timeout", 20);
        request.Method = "POST";
        request.ContentType = "multipart/form-data; boundary=" + boundary;
        request.Credentials =
            new System.Net.NetworkCredential(this.endpointUsername, 
                                             this.endpointPassword);

        //POSTする内容の作成
        String post = "";
        post = "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"file\"; filename=\"" +
                filename + "\"\r\n" +
            "Content-Type: application/octet-stream\r\n" +
            "Content-Transfer-Encoding: binary\r\n\r\n";
        byte[] startData = enc.GetBytes(post);
        post = "\r\n--" + boundary + "\r\n";
        byte[] endData = enc.GetBytes(post);

        FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read);
        request.ContentLength = startData.Length + endData.Length + fs.Length;

        System.IO.Stream requestStream = request.GetRequestStream();
        requestStream.Write(startData, 0, startData.Length);
        byte[] buffer = new byte[4096];
        int readSize = 0;
        int i = 0;

        //リクエスト実行、ここでプログレス表示ができる。
        while (true)
        {
            readSize = fs.Read(buffer, 0, buffer.Length);
            if (readSize == 0)
                break;
            i += readSize;
            requestStream.Write(buffer, 0, readSize);
        }
        fs.Close();
        requestStream.Write(endData, 0, endData.Length);
        requestStream.Close();
        
        //レスポンスの取得
        System.Net.HttpWebResponse res =
            (System.Net.HttpWebResponse)request.GetResponse();
        System.IO.Stream resStream = res.GetResponseStream();
        System.IO.StreamReader sr =
            new System.IO.StreamReader(resStream, enc);
        result = sr.ReadToEnd();
        sr.Close();
        
        ErrorResponse error = 
            JavaScriptConvert.DeserializeObject<ErrorResponse>(result);
        if (error.Message != "ok")
        {
            throw new WebException(error.Message);
        }
        return error;
    }
    catch (Exception e)
    {
        Logger.Error(result);
        Logger.Error(e);
        Logger.Error(e.StackTrace);
        return new ErrorResponse("エラーが発生しました。");
    }
}

WebClientを用いた記述の何倍だと。


で、あまりサンプルがないので、サーバー側のサンプルコードも。

<?php
if(empty($_FILES)){
    return new CMMesse_ErrorBean("Data is Empty");
}

if($_FILES['file']['error'] != 0){
    return new CMMesse_ErrorBean("Upload failed with error " . 
                                    $_FILES['file']['error']);
}
$filename = $_FILES['file']['tmp_name'];
$md5 = md5_file($filename);
$upload = Ficus_Registry::search("uploader.dir");
$zipfile = $upload . "/" . $md5 . ".zip";
move_uploaded_file($filename, $zipfile);
return new CMMesse_ErrorBean("ok");
?>

ここで、$_FILES['file']というのは、WebClientでは「file」に固定されている。WebRequestでは自力でPOSTの内容を作成しているので、

"Content-Disposition: form-data; name=\"file\"; filename=\"" +
                filename + "\"\r\n"

のように指定することができます。


ほかにPHP側で気をつけなければならないのは、

file_uploads = On
upload_max_filesize = 20M
max_execution_time = 60 
max_input_time = 120 
memory_limit = 128M 
post_max_size = 8M

これらのphp.iniでの設定。詳しくは、PHP: ファイルアップロードの処理 - Manualを見てもらえればいいと思いますが、upload_max_filesizeとpost_max_sizeは両方変えないと、アップロードできるファイルのサイズが制限されたままになってしまうので注意。特にpost_max_sizeはよく忘れる。
で、忘れた場合、C#に残るエラーメッセージは、ヤッパリ

System.Net.WebException: 要求は中止されました: 要求がキャンセルされました

なので、原因を追求する場合には、クライアントの問題なのかサーバーの問題なのかをしっかり切り分けることが重要。

  1. クライアントのタイムアウト時間は適切か
  2. php.iniの設定は適切か
  3. $_FILES['file']['error']は0か

順にチェックすること。


以上、覚書でした。

関連: