miércoles, 11 de febrero de 2009

Como hacer la descarga de ficheros con servlets

Pues este es un problema muy curioso, como sabéis los servlet devuelven un stream como respuesta. La configuración más usada para los servlet es que la respuesta sea de tipo html, que especificamos:

response.setContentType("text/html");

Pero el contenido de la respuesta lo podemos cambiar a cualquier otro Mime type:

http://www.webmaster-toolkit.com/mime-types.shtml

Según esto podemos hacer que nuestro servlet devuelva cualquier tipo de fichero.

En el siguiente ejemplo escribo un servlet capaz de enviarnos un fichero zip de cualquier parte del disco del servidor. (Nota. esto hay que manejarlo con cuidado porque puede producir un hueco de seguridad gordisimo)

package org.company.servlet.readfile;


import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.net.*;


/**
*

This class handles Streaming Data Content


**/
public class Readfile extends HttpServlet {

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    /*
    * This Method Handles Post
    *
    * @param HttpServletRequest request
    * @param HttpServletResponse response
    */
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    /*
    * This Method Handles Get
    *
    * @param HttpServletRequest request
    * @param HttpServletResponse response
    */
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String urlstr = null;
        UserBean user = (UserBean)         request.getSession().getAttribute("user");
        try {
            response.reset();
            urlstr = request.getParameter("filename");
            ServletOutputStream sOutStream = response.getOutputStream();
            streamBinaryData(user.getName(),             urlstr, sOutStream, response);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void streamBinaryData(String username, String file, ServletOutputStream outstr, HttpServletResponse resp) throws IOException{
        String ErrorStr = null;

        try {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        String inFile = "/midirectorio/" + file;
        int tam = (int) new File(inFile).length();
        bis = new BufferedInputStream(new FileInputStream(inFile));

        try {
            //Asignamos el tipo de fichero zip
            resp.setContentType("application/x-zip-compressed"); //Cualquier mime type
            //Seteamos el tamaño de la respuesta
            resp.setContentLength(tam);
            resp.setHeader("Content-Disposition", "attachment;filename=\"" + file + "\"");

            bos = new BufferedOutputStream(outstr);

            // Bucle para leer de un fichero y escribir en el otro.
            byte[] array = new byte[1000];
            int leidos = bis.read(array);
            while (leidos > 0) {
                bos.write(array, 0, leidos);
                leidos = bis.read(array);
            }


            } catch (Exception e) {
                e.printStackTrace();
                ErrorStr = "Error Streaming the Data";
                outstr.print(ErrorStr);
            } finally {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    bos.close();
                }
                if (outstr != null) {
                    outstr.flush();
                    outstr.close();
                }
            }

            } catch (Exception e) {
                System.out.println("Fichero no encontrado");
                resp.sendRedirect("fileNotFound.jsp");

            }

        }
    }

Con este servlet conseguimos que en el navegador aparezca la típica ventanita de "descargas" al hacer una llamada al servlet, que recibe como parámetro el nombre del fichero. Este es el motivo por el que debemos tener cuidado al usar esto ya que podríamos crear un hueco de seguridad y que el usuario final se pueda descargar cualquier fichero de nuestro servidor.

Lo mejor sería tener un directorio controlado del que sacar los ficheros como en el ejemplo, o que se filtre el filename de entrada para saber que está entre los permitidos, etc ...

8 comentarios:

Jose dijo...

Hola Jorge, soy Jose Angel, estuve el año pasado en el curso de experto de la ETSII. He llegado a este blog buscando unas cosillas de jsf, y si lo hubiese encontrado antes me hubieras quitado mas de un dolor de cabeza. No se si este ejemplo que tienes de descarga me puede servir y como integrarlo en mi aplicación web. Te comento, tengo una base de datos oracle 10g, una de las tablas tiene una columna de tipo BLOB, para guardar archivos (PDF, Excel, Word, JPG, los tipicos, nada raro). Entonces en una de las páginas JSF quiero poner un enlace que al hacer click en él se abra la ventanita de descarga tipica y poder guardarlo donde yo quiera. Mi pregunta es, se puede hacer esto sin servlet? Y si es asi, estoy utilizando para el link un componente ADF Faces, y con un bean que haga toda la conexión y demás. Pero no se como sacar la ventanita de descarga. Espero haberme explicado lo que quería, y decirte que tu blog desde ahora ya está en favoritos!

