using blobstore with google cloud endpoint and android
Asked Answered
T

3

33

I am developing an app-engine connected android project using the eclipse plugin. One aspect of the app is to allow user Alpha to send pictures to user Bravo. To do that I have the following setup:

User Alpha posting:

  • send image to my app engine server through endpoints
  • server stores image in blob store
  • server stores blobkey in datastore

User Bravo getting:

  • server gets blobkey from datastore
  • server gets image using blob key
  • server sends image to android app using endpoints

This setup takes upward of two (2) minutes from when my android app sends an image to when I can see it in the blob sore. Needless to say this is completely unacceptable.

My server is processing the image programmatically, thru the following code:

public static BlobKey toBlobstore(Blob imageData) throws FileNotFoundException, FinalizationException, LockException, IOException {
        if (null == imageData)
            return null;

        // Get a file service
        FileService fileService = FileServiceFactory.getFileService();

        // Create a new Blob file with mime-type "image/png"
        AppEngineFile file = fileService.createNewBlobFile("image/jpeg");// png

        // Open a channel to write to it
        boolean lock = true;
        FileWriteChannel writeChannel = fileService.openWriteChannel(file, lock);

        // This time we write to the channel directly
        writeChannel.write(ByteBuffer.wrap
            (imageData.getBytes()));

        // Now finalize
        writeChannel.closeFinally();
        return fileService.getBlobKey(file);
    }

Does anyone know how I can either adapt the official example to use endpoints (in the case where I must use my app-engine instances) or use getServingUrl (bypassing my instances) to store and serve my blobs?
Please, instead of words, include the code. Thanks.

Tundra answered 26/4, 2013 at 23:42 Comment(2)
I've been fiddeling with blobstore myself for some time. Didn't get it to work to my liking and finally ended up using the drive api to exchange pictures between users. Very easy to implement and works like a charm. Maybe thats an option for you too...Colicroot
The accepted reply is really a great answer, but it would be a lot better if someone could give the endpoint answer. Especially, how do I get the callback url to point to an endpoint method? If someone answers that for me, it would be great.Sivas
B
32

I'll share how I'm doing this. I'm not using the google-cloud-endpoints, but just my own rest based api, but it should be the same idea either way.

I'll lay it out step by step with code, hopefully it will be clear. You'd simply adapt the way you send your requests to use endpoints instead of doing it more generic like in this example. I'm including some boilerplate, but excluding try/catch,error checking etc for brevity.

Step 1 (client)

First client requests an upload url from server:

HttpClient httpclient = new DefaultHttpClient();    
HttpConnectionParams.setConnectionTimeout(httpclient.getParams(), 10000); //Timeout Limit

HttpGet httpGet = new HttpGet("http://example.com/blob/getuploadurl");
response = httpclient.execute(httpGet);

Step 2 (server)

On the server side the upload request servlet would look something like this:

String blobUploadUrl = blobstoreService.createUploadUrl("/blob/upload");

res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/plain");

PrintWriter out = res.getWriter();
out.print(blobUploadUrl);
out.flush();
out.close();

note the argument to createUploadUrl. This is where the client will be redirected once the actual upload has been completed. That's where you'll handle storing the blobkey and/or serving url and returning it to the client. You'll have to map a servlet to that url, which will handle step 4

Step 3 (client) Back to the client again to send the actual file to the upload url using the url returned from step 2.

HttpClient httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost(uploadUrlReturnedFromStep2);

FileBody fileBody  = new FileBody(thumbnailFile);
MultipartEntity reqEntity = new MultipartEntity();

reqEntity.addPart("file", fileBody);

httppost.setEntity(reqEntity);
HttpResponse response = httpclient.execute(httppost)

Once this request is sent to the servlet in step 2, it will be redirected to the servlet you specified in the createUploadUrl() earlier

Step 4 (server)

Back to the server side: This is the servlet handling the url mapped to blob/upload. We will here return the blobkey and serving url to the client in a json object:

List<BlobKey> blobs = blobstoreService.getUploads(req).get("file");
BlobKey blobKey = blobs.get(0);

