javaspring-bootfile-uploadreactive-programming

How to convert MultiPartFile to Mono<FilePart>


I have reactive API that takes Mono as an argument as show below

Mono<FileType> processAndReturnType(Mono<FilePart> partFlux)

I have to call this from another API which takes file as getType(MultipartFile partFile)

The calling api is non reactive and how can I convert MultipartFile to FilePart.

One of the way is below which works fine and but raises a security concern/ sonar issue as file is available in temp directory -

I am wondering what could be the best way to handle this. If I can directly stream. Any suggestion will help

package com.maersk.clm.naas.tenderfile.convertor;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.lang.NonNull;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

/**
 * Implementation of {@link FilePart} that wraps a {@link MultipartFile}.
 */
public class MultipartFilePart implements FilePart {

    private final MultipartFile file;

    public MultipartFilePart(MultipartFile file) {
        this.file = file;
    }

    @Override
    @NonNull
    public Flux<DataBuffer> content() {
        try {
            Path tempFile = Files.createTempFile("upload", "temp");
            file.transferTo(tempFile);
            return DataBufferUtils.read(tempFile, new DefaultDataBufferFactory(), (int) file.getSize());
        } catch (IOException e) {
            return Flux.error(e);
        }
    }

    @Override
    @NonNull
    public String filename() {
        return Objects.requireNonNull(file.getOriginalFilename());
    }

    @Override
    @NonNull
    public String name() {
        return file.getName();
    }

    @Override
    @NonNull
    public Mono<Void> transferTo(@NonNull Path dest) {
        return DataBufferUtils.write(content(), dest)
                .then()
                .publishOn(Schedulers.boundedElastic())
                .doFinally(signalType -> {
                    try {
                        Files.deleteIfExists(dest);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                });
    }

    @Override
    @NonNull
    public HttpHeaders headers() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType(Objects.requireNonNull(file.getContentType())));
        return headers;
    }
}

I tried different ways to convert but couldnt any better solution


Solution

  • You can do without the temp file. MultipartFile has two other ways to get to its contents: getBytes() returns a byte[] and getInputStream() returns an InputStream. You can use both to get a Flux.

    DataBufferUtils has method readInputStream that's easy to use:

    return DataBufferUtils.readInputStream(
            file::getInputStream,
            new DefaultDataBufferFactory(),
            (int) file.getSize());
    

    With getBytes() you can use the factory directly:

    return Flux.just(new DefaultDataBufferFactory().wrap(file.getBytes());
    

    Out of these 3 options I'd go for the readInputStream one. The temp file involves disk I/O, and the file needs to be deleted, something you forgot to do. The byte[] option needs to read the entire contents to memory synchronously before it can be wrapped. The readInputStream option on the other hand can even work with a smaller buffer size.


    Once you get this working I'd see if it's possible to create a reusable DataBufferFactory. DefaultDataBufferFactory.sharedInstance may be an option.