Monday, November 14, 2011

Serving Files in GAE

This post will describe how to serve a file in a Java application hosted in Google App Engine. The sample application provided is based on Vaadin, but the idea can be implemented in any other web framework.


First of all let's see it working.
  1. Click on the application link (gaedownloaddemo.appspot.com)
  2. Use your Google account to log in (don't freak out, you provide your credentials to Google not my application!)
  3. Press the "Download" button to download a simple text file.
If you want to try it out for yourself, the source code is here and can be checked out using this command:
svn co http://tinywebgears-samples.googlecode.com/svn/trunk/gaedownload gaedownload

Now that I have shown you I've done it, let's see what was involved doing so. Before going any further make sure you have a look at Blobstore which might be suitable for you according to your circumstances. There might be other solutions around (like this one), since a couple of months ago when I developed this. You have definitely searched around before coming here.

This can be very easy in a normal Java web application, but you can't use OutputStream in GAE because it is not Serializable. So, starting from a simple Vaadin application for GAE (in this post), I made the following changes:

First of all, I added a new entity called MyFile which has a Blob field to store the file contents:

package com.tinywebgears.gaedownload.core.model;

// Import statements here

@PersistenceCapable(identityType = IdentityType.DATASTORE)
public class MyFile implements Serializable {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private String name;

@Persistent
private String username;

@Persistent
Blob file;

@Persistent
private Key ownerKey;

public MyFile() {
}

public MyFile(String name, String username, Blob file) {
this.name = name;
this.username = username;
this.file = file;
}

// Some getters and setters here
}

After that I created a method in the service class (UserService) to create a MyFile and return a special URL as an ExternalResource (We'll see how it is used later):

private void createFile(MyFile file) throws DataPersistenceException
{
file.setOwnerKey(getUserKey());
fileRepo.persist(file);
}

@Override
public Resource serveTextFile(String filename, byte[] text) throws ServiceException
{
try
{
// Create a blob in the database. This is the only
Blob imageBlob = new Blob(text);
MyFile myFile = new MyFile(filename, getUsername(), imageBlob);
createFile(myFile);
Long id = myFile.getKey();
return new ExternalResource(TextFileServlet.SERVLET_PATH + "?" + TextFileServlet.PARAM_BLOB_ID + "=" + id);
}
catch (DataPersistenceException e)
{
throw new ServiceException("An error occurred when downloading the file " + filename + ".", e);
}
}

Then, I served this ExternalResource in a new window somewhere in the Vaadin code:

Resource resource = userServices.serveTextFile(fileName, fileContents.getBytes());
// Redirecting to servlet in order to download the file
mainWindow.open(resource, "_blank");

Now, the application needs to know how to serve this URL. For this purpose I add a new Servlet:

<servlet>
<servlet-name>FileServerServlet</servlet-name>
<servlet-class>com.tinywebgears.gaedownload.servlet.TextFileServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FileServerServlet</servlet-name>
<url-pattern>/gaedownload/servefile</url-pattern>
</servlet-mapping>

The Servlet's code looks like this:

package com.tinywebgears.gaedownload.servlet;

// Import statements here

public class TextFileServlet extends HttpServlet
{
public static final String PARAM_BLOB_ID = "id";
public static final String SERVLET_PATH = "/gaedownload/servefile";

private final Logger logger = LoggerFactory.getLogger(TextFileServlet.class);

@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException
{
Principal userPrincipal = req.getUserPrincipal();
PersistenceManager pm = PMFHolder.get().getPersistenceManager();
Long id = Long.parseLong(req.getParameter(PARAM_BLOB_ID));
MyFile myfile = pm.getObjectById(MyFile.class, id);

if (!userPrincipal.getName().equals(myfile.getUserName()))
{
logger.info("TextFileServlet.doGet - current user: " + userPrincipal + " file owner: "
+ myfile.getUserName());
return;
}

res.setContentType("application/octet-stream");
res.setHeader("Content-Disposition", "attachment;filename=\"" + myfile.getName() + "\"");
res.getOutputStream().write(myfile.getFile().getBytes());
}
}

You can always check the complete code if I have missed something here. I like this approach, because you don't need to create any HTML to upload into Blobstore. I hope you also find it useful.