The reactive way of Uploading and downloading files from Micronaut Using the project reactor.
So you might be wondering what is MINIO?
As per the official website "MinIO is a High-Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with the Amazon S3 cloud storage service. It can handle unstructured data such as photos, videos, log files, backups, and container images with the maximum supported object size of 5TB"
And Micronaut is a relatively new web framework that focuses on building modern scalable web applications, serverless functions, and microservices.
If you are coming from the spring boot ecosystem Micronaout has a very similar syntax. But it is lightweight, leaves less memory footprint, Supports AOT compilation, Has a quick start-up time. Micornout archives a low memory footprint by avoiding reflections by favoring dependency injection data at compile time.
So now let's jump into an example...!!
1) Strat the Minio using docker container
docker run \
-p 9000:9000 \
-p 9001:9001 \
-e "MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
quay.io/minio/minio server /data --console-address ":9001"
OR
2) Create the Micronaut project using https://micronaut.io/launch/
3) I have pasted my dependency maven pom for the reference
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ashrithgn</groupId>
<artifactId>minioexample</artifactId>
<version>0.1</version>
<packaging>${packaging}</packaging>
<parent>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-parent</artifactId>
<version>3.3.3</version>
</parent>
<properties>
<packaging>jar</packaging>
<jdk.version>17</jdk.version>
<release.version>17</release.version>
<micronaut.version>3.3.3</micronaut.version>
<exec.mainClass>com.ashrithgn.Application</exec.mainClass>
<micronaut.runtime>netty</micronaut.runtime>
</properties>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-groovy</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-spock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-runtime</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.reactor</groupId>
<artifactId>micronaut-reactor</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.reactor</groupId>
<artifactId>micronaut-reactor-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.micronaut.build</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Spec.*</include>
<include>**/*Test.*</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- Uncomment to enable incremental compilation -->
<!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->
<annotationProcessorPaths combine.self="override">
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-graal</artifactId>
<version>${micronaut.version}</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-validation</artifactId>
<version>${micronaut.version}</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>${micronaut.version}</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
<version>${micronaut.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amicronaut.processing.group=com.ashrithgn</arg>
<arg>-Amicronaut.processing.module=minioexample</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.13.0</version>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>removeStubs</goal>
<goal>addTestSources</goal>
<goal>generateTestStubs</goal>
<goal>compileTests</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<pluginRepositories>
</pluginRepositories>
</project>
4) Fill up the application.properties file
micronaut.application.name=demo
netty.default.allocator.max-order=3
micronaut.server.multipart.enabled=true
micronaut.http.client.max-content-length=819430400
micronaut.http.server.multipart.max-content-length=819430400
micronaut.http.server.max-content-length=819430400
micronaut.http.services.*.max-content-length=819430400
micronaut.server.multipart.max-file-size=419430400
#minio property
minio.url=http://127.0.0.1:9000
minio.client.id=AKIAIOSFODNN7EXAMPLE
minio.client.secret=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
minio.default.bucket=test-bucket
My property files have minio credentials and Micronaut's server config for enabling the multipart, increasing max-content and max-file-size.
5) Minio Factory
Ignore my naming conventions. The Minio Factory is a Singleton Class that builds the Minio client by reading configurations from a property file.
@Singleton
public class MinioFactory {
@Property(name = "minio.url")
String minioUrl;
@Property(name = "minio.client.id")
String accessKey;
@Property(name = "minio.client.secret")
String accessSecret;
public MinioClient getClient() {
try {
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.MINUTES)
.writeTimeout(10, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.MINUTES)
.build();
MinioClient client = MinioClient.builder()
.endpoint(minioUrl)
.httpClient(httpClient)
.credentials(accessKey, accessSecret)
.build();
return client;
} catch (
Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
6) Minio Adapter
This Minio Adapter class implements Minio's Client functions which are required like upload and download
I have Named this as an Adapter simple because this blindly translates input and output from one form to another, This can be Named as a Service or Wrapper.
package com.ashrithgn.components;
import com.ashrithgn.transferObject.UploadResponse;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.MediaType;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.messages.Bucket;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
@Singleton
@Slf4j
public class MinioAdapter {
@Inject
MinioFactory minioFactory;
@Property(name = "minio.default.bucket")
String bucketName;
@SneakyThrows
public Flux<Bucket> listBuckets() {
return Flux.fromStream(minioFactory.getClient().listBuckets().stream());
}
public Mono<UploadResponse> uploadFile(CompletedFileUpload file) {
return Mono.just(file).subscribeOn(Schedulers.boundedElastic()).map(f -> {
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.contentType(f.getContentType().orElse(MediaType.APPLICATION_OCTET_STREAM_TYPE).toString())
.bucket(bucketName)
.object(f.getFilename())
.stream(f.getInputStream(), f.getSize(), -1)
.build();
ObjectWriteResponse response = minioFactory.getClient().putObject(putObjectArgs);
return UploadResponse.builder().bucketName(response.bucket()).objectName(response.object()).build();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
});
}
@SneakyThrows
public Mono<InputStream> downloadFile(String fileName) {
CompletableFuture<InputStream> future = CompletableFuture.supplyAsync(() -> {
GetObjectArgs args = GetObjectArgs.builder().bucket(bucketName).object(fileName).build();
try {
GetObjectResponse response = minioFactory.getClient().getObject(args);
InputStream stream = new ByteArrayInputStream(response.readAllBytes());
return stream;
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
});
return Mono.fromFuture(future).subscribeOn(Schedulers.boundedElastic());
}
}
Also, I have Created the DTO
@Data
@Builder
@Introspected
public class UploadResponse {
private String objectName;
private String bucketName;
}
6) Controller
These Contain self-explanatory rest API which calls Minio Adapter class function
package com.ashrithgn.controller;
import com.ashrithgn.components.MinioAdapter;
import com.ashrithgn.components.MinioFactory;
import com.ashrithgn.transferObject.UploadResponse;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.minio.messages.Bucket;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@Controller("/test")
@Slf4j
public class FileController {
@Inject
MinioAdapter minioAdapter;
@Get()
@Produces(MediaType.APPLICATION_JSON)
public Flux<Bucket> healthCheck() {
log.info("test");
return minioAdapter.listBuckets();
}
@Post(value = "/upload", consumes = {MediaType.MULTIPART_FORM_DATA})
public Mono<UploadResponse> uploadFile(@Part CompletedFileUpload file) throws IOException {
return minioAdapter.uploadFile(file).log();
}
@Get(value = "/download")
public HttpResponse<Mono<InputStream>> downLoadFile(@QueryValue String fileName) throws IOException {
return HttpResponse.ok(minioAdapter.downloadFile(fileName))
.header("Content-type", "application/octet-stream")
.header("Content-disposition", "attachment; filename=\""+fileName+"\"");
}
}
Main Application Class for reference
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
Also read :