Implementación de Subida de Ficheros en Aplicaciones Java

Fundamentos del Protocolo de Transferencia

El mecanismo de carga de recursos digitales se basa en un intercambio HTTP de tipo POST. El cliente empaqueta el archivo seleccionado dentro del cuerpo de la solicitud utilizando codificación multipart. Al recibir la petición, el servidor deserializa los flujos binarios, los aloja inicialmente en un directorio efímero del sistema y aplica filtros de validación antes de persistirlos en una ubicación definitiva del sistema de archivos.

Enfoques de Desarrollo en Java

La plataforma Java ofrece múltiples vías para gestionar esta funcionalidad:

  • API Nativa de Servlets: Requiere manipular manualmente el InputStream del HttpServletRequest y parsear los límites MIME.
  • Apache Commons FileUpload: Librería externa que automatiza la descomposición de peticiones multipart, facilitando la gestión de caché, límites y seguimiento de progreso.
  • Spring MVC: Abstrae el proceso mediante la interfaz MultipartFile, permitiendo inyección de dependencias y validación declarativa.

Directrices de Seguridad y Optimización

  • Aislamiento de almacenamiento: Utilizar directorios fuera del alcance público (ej. /WEB-INF/almacen).
  • Nombrado único: Evitar colisiones aplicando UUID, sellos de tiempo o algoritmos de hash sobre el nombre original.
  • Control de capacidad: Definir umbrales máximos estrictos por fichero y por petición completa.
  • Validación de extensiones: Restringir la ingesta a tipos MIME y extensiones explícitamente permitidos.

Componentes Esenciales de la Biblioteca

La implementación clásica requiere las dependencias commons-fileupload y commons-io. El formulario cliente debe especificar method="post" y enctype="multipart/form-data". La arquitectura se sustenta en tres clases clave:

  • DiskFileItemFactory: Define el tamaño del búfer en memoria y la ruta del sistema de archivos temporal.
  • ServletFileUpload: Moter de análisis que verifica el contenido, aplica codificación, establece límites de tamaño y devuelve una colección de objetos procesables.
  • FileItem: Representación abstracta de cada campo enviado. Diferencia entre parámetros de texto y flujos binarios, permitiendo extraer metadatos y gestionar la limpieza de caché.

Flujo de Trabajo y Arquitectura de la Solución

  1. Verificar si la solicitud HTTP contiene datos multipart.
  2. Provisionar rutas absolutas para el almacenamiento seguro y la caché temporal, creando los directorios si no existen.
  3. Instanciar la factoría con parámetros de umbral y repositorio.
  4. Configurar el analizador con codificación, límites de peso y un oyente de progreso.
  5. Descomponer la petición en una lista de FileItem e iterar sobre ellos:
    • Si es campo de texto, extraer nombre y valor.
    • Si es binario, validar nombre, generar ruta aleatoria, inicilaizar flujos de E/S y transferir datos en bloques antes de liberar la caché.

Ejemplo de Implementación

Estructura del formulario cliente:

<form action="${pageContext.request.contextPath}/gestionarCarga.do" method="post" enctype="multipart/form-data">
    <div>
        <label>Identificador de Usuario:</label>
        <input type="text" name="usuarioId">
    </div>
    <div>
        <label>Seleccionar Recurso:</label>
        <input type="file" name="recursoBinario">
    </div>
    <button type="submit">Procesar</button>
</form>

Controlador Servlet reestructurado:

public class GestorArchivosServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest peticion, HttpServletResponse respuesta) throws ServletException, IOException {
        if (!ServletFileUpload.isMultipartContent(peticion)) {
            respuesta.sendError(HttpServletResponse.SC_BAD_REQUEST, "Formato de solicitud inválido");
            return;
        }

        String rutaSegura = getServletContext().getRealPath("/WEB-INF/almacen");
        String rutaTemporal = getServletContext().getRealPath("/WEB-INF/tmp");
        crearRuta(rutaSegura);
        crearRuta(rutaTemporal);

        DiskFileItemFactory fabrica = inicializarFabrica(new File(rutaTemporal));
        ServletFileUpload motor = inicializarMotor(fabrica);

        String mensajeEstado;
        try {
            List<FileItem> items = motor.parseRequest(peticion);
            mensajeEstado = procesarElementos(items, rutaSegura);
        } catch (FileUploadException ex) {
            mensajeEstado = "Fallo en la ingesta: " + ex.getMessage();
        }

        peticion.setAttribute("resultado", mensajeEstado);
        peticion.getRequestDispatcher("/estado.jsp").forward(peticion, respuesta);
    }

    private String procesarElementos(List<FileItem> componentes, String directorioBase) throws IOException {
        for (FileItem componente : componentes) {
            if (componente.isFormField()) {
                System.out.println("Parámetro: " + componente.getFieldName() + " | " + componente.getString("UTF-8"));
            } else {
                String nombreCrudo = componente.getName();
                if (nombreCrudo == null || nombreCrudo.trim().isEmpty()) continue;

                String ext = nombreCrudo.substring(nombreCrudo.lastIndexOf('.') + 1);
                String idUnico = UUID.randomUUID().toString();
                String carpetaDestino = String.join(File.separator, directorioBase, idUnico);

                File carpeta = new File(carpetaDestino);
                if (!carpeta.exists()) carpeta.mkdirs();

                File archivoFinal = new File(carpeta, "datos." + ext);
                copiarFlujo(componente.getInputStream(), archivoFinal);
                componente.delete();
            }
        }
        return "Transferencia finalizada";
    }

    private DiskFileItemFactory inicializarFabrica(File rutaCache) {
        DiskFileItemFactory fab = new DiskFileItemFactory();
        fab.setSizeThreshold(1_048_576);
        fab.setRepository(rutaCache);
        return fab;
    }

    private ServletFileUpload inicializarMotor(DiskFileItemFactory fab) {
        ServletFileUpload motor = new ServletFileUpload(fab);
        motor.setHeaderEncoding("UTF-8");
        motor.setFileSizeMax(10_485_760);
        motor.setSizeMax(52_428_800);

        motor.setProgressListener((leidos, total, indice) -> {
            double avance = total > 0 ? ((double) leidos / total) * 100 : 0;
            System.out.printf("Progreso: %.2f%% [%d/%d bytes]\n", avance, leidos, total);
        });
        return motor;
    }

    private void crearRuta(String path) {
        File dir = new File(path);
        if (!dir.exists()) dir.mkdirs();
    }

    private void copiarFlujo(InputStream entrada, File salida) throws IOException {
        try (InputStream in = entrada; FileOutputStream out = new FileOutputStream(salida)) {
            byte[] bloque = new byte[8192];
            int lectura;
            while ((lectura = in.read(bloque)) != -1) {
                out.write(bloque, 0, lectura);
            }
        }
    }
}

Etiquetas: commons-fileupload java-servlets http-multipart file-io-streams apache-commons-io

Publicado el 9-8 16:14