ImagesService imagesService = ImagesServiceFactory.getImagesService();
ServingUrlOptions servingOptions = ServingUrlOptions.Builder.withBlobKey(blobKey);

String servingUrl = imagesService.getServingUrl(servingOptions);

res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("application/json");

JSONObject json = new JSONObject();
json.put("servingUrl", servingUrl);
json.put("blobKey", blobKey.getKeyString());

PrintWriter out = res.getWriter();
out.print(json.toString());
out.flush();
out.close();

Step 5 (client)

We'll get the blobkey and serving url from the json and then send it along with user id etc to store in the datastore entity.

JSONObject resultJson = new JSONObject(resultJsonString);

String blobKey = resultJson.getString("blobKey");
String servingUrl = resultJson.getString("servingUrl");

List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);

nameValuePairs.add(new BasicNameValuePair("userId", userId));
nameValuePairs.add(new BasicNameValuePair("blobKey",blobKey));
nameValuePairs.add(new BasicNameValuePair("servingUrl",servingUrl));

HttpClient httpclient = new DefaultHttpClient();
HttpConnectionParams.setConnectionTimeout(httpclient.getParams(), 10000);

HttpPost httppost = new HttpPost(url);
httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
HttpResponse response = httpclient.execute(httppost);

// Continue to store the (immediately available) serving url in local storage f.ex

Step 6 (server) Actually storing everything in the datastore (using objectify in this example)

final String userId   = req.getParameter("userId");
final String blobKey  = req.getParameter("blobKey");
final String servingUrl = req.getParameter("servingUrl");

ExampleEntity entity = new ExampleEntity();
entity.setUserId(userId);
entity.setBlobKey(blobKey);
entity.setServingUrl(servingUrl);

ofy().save().entity(entity);

I hope this makes things more clear. If someone wants to edit the answer to use cloud endpoints instead of this more generic example, feel free :)

About the serving url

The serving url is a great way to serve images to your clients, because of the way it can dynamically scale images on the fly. For example you can send smaller images to your LDPI users by simply appending =sXXX at the end of the serving url. Where XXX is the pixel size of the largest dimension of your image. You completely avoid your instances and only pay for bandwidth, and the user only downloads what she needs.

PS!

It should be possible to stop at step 4 and just store it directly there, by passing along userId f.ex in step 3. Any parameters are supposed to be sent along to Step 4, but I did not get that to work, so this is how I do it at the moment, so I'm sharing it this way since i know it works.

Backwoodsman answered 2/6, 2013 at 21:8 Comment(13)
Great answer! that's help me a lot! I have just one question. the url in the createUploadUrl() method is relative or absolut? I mean for sample my servelet has this url : 192.168.0.6:8888/1/api/blobs/upload, what am I suppose to write in the createUploadUrl() parameter? all the path?Maenad
@Maenad Not sure if an absolute URL would work, but relative works for sure. Just try and see :)Backwoodsman
I've managed to do it in four steps, if anybody is interested I'll post the solution. Would do nothing without this tutorial though... thanks Joachim!!Leisurely
you can use endpoint instead of the first servlet that way @ApiMethod(name = "getBlobURL") public BlobAttributes getBlobURL() throws Exception { BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); String blobUploadUrl = blobstoreService.createUploadUrl("/getURL"); BlobAttributes ba= new BlobAttributes(); ba.setBlobURl(blobUploadUrl); return ba; }Country
How do you import MultipartEntity?Disseisin
what do you want to do @VandusCountry
@Backwoodsman can you please explain how to get resultJsonString?Country
@AhmadSaadFares I am saying MultipartEntity type is not a part of java core API, or rather Android core API so I am asking how to include it into the project so that I can use it. I encountered some issues including it in the project...in the end I used RetrofitDisseisin
@Disseisin It's org.apache.http.entity.mime.MultipartEntity. Apparently it's deprecated though, replaced by MultipartEntityBuilder.Backwoodsman
@AhmadSaadFares the resultJsonString is the response sent back to the client in step 4Backwoodsman
@Xylian, could you perhaps share your solution? Thanks so much!Hochman
Thanks a lot, Finally I got a complete answer with description.Lapham
Tysm you have explained all the process properly. But still i am getting stuck at step 3 in android studio. I have followed your steps and created step 4 also. I am using HttpURLConnection but even importing httpmime 4.3 jar file. My fileBody cannot be resolved can you help me out.I am trying this since few days @joachim. If you want i can send you the solutionsWessels
B
5

