Recently I have been working on an Android project that required me to store some images into cloud. Upon reading through google documentation, I learnt that the best place to store dynamic images is Google BlobService. Unfortunately for me it required some iterations to figure out how to use this service. So I thought I would document it here for the help of others.
It is important to understand how the BlobService workflow works. Following is the typical workflow of a blobservice.
It is important to understand how the BlobService workflow works. Following is the typical workflow of a blobservice.
- The client first creates an upload URL where the image would be uploaded. Since it is hard (not possible) to do a decent authentication scheme around blobservice, I decided to use a appengine endpoint to create the upload URL for me. The code for appengine endpoint implementation is very simple.
@ApiMethod(name = "getUploadURL")
public UploadURL getUploadURL() {
BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
return new UploadURL(blobstoreService.createUploadUrl("/_ah/uploads"));
}
The arugument to createUploadUrl is the suffix that would be added to your appengine base URL. We need to understand that the actual image upload is unauthenticated, i.e. anybody who has the URL can upload the image but the endpoint itself is authenticated and hence nobody would know the URL to upload anything except authenticated users.
- Now that we have the URL, the first thing that we need to do is to convert the image into a byte array that we can upload. I have created following utility function to do just that. Basically I don't want to store full sized images, I have created this function that takes a Bitmap and scales the image and converts it to a byte array. The images is scaled with actual aspect ratio with the width fixed at maximum PRODUCT_IMAGE_WIDTH.
public static byte[] getByteArrayFromBitmap(Bitmap bitmap, boolean scale) {
Bitmap scaledBitmap = bitmap;
if (scale) {
scaledBitmap = scale(bitmap);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
return stream.toByteArray();
}
public static Bitmap scale (Bitmap source){
int w = source.getWidth();
double factor = 1.0;
if (w > PRODUCT_IMAGE_WIDTH) {
factor = ((double)w) / ((double)PRODUCT_IMAGE_WIDTH);
}
else {
}
int dw = new Double((((double)factor) * ((double)(source.getWidth())))).intValue();
int dh = new Double((((double)factor) * ((double)(source.getHeight())))).intValue();
return Bitmap.createScaledBitmap(source, dw, dh, false);
}
- Now that we have the image as a byte array, we upload the image from the client using this URL. Since google has removed the old HTTPClient, I had to figure out how to use OkHTTP which seems to be the new HTTP client that needs to be used. It was really not very hard to do this, it is just that documentation around OKHttp is very sparse and it takes some trial and error to make this work. We set a response header so that the client can receive the cloud key that is generated by the blob service. We need to know this key to retrieve the image later.
String uploadURL = api.getUploadURL().execute().getUrl();
OkHttpClient httpClient = new OkHttpClient();
String filename = UUID.randomUUID().toString() + ".png";
RequestBody body = new MultipartBuilder("image-part-name")
.type(MultipartBuilder.FORM)
.addFormDataPart("file", filename,
RequestBody.create(MediaType.parse("image/png"), imageByteArray))
.build();
Request request = new Request.Builder().url(uploadURL).post(body).build();
Response response = httpClient.newCall(request).execute();
String cloudKey = response.header("X-MyApplication-Blob-Cloud-key");
// We can store this cloud key in cloud store entity with the other data related to
// image so that we can retrieve it later.
- Let's look at the implementation of blobservice. We basically need to create a servlet to handle the upload request. The doPost of the servlet should handle the upload. WIth some trial and error, I figured out that the blobservice API looks for a multipart payload with image byte array and header with the name "file". You can also provide the bytearray type, otherwise it decodes it with the filename extension that you provide as part of the value.
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
Map> blobs = blobstoreService.getUploads(req);
List blobKeys = blobs.get("file");
if (blobKeys == null || blobKeys.isEmpty()) {
} else {
res.setHeader("X-MyApplication-Blob-Cloud-key", blobKeys.get(0).getKeyString());
}
}
- Retrieving the image is also very simple. You need another servlet to do that. In my case I have a servlet with a GET method which returns the image.
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
blobstoreService.serve(blobKey, res);
}
As we can see, it is quite easy to build a work flow where the image is stored with blobservice and other metadata is stored with google cloud store with cloud key. The image can be retrieved based on the other metadata and lookup from cloud store.