The reactive way of Uploading and downloading files from Micronaut Using the project reactor.

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

MINIO: Getting Started With Ubuntu 18.04
Minio is another object store just like AWS S3, This is self hosted so easily can be used in private cloud. and it is open source so its absolute free use and modify. this one of the fastest growing self managed object store out there. so when it comes to

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 :

Uploading & Download files from MINIO using spring web flux.
Simple Guide Reactive way to Upload and download a file from minio object store using Spring boot’s Web flux. also commonly known as reactive SB
Spring boot: uploading and downloading file from Minio object store
If you have landed on this page means either your working with spring boot and trying to upload an file to a privately hosted minio object store server. so we know spring boot has gained popularity for its simple approach to bootstrap an application in very less time, Also minio
File Upload And Download Inside Micronaut Http controller Java.
This article explains how to upload and download a file from a Micronauts HTTP controller. on Netty server. Also helps to override the default server config.