Sunday, June 3, 2012

Using /etc/mime.types for Grails Files Downloads

I find it sorely disappointing that ServletContext#getMimeType() doesn't just use apache's standard /etc/mime.types automatically. Instead, it seems to use some smaller subset of extension mappings included in some servlet jar somewhere. Fortunately, it's easy enough to use spring's ConfigurableMimeFileTypeMap bean to get the mappings from the mime.types files of your choosing.

The way I like to set it up is to add a custom configuration property in my grails-app/conf/Config.groovy, and specify a comma-separated list of file paths to use (that way I can use the standard /etc/mime.types file, but also add in my own custom mime.types file if I need some mappings not in the standard one):

grails-app/conf/Config.groovy
mime.types = '/etc/mime.types'

Then, in grails-app/conf/spring/resources.groovy, I define a ConfigurableMimeFileTypeMap bean (I call it fileTypeMap, but you can call it whatever):

grails-app/conf/resources.groovy
fileTypeMap(org.springframework.mail.javamail.ConfigurableMimeFileTypeMap) { application.config.mime.types.split(/,/).each { mappingLocation = new org.springframework.core.io.FileSystemResource(it) } }

Finally, in the controller actions where I want to stream out a file, I use the ConfigurableMimeFileTypeMap bean's getContentType() method to calculate the file's content type from its name:

grails-app/controllers/MyController.groovy
def fileTypeMap def download = { try { // some logic here to locate the appropriate file to stream def file = new File('/tmp/foo.txt') // use /etc/mime.types to determine the file's content type from its extension def contentType = fileTypeMap.getContentType(file.name.toLowerCase()) // must special-case text/html; see http://jira.grails.org/browse/GRAILS-1223 if (contentType == 'text/html') return render(contentType: contentType, text: file.text) // set up standard (inline) file-download headers response.contentType = contentType response.contentLength = file.length() as Integer response.setHeader 'Content-Disposition', "inline; filename=\"${file.name}\"" // stream the file file.inputStream.withStream { response.outputStream << it } } catch (FileNotFoundException e) { response.sendError 404 } }