コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

ServletとjQueryによる非同期ファイルアップロード

最近色々なところで見かける、ファイルをアップロードする際にプログレスバーを表示する方法を色々調べてみました。
以前調べていた時には、インラインフレームへのサブミットとCommons FileUploadを組み合わせて行う方法しか見つからなかったのですが、今はJavaScript側での仕組みが整って来ました。
ちなみにChromeFirefoxの最新版でしか確認していません。IEは動きません。
とりあえずコードです。アップロードを受信するサーブレットはいつもどおり、丸パクリの使いまわしです。この部分はサーブレットでなくてもサーバーサイドでファイルのアップロードを受信できればなんでも構いません。

package net.kuwalab.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet(name = "Upload", urlPatterns = { "/upload" })
@MultipartConfig(fileSizeThreshold = 5000000, maxFileSize = 700 * 1024 * 1024, location = "e:/temp")
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        request.setCharacterEncoding("utf-8");
        Part part = request.getPart("file");
        String name = getFilename(part);
        part.write(name);

        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");

        PrintWriter pw = response.getWriter();
        pw.println("{\"result\":\"ok\"}");
        pw.flush();
        pw.close();
    }

    private String getFilename(Part part) {
        for (String cd : part.getHeader("Content-Disposition").split(";")) {
            if (cd.trim().startsWith("filename")) {
                return cd.substring(cd.indexOf('=') + 1).trim()
                        .replace("\"", "");
            }
        }

        return null;
    }
}

重要なアップロードするHTMLです。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>トップページ</title>
 </head>
 <body>
  <input type="file" id="file"><input type="button" id="stop" value="停止">
  <div id="fileInfo"></div>
  <div id="ajaxStatus"></div>
  <div id="status"></div>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript">
(function() {
    var ajax; // 途中で停止する場合に必要。
    var ajaxSettings = {
        xhr: function() {
            var xhr = new window.XMLHttpRequest();
            // アップロードの進捗状況を取得
            xhr.upload.addEventListener("progress", function(event) {
                if (event.lengthComputable) {
                    $('#status').html(parseInt(parseInt((event.loaded / event.total) * 100)) + '%');
                }
            }, false);
            // 中止の際に何かしたい場合
            xhr.addEventListener("abort", function(event) {
            }, false);
            
            return xhr;
        },
        type: 'post',
        url: 'upload',
        processData: false,
        contentType: false, // 送信するデータをFormDataにする場合、こうしないといけない。
        cache: false,
        dataType: 'json',
        success: function(data) {
            if (data && data.result == 'ok') {
                $('#ajaxStatus').html('アップロード完了');
            } else {
                $('#ajaxStatus').html('アップロード失敗');
            }
            ajax = null;
        },
        error: function(xhr, textStatus, errorThrown) {
            alert('エラー\n' + textStatus);
            ajax = null;
        }
    };
    
    // ファイルが選択されたらアップロードする。
    $('#file').bind('change', onChangeFile);

    function onChangeFile() {
        // ファイルの情報を取得
        var uploadFile = $('#file')[0].files[0];
        $('#fileInfo').html(uploadFile.name + ' サイズ:' + uploadFile.size);
        var formData = new FormData();
        formData.append("file", uploadFile);
        ajaxSettings.data = formData;

        ajax = $.ajax(ajaxSettings);
    }

    $('#stop').click(function() {
        if (ajax) {
            // abortで送信を停止。
            ajax.abort();
        }
    });
}());
  </script>
 </body>
</html>

FormData

非同期でmultipartリクエストするためにはFormDataオブジェクトを利用する必要があります。送信するファイルデータは以下のようにFormDataオブジェクトにappendして設定します。

var formData = new FormData();
formData.append("file", uploadFile);
ajaxSettings.data = formData;

contentType

jQueryで$.ajaxするパラメータも非常に重要です。まず重要なのがcontentTypeパラメータで、falseにする必要があります。
ajaxのリクエストがFormDataの場合には適切なcontentType(multipart/form-data; ○○ boundary=△△)になるため、contentTypeをfalseにしないといけません。false以外の場合には設定を上書きしてしまい、送信がうまくいきません。このあたりは仕様に記載されています。

アップロードの進捗状況

送信状況を取得するためには、$.ajaxのパラメータのxhrを設定する必要があります。ここでは生のXMLHttpRequestオブジェクトを操作してxhr.uploadオブジェクトのprogressイベントを処理します。progressイベントは定期的に呼び出されますのでそのタイミングで進捗状況を処理します。

アップロードを途中で止める

最後にアップロードを途中で止める方法です。$.ajaxの戻り値でXMLHttpRequestが返されますので、そのメソッドabortを呼び出します。

うん。簡単。