使用 GWT 和 AppEngine Blobstore 进行多文件上传?

5
我该如何创建一个现代化的、类似于Gmail的、支持多文件上传的 GWT 和 AppEngine Blobstore ?最常见的解决方案是 gwtupload,这是由 Manolo Carrasco 写的优秀 GWT 组件。然而,最新版本 0.6.6 无法与 Blobstore 兼容(至少我无法让它工作),并且它不支持多文件选择。最新的 0.6.7 快照中有一个支持多文件选择的补丁,但它仍然将它们作为一个巨大的 POST 请求发送(使用 HTML5 中的 "multiple" 属性来选择多个文件),并且整个文件组的进度都会显示出来。在 SO 上也有其他关于此问题的提问(例如 herehere),但答案通常使用 HTML5 的 "multiple" 属性,并将它们作为一个大的 POST 请求发送。虽然可以工作,但这不是我想要的。

Java还是Python?为什么不像@jos建议的那样只使用Java插件plupload,然后使用一个单一的文件上传处理程序来处理每个文件上传呢? - Jimmy Kane
Jimmy,我实际上是一步写的,既有问题也有答案供其他人查找和使用(不过要等两天才能接受答案)。这段代码是为GWT编写的,它是Java语言。 - joscarsson
抱歉,我在电脑前工作了8个小时后,我的眼睛很不舒服。 - Jimmy Kane
1个回答

14

Nick Johnson写了一些关于这个的博客文章。他使用了一个常用且被广泛认可的JavaScript上传组件,称为Plupload,并将文件上传到用Python编写的AppEngine应用程序中。 Plupload支持不同的后端(运行时)以支持多文件选择(HTML5,flash,Silverlight等),并处理上传进度和其他上传相关的客户端事件。

他的解决方案存在两个问题:(1)它是用Python编写的,(2)它是用JavaScript编写的。这就是gwt-plupload出现的地方。这是由Samuli Järvelä编写的Plupload的JSNI包装器,它使得在GWT环境中可以使用Plupload。但是,该项目已过时(自2010年以来没有提交),但我们可以将其用作启示。

因此,下面是构建多文件上传组件的逐步说明。这将全部放在一个项目中,但是(特别是JSNI包装器)可以提取到自己的.jar文件或库中以在其他项目中重用。源代码在这里的Bitbucket上可用。

该应用程序可在AppEngine上使用(不计费,因此不能保证它可用或有效):http://gwt-gaemultiupload-example.appspot.com/

屏幕截图

Example application screenshot Example application screenshot Example application screenshot

第1步- Servlets

Blobstore的工作方式如下:

  1. 客户端请求blobstore以获取可用于上传文件的URL。
  2. 客户端将文件POST到收到的URL。
  3. 当整个POST接收完毕时,blobstore会将客户端重定向到成功URL(在创建上传URL时指定)。

为了支持这一点,我们需要两个servlet。一个用于生成文件上传的URL(请注意,每个文件上传都需要唯一的URL),另一个用于接收已完成的上传。两者都很简单。以下是URL生成器servlet,它将URL以纯文本形式写入HTTP响应中。

public class BlobstoreUrlGeneratorServlet extends HttpServlet {     
    private static BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("Content-Type", "text/plain");
        resp.getWriter().write(blobstore.createUploadUrl("/uploadfinished"));
    }
}

接着,用于接收成功上传的servlet,将blobkey打印到System.out

public class BlobstoreUploadFinishedServlet extends HttpServlet {
    private static BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Map<String, List<BlobKey>> blobs = blobstore.getUploads(req);
        List<BlobKey> blobKeyList = blobs.get("file");

        if (blobKeyList.size() == 0)
            return;

        BlobKey blobKey = blobKeyList.get(0);

        System.out.println("File with blobkey " + blobKey.getKeyString() + " was saved in blobstore.");
    }
}

我们还需要在web.xml中进行注册。

<servlet>
    <servlet-name>urlGeneratorServlet</servlet-name>
    <servlet-class>gaemultiupload.server.BlobstoreUrlGeneratorServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>urlGeneratorServlet</servlet-name>
    <url-pattern>/generateblobstoreurl</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>uploadFinishedServlet</servlet-name>
    <servlet-class>gaemultiupload.server.BlobstoreUploadFinishedServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>uploadFinishedServlet</servlet-name>
    <url-pattern>/uploadfinished</url-pattern>
</servlet-mapping>
如果现在运行该应用并访问http://127.0.0.1:8888/generateblobstoreurl,我们会看到类似于下面的内容。
http://<computername>:8888/_ah/upload/ahpnd3QtZ2FlbXVsdGl1cGxvYWQtZXhhbXBsZXIbCxIVX19CbG9iVXBsb2FkU2Vzc2lvbl9fGAEM 

如果我们将文件发布到该URL,它将被保存在blobstore中。但请注意,本地开发Web服务器的默认URL是http://127.0.0.1:8888/,而blobstore生成的URL是http://<computername>:8888/。这将导致后续出现问题,因为出于安全原因,Plupload无法将文件POST到另一个域。这仅会在本地开发服务器上发生问题,发布的应用程序将只有一个URL。通过在Eclipse中编辑运行配置,将-bindAddress <computername>添加到参数中来解决此问题。这将导致本地开发服务器将Web应用程序托管在http://<computername>:8888/上。在更改后,您可能需要允许GWT浏览器插件加载应用程序中的<computername>

到目前为止,我们已经拥有了所需的servlet。

第二步 - Plupload

