File download con Spring MVC Framework

De ChuWiki

La forma "fácil" de permitir la descarga de un fichero desde nuestra aplicación web es simplemente poner un enlace en nuestra página a ese fichero

 <a href="path/fichero">Descargar fichero</a>

sin embargo, esto tiene varias pegas cuando estos ficheros son ficheros que han subido los usuarios

  • El fichero tiene que estar como un fichero normal en el sistema de ficheros (en el disco). No podemos guardar el fichero en base de datos, por ejemplo.
  • El fichero tiene que estar en un path que nuestro servidor haga público. En el caso de una aplicacion.war (con tomcat por ejemplo), el fichero debe estar por debajo de $TOMCAT_HOME/webapps/aplicacion y el problema con ello es que si redesplegamos la aplicacion, nos cargamos el fichero. Si el fichero no está en el nuevo war (y en principio no lo está porque hemos dicho que son ficheros subidos por los usuarios), lo hemos perdido.
  • Si al usuario le da por subir un fichero .jsp con código, cuando hagamos click en el enlace correspondiente, en vez de descargar el fichero ... ¡¡ se ejecutará !!. Cualquier usuario malintencionado puede subir código .jsp mal intencionado.

Por ello suele ser útil hacer un servlet, página .jsp o como en nuestro caso, Controller de Spring Framework encargado de hacer la descarga. Este Controller recibe de alguna forma qué fichero quiere descargarse el usuario (el nombre del fichero, un id o lo que sea) y se encarga de leer dicho fichero y devolverlo. Con esto, podemos leer el fichero de base de datos, de cualquier path del servidor sea o no público y por supuesto, leer y entregar el posible fichero .jsp que haya subido un usuario sin ejecutarlo.

El Controller de Spring Framework puede ser como el siguiente (usa Apache Commons IO).

package com.chuidiang.ejemplos_spring;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;    // Apache commons IO
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class FileDownloadController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        try {
            // Suponemos que es un zip lo que se quiere descargar el usuario.
            // Aqui se hace a piñón fijo, pero podría obtenerse el fichero
            // pedido por el usuario a partir de algún parámetro del request
            // o de la URL con la que nos han llamado.
            String nombreFichero = "fichero.zip";
            String unPath = "c:/path_fichero/";

            response.setContentType("application/zip");
            response.setHeader("Content-Disposition", "attachment; filename=\""
                    + nombreFichero+ "\"");

            InputStream is = new FileInputStream(unPath+nombreFichero);
            
            IOUtils.copy(is, response.getOutputStream());
            
            response.flushBuffer();
            
        } catch (IOException ex) {
            // Sacar log de error.
            throw ex;
        }
        
        return null;
    }
}

Veamos los trozos importantes de código.

Hemos puesto a piñón fijo el path (c:/path_fichero/) y el nombre de fichero (fichero.zip). En una aplicación real el Controller debería deducir qué fichero se quiere descargar a partir de algún parámetro en request o en la URL y puede que tenga que leerlo del disco, como en el ejemplo, o extraer el contenido de una base de datos.

En el response es importante poner el tipo de fichero ( setContentType("application/zip") ), de forma que el navegador sepa que hacer con él. Si conoce el tipo de fichero (txt, html, ...) lo mostrará. Si no lo conoce, ofrecerá la descarga. Para facilitar la descarga ofreciendo un nombre de fichero, podemos poner la "Content-Disposition" con la línea response.setHeader("Content-Disposition", ...);

Finalmente, usando Apache commons IO (o directamente con código java puro y duro), abrimos el fichero y lo vamos leyendo para meter los bytes en el outputStream del response. response.flushBuffer() cuando terminemos.