Google App Engine/Java において、例えば画像ファイルをblobstoreへアップロードする方法はいくつかありますが、その代表的なものは、公式ドキュメント(こちら)にも記述されている以下の方法ではないかと思います。
// file Upload.java import java.io.IOException; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.blobstore.BlobstoreService; import com.google.appengine.api.blobstore.BlobstoreServiceFactory; public class Upload extends HttpServlet { private BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req); BlobKey blobKey = blobs.get("myFile"); if (blobKey == null) { res.sendRedirect("/"); } else { res.sendRedirect("/serve?blob-key=" + blobKey.getKeyString()); } } } // file Serve.java // 今回はアップロードするまでに起きる問題についてのお話なので、省略。
// file index.jsp <%@ page import="com.google.appengine.api.blobstore.BlobstoreServiceFactory" %> <%@ page import="com.google.appengine.api.blobstore.BlobstoreService" %> <% BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); %> <html> <head> <title>Upload Test</title> </head> <body> <form action="<%= blobstoreService.createUploadUrl("/upload") %>" method="post" enctype="multipart/form-data"> <input type="text" name="foo"> <input type="file" name="myFile"> <input type="submit" value="Submit"> </form> </body> </html>
// web.xml <?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>Upload</servlet-name> <servlet-class>Upload</servlet-class> </servlet> <servlet> <servlet-name>Serve</servlet-name> <servlet-class>Serve</servlet-class> </servlet> <servlet-mapping> <servlet-name>Upload</servlet-name> <url-pattern>/upload</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Serve</servlet-name> <url-pattern>/serve</url-pattern> </servlet-mapping> </web-app>
ここでは、アップロード後すぐにリダイレクトするという謎のサンプルになってますが、今回はアップロードまでのお話。
ここで問題なのが、
index.jspの「action="<%= blobstoreService.createUploadUrl("/upload") %>"」の部分。index.jspが開かれたときには既にこの部分というのは解釈され、「action="http://localhost:8888/_ah/upload/xxxxxxxxxx..."」といったURLに置き換わっています。 別にこのこと自体は正常なことなんですが、どうやらこうして生成されたURLには有効期間があるようなのです。それも10分か15分くらいという、さほど長くない時間。(厳密には調べていません、すみません。)この有効期間を過ぎて失効してしまったURLをそのまま用いてアップロードを行おうとする(formをsubmitする)と、「Bad Request - The upload URL has expired」が出て、アップロードに失敗してしまいます。
例えば、先日リリースさせて頂いた「Masterpiece」では、「自分の愛用品の写真を選択し、それに関しての説明を入力した後にアップロードする」という仕様になっています。これだと、説明文の入力に手間取ってしまうと、「登録画面を開いてから10分ないし15分経過後に登録ボタンを押下」、なんてことはざらにあるわけです(恥ずかしながら、リリースした後にこの事象に気が付きました・・・ご指摘頂いた方、ありがとうございます)。
対処方法。
少し調べてみたところ、こちらのStack Overflowでのやりとりが、参考になりそうでした。jQueryのgetを使う方法ですね。
まず、jspを以下のように変更します。すぐにsubmitさせるのではなく、javascriptを呼び出すようにします(formにnameを付け、actionのところは空白にしておきます)。
// file index.jsp <script type="text/javascript" src="/js/upload.js"></script> <html> <head> <title>Upload Test</title> </head> <body> <form action="" method="post" enctype="multipart/form-data" name="uploadForm"> <input type="text" name="foo"> <input type="file" name="myFile"> <input type="button" value="Submit" onclick="submitButtonClick();"> </form> </body> </html>
次が、ボタン押下時に呼び出されるjavascriptです。
//upload.js function submitButtonClick(){ $.get("/getBlobUrl", function(data){ document.uploadForm.action = data.url; document.uploadForm.submit(); }, 'json'); }
これにより、ボタンが押下されるとまず「/getBlobUrl」がrequestされ、その結果(data)がcallback関数に渡される、となります(別に受け取るデータ形式がjsonである必要はないと思います)。callback関数内では、受け取ったデータからURLを取得し、formのactionの値を書き換えた後に、formをsubmitしてます。つまり、「/getBlobUrl」で「<%= blobstoreService.createUploadUrl("/upload") %>」に相当することをしてやればいいわけです。
ここまで書けば大丈夫だと思いますが、続いては「/getBlobUrl」の部分。
// file GetBlobUrl.java (省略) import com.google.appengine.api.blobstore.BlobstoreService; import com.google.appengine.api.blobstore.BlobstoreServiceFactory; public class GetBlobUrl extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("application/json; charset=UTF-8"); BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); String url = blobstoreService.createUploadUrl("/upload"); Map<String, String> map = new HashMap<String, String>(); map.put("url", url); JSON.encode(map, this.response.getOutputStream()); } } }
web.xmlとかは省略で。 こうすることで、ボタンが押下されてからアップロード用のURLが生成されるわけで、URLの失効を考える必要もなくなります。たぶん。
ぼくはSlim3で作っているので、全てが上記の通りではありませんが、「GetBlobUrlController」を作り、その中で同等のことをすることで、今のところは問題なし男ちゃんです。 不備・不足等ありましたら、ご指摘頂けると幸いです。