API para carga fragmentada de archivos en backend Java

Este artículo presenta una implementación de back end Java para la carga fragmentada de archivos. Incluye una herramienta para la gestión de archivos y un controlador REST para manejar el registro de archivos, la subida de fragmentos y la unión final.

Clase de utilidades para manejo de archivos La siguiente clase proporciona métodos para listar, fusionar y eliminar archivos, así como para ordenar fragmentos por su índice numérico.

package com.example.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class GestorArchivos {
    /**
     * Combina todos los archivos dentro de un directorio en un archivo de salida único.
     * Los archivos se ordenan por el número en su nombre antes de la fusión.
     */
    public static void combinarArchivos(String directorioOrigen, String archivoDestino) throws IOException {
        File destino = new File(archivoDestino);
        try (FileOutputStream salida = new FileOutputStream(destino, true);
             FileChannel canalSalida = salida.getChannel()) {
            
            File fuente = new File(directorioOrigen);
            if (fuente.isDirectory()) {
                List<File> fragmentos = listarYOrdenar(directorioOrigen);
                long posicion = 0;
                for (File fragmento : fragmentos) {
                    try (FileInputStream entrada = new FileInputStream(fragmento);
                         FileChannel canalEntrada = entrada.getChannel()) {
                        canalSalida.transferFrom(canalEntrada, posicion, fragmento.length());
                        posicion += fragmento.length();
                    }
                }
            }
        }
    }

    /**
     * Lista los archivos en un directorio y los ordena ascendente por el número extraído de su nombre.
     * Ejemplo: "chunk_2.part" -> número 2.
     */
    public static List<File> listarYOrdenar(String rutaDirectorio) {
        File directorio = new File(rutaDirectorio);
        File[] archivos = directorio.listFiles();
        if (archivos == null) return Collections.emptyList();
        
        List<File> lista = Arrays.asList(archivos);
        Collections.sort(lista, (a, b) -> {
            int numA = extraerNumero(a.getName());
            int numB = extraerNumero(b.getName());
            return Integer.compare(numA, numB);
        });
        return lista;
    }

    private static int extraerNumero(String nombreArchivo) {
        // Ejemplo de nombre: "chunk_45.part"
        String nombreSinExtension = nombreArchivo.contains(".") ?
                nombreArchivo.substring(0, nombreArchivo.lastIndexOf('.')) : nombreArchivo;
        String[] partes = nombreSinExtension.split("_");
        if (partes.length >= 2) {
            try {
                return Integer.parseInt(partes[1]);
            } catch (NumberFormatException e) {
                return 0;
            }
        }
        return 0;
    }

    /**
     * Elimina un directorio y todo su contenido de forma recursiva.
     */
    public static boolean eliminarDirectorio(String rutaDirectorio) {
        File directorio = new File(rutaDirectorio);
        if (!directorio.exists() || !directorio.isDirectory()) {
            return false;
        }

        File[] hijos = directorio.listFiles();
        if (hijos != null) {
            for (File hijo : hijos) {
                if (hijo.isDirectory()) {
                    if (!eliminarDirectorio(hijo.getAbsolutePath())) {
                        return false;
                    }
                } else {
                    if (!hijo.delete()) {
                        return false;
                    }
                }
            }
        }
        return directorio.delete();
    }
}

Controlador para la carga fragmentada Este contorlador expone endpoints para el registro de un archivo nuevo, la subida de fragmentos individuales y la solicitud de unión final.

package com.example.controller;

import com.example.util.GestorArchivos;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

@RestController
@RequestMapping("/api/upload")
public class FragmentosController {

    @Value("${app.storage.path}")
    private String rutaAlmacenamiento;

    /**
     * Endpoint para registrar un archivo nuevo. Crea un directorio con el hash proporcionado
     * y devuelve la cantidad de fragmentos ya existentes (para reanudación).
     */
    @PostMapping("/iniciar")
    public ResponseEntity<Map<String, Object>> iniciarCarga(@RequestBody Map<String, String> solicitud) {
        String hash = solicitud.get("hash");
        if (hash == null || hash.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("error", "El parámetro 'hash' es obligatorio."));
        }

        Path directorioArchivo = Paths.get(rutaAlmacenamiento, hash);
        try {
            Files.createDirectories(directorioArchivo);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body(Map.of("error", "No se pudo crear el directorio de trabajo."));
        }

        long fragmentosExistentes = 0;
        try {
            fragmentosExistentes = Files.list(directorioArchivo).count();
        } catch (IOException e) {
            // Ignorar, se asume 0 fragmentos.
        }

        Map<String, Object> respuesta = new HashMap<>();
        respuesta.put("fragmentosExistentes", fragmentosExistentes);
        return ResponseEntity.ok(respuesta);
    }

    /**
     * Endpoint para recibir y guardar un fragmento individual del archivo.
     */
    @PostMapping("/fragmento")
    public ResponseEntity<Map<String, String>> subirFragmento(
            @RequestParam("file") MultipartFile archivo,
            @RequestParam("hash") String hash,
            @RequestParam("nombreArchivo") String nombreArchivo) {
        
        if (hash == null || hash.trim().isEmpty() || nombreArchivo == null || nombreArchivo.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("error", "Parámetros 'hash' y 'nombreArchivo' son obligatorios."));
        }

        Path directorio = Paths.get(rutaAlmacenamiento, hash);
        Path destino = directorio.resolve(nombreArchivo);
        
        try (InputStream entrada = archivo.getInputStream();
             OutputStream salida = new BufferedOutputStream(new FileOutputStream(destino.toFile()))) {
            
            byte[] buffer = new byte[8192];
            int bytesLeidos;
            while ((bytesLeidos = entrada.read(buffer)) != -1) {
                salida.write(buffer, 0, bytesLeidos);
            }
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body(Map.of("error", "Fallo al guardar el fragmento."));
        }
        
        return ResponseEntity.ok(Map.of("mensaje", "Fragmento guardado correctamente."));
    }

    /**
     * Endpoint para solicitar la unión de todos los fragmentos en el archivo final.
     * Después de la unión, elimina el directorio de fragmentos.
     */
    @GetMapping("/completar")
    public ResponseEntity<Map<String, String>> completarCarga(
            @RequestParam("hash") String hash,
            @RequestParam("nombreFinal") String nombreFinal) {
        
        if (hash == null || hash.trim().isEmpty() || nombreFinal == null || nombreFinal.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("error", "Parámetros 'hash' y 'nombreFinal' son obligatorios."));
        }

        String directorioFragmentos = Paths.get(rutaAlmacenamiento, hash).toString();
        String archivoFinal = Paths.get(rutaAlmacenamiento, nombreFinal).toString();
        
        try {
            GestorArchivos.combinarArchivos(directorioFragmentos, archivoFinal);
            GestorArchivos.eliminarDirectorio(directorioFragmentos);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body(Map.of("error", "Fallo durante la unión de los fragmentos."));
        }
        
        return ResponseEntity.ok(Map.of("mensaje", "Archivo ensamblado correctamente.", "ruta", archivoFinal));
    }
}

Esta implementación sigue un flujo común en la carga fragmentada: registrar la sesión, subir partes del archivo en paralelo o secuencialmente, y finalizar uniéndolas en el servidor. Los nombres de archivo de los fragmentos deben seguir el patrón chunk_[índice].part para asegurar un orden correcto durante la unión.

Etiquetas: java Spring Boot API REST carga de archivos multipart

Publicado el 6-18 02:56