だるろぐ

明日できることは、今日しない。

WebMatrix でファイルのアップロード(2)

まずはお詫びを。

あと、最初から複数ファイルのアップデートに対応できるように記述している。

WebMatrix でファイルのアップロード - だるろぐWebMatrix でファイルのアップロード - だるろぐ

あれはウソだ。

いや、複数ファイルのアップロード自体はできるのだけれど、結果を返す処理が単体ファイルを前提としていたので最後のファイルの結果しか得られない。正しくは、

var model = new List<dynamic>();

とでもして、複数のファイルの結果を格納できるようにすべきだった。

さてはて。

このように Upload.cshtml はめでたく複数ファイルのアップロードに対応できたし、 Ajax には Json で応答するようにもなった。ならば、ドラッグ&ドロップで複数ファイルのアップロードもしてみたいよね。というわけでやってみた。

f:id:daruyanagi:20120820014053p:plain

初期状態。

f:id:daruyanagi:20120820014058p:plain

ファイルをドラッグ&ドロップ。これにはもちろん、 Drag & Drop の API を利用する。

画像のプレビューは HTML5 の File Reader API を利用して実装してある。JavaScript は見よう見まねで書いてみたけれどなかなか難しい……けれど、 cshtml ならば自動補完機能の恩恵をうけることができるのでまだマシ。jQuery だと

$.event.props.push('dataTransfer');

という呪文を唱えないと動かないのを知らなくて、かなり悩んだ。

f:id:daruyanagi:20120820014101p:plain

Ajax でファイルを Upload.cshtml へ送ると、画面遷移なしで結果が表示される。これには FormData という仕組みを利用した。

まぁ、ここで JavaScript の話をする気はないので本題に入るけど、これ。

f:id:daruyanagi:20120820014842p:plain

デカいファイルをアップロードしようとすると発生するのだけれど、この例外をトラップするのが面倒……。無理やり頑張ってトラップしてみたのだけれど、 try 文がやたらネストするし、 Request に少しでもアクセスしようものなら発生するので IsAjax が取れずに少し困っている*1

# Upload.cs

@using System.IO

@functions 
{
    private enum Result
    {
        Success = 0,
        Error   = -1,
    };

    private const string OUTPUT_DIR = "~/Files/";
    private Dictionary<string, string> AllowedFileType = new Dictionary<string, string>();

    private void VerifyOutputDir(string path)
    {
        if (!Directory.Exists(path))
        {
            throw new DirectoryNotFoundException(
                string.Format("{0} does not exists.", path)
            );
        }
    }

    private void VerifyPostedFile(HttpPostedFileBase file)
    {
        if (file.ContentLength == 0)
        {
            throw new ArgumentException("File is null.");
        }
                
        if (!AllowedFileType.ContainsKey(file.ContentType))
        {
            throw new ArgumentException(
                string.Format(
                    "{0} is not allowed format",
                    file.ContentType
                )
            );
        }
    }

    private string GetOutputFilename(HttpPostedFileBase file)
    {
        return string.Format(
            "{0:yyyyMMdd-HHmmssfff}.{1}",
            DateTime.Now,
            AllowedFileType[file.ContentType].ToLower()
        );
    }
}

@{
    var model = new List<dynamic>();

    AllowedFileType.Add("image/jpeg", "jpg");
    AllowedFileType.Add("image/png" , "png");
    AllowedFileType.Add("image/gif" , "gif");
    
    var dir = Server.MapPath(OUTPUT_DIR);

    try
    {
        if (IsPost) // <-- ここでもエラーが発生しうるので try…catcgh せざるを得ない
        {
            foreach (var file in Request.Files.ToEnumerable())
            {
                try
                {
                    VerifyOutputDir(dir);
                    VerifyPostedFile(file);
                
                    var src = Path.GetFileName(file.FileName);
                    var dst = GetOutputFilename(file);
                
                    file.SaveAs(Path.Combine(dir, dst));

                    model.Add( new {
                        Result = Result.Success,
                        Message = string.Format("{0} is uploaded as {1}.", src, dst),
                        Link = VirtualPathUtility.ToAbsolute(Path.Combine(OUTPUT_DIR, dst)),
                    });
                }
                catch (Exception e)
                {
                    model.Add(new
                    {
                        Result = Result.Error,
                        Message = e.Message,
                        Link = string.Empty,
                    });
                }
            }
        }
        else
        {
            model.Add (new {
                Result = Result.Error,
                Message = "You can use only POST method.",
                Link = string.Empty,
            });
        }
        
        if (IsAjax)
        {
            Response.ContentType = "application/json";
            Response.Write(Json.Encode(model));
            return;
        }
    }
    catch (Exception e)
    {
        model.Add( new {
            Result = Result.Error,
            Message = e.Message,
            Link = string.Empty,
        });

        Response.ContentType = "application/json";
        Response.Write(Json.Encode(model));
        return; // <-- IsAjax が使えない(エラーが起こりうる)ので出力先を分岐できない
    }
}

<h1>Uploading Result</h1>

@foreach (var item in model)
{
    <h2>@item.Result</h2>
    <p>@item.Message</p>
    if (!string.IsNullOrEmpty(item.Link))
    {
        <p><img src="@item.Link" /></p>
    }
}

<p>&raquo; Back to <a href="~/">home</a></p>

とりあえず動くけど、ブラウザーからアクセスした時にリクエストサイズ超過のエラーが出てもそれをユーザーに知らせることができない(Json で返せるのみ)。やっぱりこういうのは Json のみを返す API として作って、ビューは完全に分離したほうがいいなと思った。

*1:例外自体は IIS のリクエストのサイズ制限を緩和すれば抑制できるはず