Harnessing Cloud Storage with MinIO in Spring Boot: A Student's Guide

Introduction

When building modern applications, efficient data storage and retrieval are critical components to consider. Today, we embark on a journey into the realm of MinIO, an open-source cloud storage solution, to enhance our Spring applications. Let's delve into the basics of MinIO and its seamless integration with the Spring framework.

What's MinIO, Anyway?

MinIO is an open-source object storage solution that provides an Amazon Web Services S3-compatible API and supports all core S3 features. MinIO is built to deploy anywhere and it's not only easy to use but also works super well with Spring – the Java framework we all know and love.

Installation

First, you'll need to install the MinIO server. Installing instructions tailored to your operating system are on the official MinIO documentation website.

After you can explore the available features using the public instance at https://play.min.io/minio/. You can use the following credentials :

  • Access Key : Q3AM3UQ867SPQQA43P2F

  • Secret Key : zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG

Integrating MinIO with Spring Boot

Now that we have our MinIO set up, we'll add it to our Spring project. First, add the minio dependency to your pom.xml file.

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>

Configuration

Although minio has its default configuration keys, we can change them for our personal use cases in the applications.properties file. Let's configure our application to connect to Minio public instance.


# Minio Host
spring.minio.url=https://play.min.io
# Minio Bucket name for your application
spring.minio.bucket=Test
# Minio access key (login)
spring.minio.access-key=Q3AM3UQ867SPQQA43P2F
# Minio secret key (password)
spring.minio.secret-key=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
# Minio default folder
spring.minio.default-folder=/
The minio library only manages one bucket for your application and it must already exist when the application starts.

Next, we make a configuration class that uses the above keys to create an return an instance of the minio client which we will use to upload and download our files.

//insert your package
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig{
    @Value("${spring.minio.access-key}")
    String accessKey;
    @Value("${spring.minio.secret-key}")
    String secretKey;
    @Value("${spring.minio.url}")
    String minioUrl;

    @Bean
    public MinioClient generateMinioClient(){
        try{
            return MinioClient.builder()
                    .endpoint(minioUrl)
                    .credentials(accessKey,secretKey)
                    .build();
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }
}

Uploading Files

Now, we can implement our file upload logic. To do this, we create a MinioAdapter class where we will store all our logic.

// your package name
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.messages.Bucket;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.*;
import java.util.List;

@Service
public class MinioAdapter {

    @Autowired
    MinioClient minioClient;

    @Value("${spring.minio.bucket}")
    String defaultBucketName;

    @Value("${spring.minio.default-folder}")
    String defaultBaseFolder;

    public void uploadFile(String name) {
        File file = new File("src/main/resources/" + name);
        try (FileInputStream iofs = new FileInputStream(file);){
            minioClient.uploadObject(UploadObjectArgs.builder()
                    .bucket(defaultBucketName)
                    .object(name)
                    .filename("src/main/resources/" + name)
                    .build());
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

    }


    @PostConstruct
    public void init() {
    }
}

The above code allows us to store a file in our resources folder in the default bucket we defined in our application.properties file.

Next, we implement our controller and create an endpoint to upload the files.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;



@RestController
@RequestMapping("/files")
public class MinioStorageController{
    @Autowired
    MinioAdapter minioAdapter;

    @PostMapping(path = "/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public @ResponseBody ResponseEntity<String> uploadFile(@RequestPart(value = "file", required = false) MultipartFile files) throws IOException {
        minioAdapter.uploadFile(files.getOriginalFilename());
        return new ResponseEntity<>("File uploaded successfully!", HttpStatus.OK);
    }
}

The code begins by injecting an instance of MinioAdapter, the dedicated service we created to handle our logic. A POST endpoint, reachable at "/files/upload" is established to handle incoming file uploads. The endpoint is designed to consume data in the form of multipart form data, a common method for transmitting files over HTTP.

The next step involves invoking the uploadFile method of the injected MinioAdapter. The filename of the uploaded file is extracted using files.getOriginalFilename() and passed to the MinioAdapter for handling.

Upon the successful execution of the file upload logic, a response entity is constructed. This entity encapsulates a success message — "File uploaded successfully!" — and an HTTP status of OK (200). This response is then dispatched back to the client, acknowledging the completion of the file upload process.

Downloading Files

Similar to how we upload files, we write our logic to download files from the bucket in the MinioAdapter class

public byte[] getFile(String key) {
        try {
            InputStream obj = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(defaultBucketName)
                            .object(defaultBaseFolder + "/" + key)
                            .build());

            byte[] content = IOUtils.toByteArray(obj);
            obj.close();
            return content;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

Next, we create an endpoint to get the file in our Controller.

@GetMapping(path = "/download")
    public ResponseEntity<ByteArrayResource> getFile(@RequestParam(value = "file") String file) throws IOException {
        byte[] data = minioAdapter.getFile(file);
        ByteArrayResource resource = new ByteArrayResource(data);

        return ResponseEntity
            .ok()
                .contentLength(data.length)
                .contentType(MediaType.APPLICATION_OCTET_STREAM) // Set the Content-Type
                .header("Content-Disposition", "attachment; filename=\"" + file + "\"; filename*=UTF-8''" + URLEncoder.encode(file, StandardCharsets.UTF_8.toString()))
                .body(resource);
    }

A GET endpoint, reachable at "/files/download" is established to handle file downloads. The next step involves invoking the getFile method of the injected MinioAdapter. Subsequently, a ByteArrayResource named resource is instantiated, wrapping the byte array obtained from the MinIO storage.

The core of the method lies in the construction of the response entity using ResponseEntity.ok(). This signifies a successful HTTP response with an HTTP status of OK (200). We then configure various aspects of the response:

  • .contentLength(data.length): Sets the content length of the response to the size of the byte array, ensuring accurate transmission.

  • .contentType(MediaType.APPLICATION_OCTET_STREAM): Specifies the content type as application/octet-stream, indicating that the response contains binary data.

  • .header("Content-Disposition", ...): Defines the Content-Disposition header, instructing the client's browser to treat the response as an attachment. The filename is included, facilitating a seamless download experience. Additionally, a UTF-8 encoded filename parameter is appended to support international characters in the filename.

Now, you can use Postman or any API testing tool to check the endpoint and verify if the files are being uploaded and downloaded.

Conclusion

In this article, we've explored how to use upload and download files from MinIO in your Spring applications. Now go, explore, and build amazing applications with the power of scalable and performant storage.
The code implementations are available on GitHub. If you have any questions or improvements, please let me know in the comments below.