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.