Jorge Cantón Ferrero dijo...

Hola Jose me alegra saber de tí. Pues te comento para poder sacar hacia fuera un fichero este debe estar en un lugar publico, y con esto quiero decir que sea accesible con una URL. En el ejemplo que yo pongo este no es el caso y es cuando queremos poder descargar un fichero del disco que no es publico por eso necesito usar un servlet yo que tu lo haría con un servlet ya que necesitas usar mucho codigo java y va a quedar demasiado para un jsp. Por último la ventana de descarga la saca solo el propio navegador solo tienes que mandarle una respuesta de tipo fichero como se explica en este ejemplo y el navegador es el que mostrará la ventana de descarga automaticamente.

Espero resolver tus dudas, en cualquier caso escribeme comentarios en el blog para todas las dudas que puedan surgirte .

Un saludo

Jose dijo...

Hola Jorge, pues estoy usando JSF y toda la funcionalidad de la pagina la hago con un bean que es donde pongo todo el codigo java. Por lo tanto no me importa que el codigo se haga un poco largo. Lo que busco es simple, lo que ocurre es que estoy usando los componentes ADF que incorpora el JDeveloper, que lo hacen todo muy sencillo y bonito, pero no encuentro el componente necesario para la descarga de un archivo. Ya he utilizado el componente que sirve para subir algo (af:InputFile) pero no existe uno que sea (af:OutputFile). En concreto lo que quiero hacer es una vez obtenido de la base de datos el fichero, que se abra una ventana del navegador cuando pulses en un link o un icono para descargar el fichero y guardarlo en la carpeta que yo quiera. Seguro que no debe ser tan complicado como me parece ahora mismo pero es que no doy con la forma. Nuevamente muchas gracias por tu ayuda y por el blog!

Jorge Cantón Ferrero dijo...

Hola Jose. Pero como te comentaba no necesitas ningún componente de ADF o JSF la ventana que quieres la generan los navegadores automáticamente. Simplemente debes mandarle con un link hasta un fichero, para ello o el fichero esta público accesible bajo una dirección URL o como en el ejemplo que te pongo en la respuesta del servlet el objeto response le enchufas el String con los datos del contenido del fichero. Yo no veo ahora mismo la forma de hacerlo desde JSF yo haría un link que llamara a un servlet y la respuesta de este servlet fuera el contenido del fichero. Automaticamente el browser nos mostrará una ventana de descarga del fichero al reconocer que el contenido de la respuesta no es html sino un fichero binario.

Un saludo

Skwmak dijo...

Muy buen post, lo he testeado en Eclipse y Flex y funciona de lo más bien. Pero solamente falla cuando no encuentra el archivo, esto dado que no poseo la clase "fileNotFound.jsp". Para esto tuve que utilizar un metodo que obtenga el tamaño del fichero.

public static byte[] obtenerTamañoArchivo(File file) throws IOException {
InputStream is = new FileInputStream(file);

long length = file.length();

if (length > Integer.MAX_VALUE) {

}
byte[] bytes = new byte[(int)length];

// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
offset += numRead;
}

if (offset < bytes.length) {
throw new IOException("error de lectura "+file.getName());
}


is.close();
return bytes;
}

Espero que les sirva si alguien tuvo el mismo problema.

Saludos.

Jorge Cantón Ferrero dijo...

Estupendo Skwmak gracias por tu aporte.

Hector dijo...

Hola qué tal Jorge, me parece un buen post. Quisiera saber si existe una manera de que el response del servlet escriba en la misma página donde fue llamado, es decir que no abra una nueva página.

Harry Izquierdo dijo...

Hector... claro que se puede para eso tienes que usar ajax, te recomiendo documentacion sobre jquery y las funciones $.get , $.post , $ajax