I used the answer of this question to build my own system that uses AppEngine Endpoints. Unlike the posts above, I want to have a clean API that directly transmits the image (as byte array) to Google Endpoint and the upload to BlobstorageService is done on the backend side. The benefit of that is that i have an atomic API. The drawback obviously the load on the server as well as the heavy marshalling operations on the client.

Android - load, scale and serialize image and upload to endpoints

void uploadImageBackground(Bitmap bitmap) throws IOException {
    // Important! you wanna rescale your bitmap (e.g. with Bitmap.createScaledBitmap)
    // as with full-size pictures the base64 representation would not fit in memory

    // encode bitmap into byte array (very resource-wasteful!)
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
    byte[] byteArray = stream.toByteArray();
    bitmap.recycle();
    bitmap = null;
    stream = null;

    // Note: We encode ourselves, instead of using image.encodeImageData, as this would throw
    //       an 'Illegal character '_' in base64 content' exception
    // See: https://mcmap.net/q/453419/-upload-photos-from-android-app-to-google-cloud-storage-app-engine-illegal-character-39-_-39
    String base64 = Base64.encodeToString(byteArray, Base64.DEFAULT);
    byteArray = null;

    // Upload via AppEngine Endpoint (ImageUploadRequest is a generated model)
    ImageUploadRequest image = new ImageUploadRequest();
    image.setImageData(base64);
    image.setFileName("picture.png");
    image.setMimeType("image/png");
    App.getMyApi().setImage(image).execute();
}

Backend API Endpoint - Upload image to BlobstorageService

@ApiMethod(
        name = "setImage",
        path = "setImage",
        httpMethod = ApiMethod.HttpMethod.POST
)
public void saveFoodImageForUser(ImageUploadRequest imageRequest) throws IOException {
    assertNotEmpty(userId, "userId");
    assertNotNull(imageRequest, "imageRequest");

    // create blob url
    BlobstorageService blobService = BlobstoreServiceFactory.getBlobstoreService();
    String uploadUrl = blobService.createUploadUrl("/blob/upload");

    // create multipart body containing file
    HttpEntity requestEntity = MultipartEntityBuilder.create()
            .addBinaryBody("file", imageRequest.getImageData(),
                    ContentType.create(imageRequest.getMimeType()), imageRequest.getFileName())
            .build();

    // Post request to BlobstorageService
    // Note: We cannot use Apache HttpClient, since AppEngine only supports Url-Fetch
    //  See: https://cloud.google.com/appengine/docs/java/sockets/
    URL url = new URL(uploadUrl);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setDoOutput(true);
    connection.setRequestMethod("POST");
    connection.addRequestProperty("Content-length", requestEntity.getContentLength() + "");
    connection.addRequestProperty(requestEntity.getContentType().getName(), requestEntity.getContentType().getValue());
    requestEntity.writeTo(connection.getOutputStream());

    // BlobstorageService will forward to /blob/upload, which returns our json
    String responseBody = IOUtils.toString(connection.getInputStream());

    if(connection.getResponseCode() < 200 || connection.getResponseCode() >= 400) {
        throw new IOException("HTTP Status " + connection.getResponseCode() + ": " + connection.getHeaderFields() + "\n" + responseBody);
    }

    // parse BlopUploadServlet's Json response
    ImageUploadResponse response = new Gson().fromJson(responseBody, ImageUploadResponse.class);

    // save blobkey and serving url ...
}

Servlet that handles callback from BlobstorageService

public class BlobUploadServlet extends HttpServlet {
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        BlobstorageService blobService = BlobstoreServiceFactory.getBlobstoreService();
        List<BlobKey> blobs = blobService.getUploads(req).get("file");
        if(blobs == null || blobs.isEmpty()) throw new IllegalArgumentException("No blobs given");

