Wednesday, May 8, 2013

Stupid download of browser self-generated content

HTML5 is here but we got IE 8 to support and then IE 9 is gonna be around for a while too. Often times we might be generating data on the browser that the server is not aware of and need not be aware of, e.g. we might be drawing some doodles and we want to save it and keep a copy locally on our desktop. Now since the content is already here at the browser, why should we not be able to write to a file? Well that's why we have data uri (also see Data_URI_scheme)
But tragically, it doesn't work with most contemporary browsers. We could then use something like downloadify (uses flash though).
So, is there a javascript/html only solution? None so far that works perfectly (Data-uri on Safari and Firefox 19- requires the user to enter a filename and there is no default name support. On IE there is the document.execCommand that you could use but it require pop-up to be enabled and once again you can't suggest a default file name).
Well it might seem very dumb but sacrificing latency and server cpu cycles and just doing a round-trip to the server and using an anchor element seems to be the safest bet:
  var linkEl = this.$('a.export-csv');
  linkEl.click( function _onExportCsv (evt) {
      var csvText = $('.scratch-pad').text();//get the content
      var filename = "mydata"+JSON.stringify(new Date()).replace(/"/g,'');//get a default filename"
      if (/chrome/i.test(navigator.userAgent) || /Mozilla\/5\.0.+\srv:2\d\.\d/i.test(navigator.userAgent)) {
        //this works only on Chrome and FF20+
        linkEl.attr('href',"data:plain/text," + encodeURIComponent(csvText));
        linkEl.attr('download',filename);
        return true; //DO follow the link NOW
      } else {
        var formTemplate = _.template(
        '<form action="<%=downloadEchoUrl%>" method="post">\
          <input type="hidden" name="filename" value="<%-filename%>" />\
          <input type="hidden" name="contentToEcho" value="<%-contentToEcho%>" />\
        &lt/form>');
        //using POST for safety - the payload could be large and IE doesn't suppor long URLs
        $(formTemplate({
          downloadEchoUrl: "http://ustaman.com/echo",
          filename:filename,
          contentToEcho: encodeURIComponent(csvText)
          //note the template will do an escape, use of &lt%- instead of $lt%= with underscore
        })).appendTo('body').submit().remove();
        return false; //do NOT follow the link
      }
  });
And here is a sample "echo" server in Java (one could write a simpler version using node or python or what not).
package com.ustaman;

import java.io.IOException;
import java.net.URLDecoder;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(name = "FileDownloadEchoServlet", asyncSupported = false, urlPatterns = { "/echo/*" }, loadOnStartup = 4)
public class FileDownloadEchoServlet extends HttpServlet {

    private static final long serialVersionUID = 1343508390612726798L;

    private static Logger logger = LoggerFactory
            .getLogger(FileDownloadEchoServlet.class);

    @Override
    /* We could use Spring MVC with @RequestBody for production code */
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    /* We could use Spring MVC with @RequestBody for production code */
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("data/plain");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader(
                "Content-Disposition",
                String.format("attachment; filename=%s",
                        req.getParameter("filename")));
        resp.getOutputStream().write(
                URLDecoder.decode(req.getParameter("contentToEcho"), "UTF-8")
                        .getBytes("UTF-8"));
        resp.getOutputStream().close();
        logger.info("File download echoed");
    }

}