Angular y Spring. Múltiple subida de archivos (Backend) con Java Spring Boot

Angular+SpringBoot

En este tutorial vamos a realizar el backend en Spring boot de un proyecto para subir múltiples/varios archivos MultipartFile[]. Si quieres ver la parte frontend da clic aquí

El resultado que vamos a obtener con este proyecto mas la parte de Front en Angular es este:

resultadoArchivosAngularSpring

Nuestro archivos se van a guardar en una carpeta que definamos en nuestro backend


Creación del proyecto en Spring Initializr.

Nos dirigimos a la página de Spring Initializr donde vamos a crear un proyecto con la siguiente estructura:

spring

Presionamos generate y una vez descargado, lo vamos a abrir con nuestro IDE preferido, yo voy a usar IntelliJ IDEA

Creación de estructura del proyecto

Creamos los siguientes paquetes y clases:

estructuraSpring
  • Controller –> Para exponer nuestros endpoints.
  • Exception –> Para manejar la excepción de tamaño excedido
  • Message –> Modelo del mensaje que vamos a devolver en las respuestas
  • Model –> Modelo del archivo
  • Service -> Interfaz donde vamos a declarar los métodos a implementar, InterfazImp es ya la implementación de la lógica de los métodos de la interfaz

Creación del modelo del archivo

Paquete model

public class FileModel {

    private String name;
    private String url;

    public FileModel() {
    }

    public FileModel(String name, String url) {
        this.name = name;
        this.url = url;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

Creación de la Interfaz de Servicio

Paquete service

import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Path;
import java.util.stream.Stream;

public interface FileService {

    /*
    Metodo para crear la carpeta donde vamos a guardar los archivos
     */
    public void init();

    /*
    Metodo para guardar los archivos
     */
    public void save(MultipartFile file);

    /*
    Metodo para cargar un archivo
     */
    public Resource load(String filename);

    /*
    Metodo para borrar todos los archivos cada vez que se inicie el servidor
     */
    public void deleteAll();

    /*
    Metodo para Cargar todos los archivos
     */
    public Stream<Path> loadAll();

    /*
    Metodo para Borrar un archivo
     */
    public String deleteFile(String filename);
}

Implementación de la interfaz

Paquete Service

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

@Service
public class FileServiceImp implements FileService {
  	//Nombre de la carpeta donde vamos a almacenar los archivos
    //Se crea a nivel de raiz la carpeta
    private final Path root = Paths.get("uploads");

    @Override
    public void init() {
        try {
            Files.createDirectory(root);
        } catch (IOException e) {
            throw new RuntimeException("No se puede inicializar la carpeta uploads");
        }
    }

    @Override
    public void save(MultipartFile file) {
        try {
            //copy (que queremos copiar, a donde queremos copiar)
            Files.copy(file.getInputStream(), 
                       this.root.resolve(file.getOriginalFilename()));
        } catch (IOException e) {
            throw new RuntimeException("No se puede guardar el archivo. Error " + e.getMessage());
        }
    }

    @Override
    public Resource load(String filename) {
        try {
            Path file = root.resolve(filename);
            Resource resource = new UrlResource(file.toUri());

            if(resource.exists() || resource.isReadable()){
                return resource;
            }else{
                throw new RuntimeException("No se puede leer el archivo ");
            }

        }catch (MalformedURLException e){
            throw new RuntimeException("Error: " + e.getMessage());
        }
    }

    @Override
    public void deleteAll() {
        FileSystemUtils.deleteRecursively(root.toFile());
    }

    @Override
    public Stream<Path> loadAll(){
        //Files.walk recorre nuestras carpetas (uploads) buscando los archivos
        // el 1 es la profundidad o nivel que queremos recorrer
        // :: Referencias a metodos
        // Relativize sirve para crear una ruta relativa entre la ruta dada y esta ruta
        try{
            return Files.walk(this.root,1).filter(path -> !path.equals(this.root))
                    .map(this.root::relativize);
        }catch (RuntimeException | IOException e){
            throw new RuntimeException("No se pueden cargar los archivos ");
        }
    }

    @Override
    public String deleteFile(String filename){
        try {
            Boolean delete = Files.deleteIfExists(this.root.resolve(filename));
                return "Borrado";
        }catch (IOException e){
            e.printStackTrace();
            return "Error Borrando ";
        }
    }


}

Modelo de respuesta del mensaje

Paquete message

public class FileMessage {
    private String message;

    public FileMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Creación del controlador

Paquete controller

package com.vmanuelpm.controller;

import com.vmanuelpm.message.FileMessage;
import com.vmanuelpm.model.FileModel;
import com.vmanuelpm.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Controller
@CrossOrigin("*")
public class FileController {
	//Inyectamos el servicio
    @Autowired
    FileService fileService;

    @PostMapping("/upload")
    public ResponseEntity<FileMessage> uploadFiles(@RequestParam("files")MultipartFile[] files){
        String message = "";
        try{
            List<String> fileNames = new ArrayList<>();

            Arrays.asList(files).stream().forEach(file->{
                fileService.save(file);
                fileNames.add(file.getOriginalFilename());
            });

            message = "Se subieron los archivos correctamente " + fileNames;
            return ResponseEntity.status(HttpStatus.OK).body(new FileMessage(message));
        }catch (Exception e){
            message = "Fallo al subir los archivos";
            return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new FileMessage(message));
        }
    }

    @GetMapping("/files")
    public ResponseEntity<List<FileModel>> getFiles(){
        List<FileModel> fileInfos = fileService.loadAll().map(path -> {
          String filename = path.getFileName().toString();
          String url = MvcUriComponentsBuilder.fromMethodName(FileController.class, "getFile",
                  path.getFileName().toString()).build().toString();
          return new FileModel(filename, url);
        }).collect(Collectors.toList());

        return ResponseEntity.status(HttpStatus.OK).body(fileInfos);
    }


    @GetMapping("/files/{filename:.+}")
    public ResponseEntity<Resource> getFile(@PathVariable String filename){
        Resource file = fileService.load(filename);
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\""+file.getFilename() + "\"").body(file);
    }

    @GetMapping("/delete/{filename:.+}")
    public ResponseEntity<FileMessage> deleteFile(@PathVariable String filename) {
        String message = "";
        try {
            message = fileService.deleteFile(filename);
            return ResponseEntity.status(HttpStatus.OK).body(new FileMessage(message));
        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new FileMessage(message));
        }
    }

}
  • @CrossOrigin(*) esta configurado de esta manera para permitir que apunten a nuestros endpoint desde cualquier origen.
  • Importante colocar la anotación @Controller para indicarle a Spring que es un controlador
  • @Autowired es para indicar que vamos a realizar una inyección de dependencias.
  • @GetMapping y @PostMapping son el tipo de petición HTTP que indicamos