下载Plupload(我使用了最新版本1.5.4),解压缩并将js文件夹复制到我们的GWT应用程序中的war目录下。对于本示例,我们不会使用jquery.plupload.queuejquery.ui.plupload,因为我们将创建自己的GUI。我们还需要jQuery,我从Google API下载了它。

接下来,我们需要在应用程序中包含JavaScripts,因此编辑index.html并将以下内容添加到<head>标记中。

<script type="text/javascript" language="javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" language="javascript" src="js/plupload.full.js"></script>

现在我们已经在应用程序中包含了Plupload。接下来,我们需要将其包装以便与GWT一起使用。这就是使用gwt-plupload的地方。我没有使用该项目的jar文件,而是复制了源文件以便进行修改。包装器的主要对象是Plupload类,它由PluploadBuilder构建。还有一个接口PluploadListener,可以实现以接收客户端事件。

步骤3 - 将其组合在一起

现在我们需要实际在GWT应用程序中使用Plupload。我将以下内容添加到Index.ui.xml UIBinder中:

<g:Button text="Browse" ui:field="btnBrowse" />
<g:Button text="Start Upload" ui:field="btnStart" /><br />
<br />
<h:CellTable width="600px" ui:field="tblFiles" />

这里有一个用于浏览文件的按钮,一个用于开始上传的按钮,以及一个我们将用于显示上传状态的 CellTable。在 Index.java 中,我们初始化 Plupload 如下:

btnBrowse.getElement().setId("btn-browse");
PluploadBuilder builder = new PluploadBuilder();
builder.runtime("html5");
builder.useQueryString(false);
builder.multipart(true);
builder.browseButton(btnBrowse.getElement().getId());
builder.listener(this);
plupload = builder.create();
plupload.init();
runtime属性告诉Plupload要使用哪些后端(我只测试过HTML5,但其他后端也应该可行)。Blobstore需要启用multipart。我们还需要为浏览按钮设置一个ID,然后告诉Plupload使用该ID。单击此按钮将弹出Plupload的文件选择对话框。最后,我们作为监听器(实现PluploadListener)添加自己,并创建并初始化Plupload。

要显示准备上传的文件,我们只需在来自UploadListener的事件中向tblFilesDataProvider列表数据提供程序添加数据即可。

@Override
public void onFilesAdded(Plupload p, List<File> files) {
    tblFilesDataProvider.getList().addAll(files);
}
为了显示进度,我们只需在收到进度通知时更新列表:

要显示进度,我们只需要在收到进度更改通知时更新列表:


@Override
public void onFileUploadProgress(Plupload p, File file) {
    tblFilesDataProvider.refresh();
}
我们还为btnStart实现了一个点击处理程序,只是告诉Plupload开始上传。
@UiHandler("btnStart")
void btnStart_Click(ClickEvent event) {
    plupload.start();
}
现在可以选择文件,它们将被添加到待上传列表中,我们可以开始上传。唯一剩下的部分是实际使用我们之前实现的servlet。目前,Plupload不知道将上传POST到哪个URL,因此我们需要告诉它。这就是我对gwt-plupload源代码所做的更改(除了小型错误修复); 我添加了一个名为fetchNewUploadUrl的函数到Plupload中。它会执行一个Ajax GET请求来获取我们之前定义的servlet的上传URL。它同步执行此操作(稍后会清楚原因)。当请求返回时,它将此URL设置为Plupload的POST URL。
private native void fetchNewUploadUrl(Plupload pl) /*-{
    $wnd.$.ajax({
        url: '/generateblobstoreurl',
        async: false,
        success: function(data) {
          pl.settings.url = data;
        },
    });
}-*/;

public void fetchNewUploadUrl() {
    fetchNewUploadUrl(this);
}

Plupload会将每个文件都放在自己的POST请求中发送。这意味着我们需要在每次上传开始之前为其提供一个新的URL。幸运的是,在PluploadListener中有一个事件可以实现此功能。而以下事件处理程序接收上传URL之前必须是同步的原因是:否则,上传将在我们收到上传URL之前就已经开始了(pl.fetchNewUploadUrl()将立即返回)。

@Override
public void onBeforeUpload(Plupload pl, File cast) {
    pl.fetchNewUploadUrl();
}

完成了!现在您拥有了GWT HTML5多文件上传功能,可以将文件放置在AppEngine Blobstore中!

传递参数

如果您想要其他参数(例如:上传的文件所属实体ID),我添加了一个示例来演示如何添加参数。在Plupload上有一个名为setExtraValue()的方法可供使用,我已经实现了它:

public native void setExtraValue(String value) /*-{
    this.settings.multipart_params = {extravalue: value}
}-*/;

可以将额外的值作为multipart_params传递。这是一个映射,因此功能可以扩展以允许许多任意键值对。该值可以在onBeforeUpload()事件处理程序中设置

@Override
public void onBeforeUpload(Plupload pl, File cast) {
    pl.setExtraValue(System.currentTimeMillis() + " is unique.");
    pl.fetchNewUploadUrl();
}

并在接收完成上传的Servlet中检索

String value = req.getParameter("extravalue");

这个示例项目包含相关样本代码。

最后的话

我并不是一名专业的GWT开发人员。在经过数小时的挫败和找不到我想要的功能之后,这就是我想出来的东西。在我让它工作之后,我认为我应该写一个完整的示例,因为我使用/遵循的每个组件/博客文章等都留下了一些部分。我不认为这是最佳实践代码。评论、改进和建议都受欢迎!


哇!发这个帖子的人真是传奇。非常感谢!! - slugmandrew
对我来说是非常棒的答案!真的很棒。+1 - Cataclysm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接