        BlobKey blobKey = blobs.get(0);

        ImagesService imagesService = ImagesServiceFactory.getImagesService();
        ServingUrlOptions servingOptions = ServingUrlOptions.Builder.withBlobKey(blobKey);

        String servingUrl = imagesService.getServingUrl(servingOptions);

        res.setStatus(HttpServletResponse.SC_OK);
        res.setContentType("application/json");

        // send simple json response (ImageUploadResponse is a POJO)
        ImageUploadResponse result = new ImageUploadResponse();
        result.setBlobKey(blobKey.getKeyString());
        result.setServingUrl(servingUrl);

        PrintWriter out = res.getWriter();
        out.print(new Gson().toJson(result));
        out.flush();
        out.close();
    }
}

The only thing left to do is to bind /blob/upload to UploadBlobServlet.

Note: This doesn't seem to work when AppEngine is running locally (if executed locally, then the POST to BlobstorageService would always return a 404 NOT FOUND)

Beller answered 21/7, 2015 at 13:15 Comment(3)
how to bind '/blob/upload' to UploadBlobServlet.?Shortsighted
Isn't there a 1mb limit to the size of the image you can upload via endpoint method? Thanks for sharing the code btwBold
@MicroR No the limit is 10MiB. cloud.google.com/datastore/docs/concepts/limitsChloro
L
2

Since I tried with many way to do the callback service in the api of endpoint, I abort that aproach. However, I could solve that problem making a parallel servlet to the api endpoint, it only needs define the class server and add it web.xml configuration. Here my solution:

1 Enpoint Service for get the URL for upload: Then the service coudl be protected with clientId

@ApiMethod(name = "getUploadURL",  httpMethod = HttpMethod.GET)
    public Debug getUploadURL() { 
        String blobUploadUrl =  blobstoreService.createUploadUrl("/update");
        Debug debug = new Debug(); 
        debug.setData(blobUploadUrl);
        return debug; 
    }

2. Now the Client can call to endpoint for get the upload URL:
Maybe some like this (for android use you client library enpoint too):

gapi.client.debugendpoint.getUploadURL().execute(); 

3. The next step is todo a post to url catched in last step: You can do that with a httpClient of android, again, in my case I need upload from a web then I use a form, and onChangeFile() event callback for get the uploadurl (using step 3) then when it response to change the form parameters "action" and "codeId" before that someone decide do click on submit button:

<form id="submitForm"  action="put_here_uploadUrl" method="post" enctype="multipart/form-data">
<input type="file" name="image" onchange="onChangeFile()">
<input type="text" name="codeId" value='put_here_some_dataId'>
<input type="submit" value="Submit"></form>

4 Finally the paralele servlet class:

@SuppressWarnings("serial")
public class Update  extends HttpServlet{

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {    

        String userId   = req.getParameter("codeId");

        List<BlobKey> blobs = BSF.getService().getUploads(req).get("image");
        BlobKey blobKey = blobs.get(0);

        ImagesService imagesService = ImagesServiceFactory.getImagesService();
        ServingUrlOptions servingOptions = ServingUrlOptions.Builder.withBlobKey(blobKey);
        String servingUrl = imagesService.getServingUrl(servingOptions);

        resp.setStatus(HttpServletResponse.SC_OK);
        resp.setContentType("application/json");


        JSONObject json = new JSONObject();
        try {
            json.put("imageUrl", servingUrl);
            json.put("codeId", "picture_of_"+userId);
            json.put("blobKey",  blobKey.getKeyString());
        } catch (JSONException e){

            e.printStackTrace();            
        }

        PrintWriter out = resp.getWriter();
        out.print(json.toString());
        out.flush();
        out.close();
    }
}

and add to web.xml, where com.apppack is the package of Update Class

<servlet>
<servlet-name>update</servlet-name>
<servlet-class>com.apppack.Update</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>update</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
Listlessness answered 27/2, 2015 at 19:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.