Configuración del tamaño máximo del Multipart

En el archivo application.properties vamos a colocar lo siguiente:

#Maximo del archivo para cada request
spring.servlet.multipart.max-file-size=500KB

#Maximo del multipart
spring.servlet.multipart.max-request-size=500KB

Creación de la excepción de tamaño máximo excedido

Paquete exception

//este import hace referencia al modelo del mensaje
import com.vmanuelpm.message.FileMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@ControllerAdvice
public class FileUploadException {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<FileMessage> maxSizeException(MaxUploadSizeExceededException exc){
        return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
                .body(new FileMessage("Uno o mas archivos exceden el tamaño maximo"));
    }

}

En caso de que el tamaño máximo sea excedido cuando realicen una petición se va a dispara esta excepción MaxUploadSizeExceededException

Iniciar la creación del storage

El metodo init() creado anteriormente en la interfaz del servicio nos permite crear nuestro storage, en este caso la carpeta “uploads” en la raíz del proyecto, además cada vez que hagamos un bootRun se limpia el storage con el método deleteAll(), en el archivo main de nuestro proyecto hacemos lo siguiente

import com.vmanuelpm.service.FileService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@SpringBootApplication
public class UploadFilesApplication  implements CommandLineRunner {

	@Resource
	FileService fileService;

	public static void main(String[] args) {
		SpringApplication.run(UploadFilesApplication.class, args);
	}

	@Override
	public void run(String... arg) throws Exception{
		fileService.deleteAll();
		fileService.init();
	}

  /* En caso que tenga problema con los cors agregar este Bean */
	@Bean
	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST","PUT", "DELETE");
			}
		};
	}

}

IMPORTANTE no olvidar implementar CommandLineRunner ya que nos permite ejecutar código después de que la aplicación de Spring inicia.

Probando los servicios

Primero construimos nuestra aplicación:

Luego corremos la aplicación:

Correr aplicación por medio de comando:

./gradlew bootRun

Utilizamos Postman para probar nuestros servicios:

postman
Subida de multiples archivos
Resultados de subir los archivos
Obtener todos los archivos
Borrar archivo por nombre

En un navegador para obtener el archivo coloca el siguiente endpoint

http://localhost:8080/files/firma.png

Dejo el link al repositorio de este proyecto para que lo puedas ver si tienes dudas.

Este es el tutorial en video

Si quieres realizar la parte Front da clic aquí


2 Replies to “Angular y Spring. Múltiple subida de archivos (Backend) con Java Spring Boot”

Leave a Reply

Your email address will not be published.