I'm trying to work out why a file upload to minio (S3 compliant document store) via a WebFlux endpoint doesn't complete; I always only get 4kb of the file into Minio.
My endpoint:
public Mono<ServerResponse> uploadFile(ServerRequest request) {
log.info("Uploading file...");
log.info("Content Type: {}", request.headers().contentType().orElse(MediaType.TEXT_PLAIN));
return request.body(BodyExtractors.toMultipartData())
.flatMap(map -> {
Map<String, Part> parts = map.toSingleValueMap();
return Mono.just((FilePart) parts.get("files"));
})
.flatMap(this::saveFile)
.flatMap(part -> ServerResponse.ok().body(BodyInserters.fromObject(part)));
}
private Mono<String> saveFile(FilePart part) {
return part.content().map(dataBuffer -> {
try {
log.info("Putting file {} to minio...", part.filename());
client.putObject("files", part.filename(), dataBuffer.asInputStream(), part.headers().getContentType().getType());
} catch(Exception e) {
log.error("Error storing file to minio", e);
return part.filename();
}
return part.filename();
}).next();
}
I'm pretty sure this is a blocking vs non-blocking issue but if I try and add a blockFirst()
call in I get an Exception saying it's not allowed at runtime.
Is there a way to stream the data effectively or is this a case that the Minio client is just not compatible with WebFlux?
I'm trying to post from a React component like so:
class DataUpload extends React.Component {
constructor(props) {
super(props);
this.state = {
fileURL: '',
};
this.handleUploadData = this.handleUploadData.bind(this);
}
handleUploadData = ev => {
ev.preventDefault()
const data = new FormData();
data.append('files', this.uploadInput.files[0]);
data.append('filename', this.fileName.value);
fetch('http://localhost:8080/requestor/upload', {
method: 'POST',
body: data,
}).then((response) => {
response.json().then((body) => {
this.setState({ fileURL: `http://localhost:8080/${body.file}`});
});
});
}
render() {
return (
<form onSubmit={this.handleUploadData}>
<div>
<input ref={(ref) => { this.uploadInput = ref; }} type="file" />
</div>
<div>
<input ref={(ref) => { this.fileName = ref; }} type="text" placeholder="Enter the desired name of the file" />
</div>
<br/>
<div><button>Upload</button></div>
</form>
);
}
}
export default DataUpload;
I always only get 4kb of the file into Minio
That's because spring chunks files into 4kb parts and you have to collect them all by yourself. You can do it like this:
request.body(BodyExtractors.toMultipartData())
.map(dataBuffers -> dataBuffers.get("files"))
.filter(Objects::nonNull)
//get the file name and pair it with it's "Part"
.map(partsList -> {
List<Pair<String, Part>> pairedList = new ArrayList<>();
for (Part part : partsList) {
String fileName = ((FilePart) part).filename();
pairedList.add(new Pair<>(fileName, part));
}
return pairedList;
})
.flux()
.flatMap(Flux::fromIterable)
//here we collect all of the file parts with the buffer operator and zip them with filename
.flatMap(partWithName -> Mono.zip(Mono.just(partWithName.getFirst()), partWithName.getSecond().content().buffer().single()))
.buffer()
.single()
.doOnNext(filePartsWithNames -> {
//here we have a list of all uploading file parts and their names
for (Tuple2<String, List<DataBuffer>> filePartsWithName : filePartsWithNames) {
String fileName = filePartsWithName.getT1();
List<DataBuffer> buffers = filePartsWithName.getT2();
System.out.println("Filename = " + fileName);
//"buffers" is a list of 4kb chunks of the files
for (DataBuffer buffer : buffers) {
System.out.println("Buffer size = " + buffer.readableByteCount());
//here you can use buffer.asInputStream() to read the file part and
//then save it on disk or do something else with it
}
}
})