Fading Coder

One Final Commit for the Last Sprint

Home > Tools > Content

Deploying MinIO for Object Storage with Spring Boot Integration

Tools 1

Windows Environment Setup

MinIO is a lightweight, high-performance object storage system written in Go. It provides an S3-compatible API and is suitable for local deployments or on-premise infrastructure.

Local Installation

  1. Download the windows-amd64 executable from the official repository.
  2. Place minio.exe into a dedicated directory, such as C:\Program Files\MinIO.
  3. Define the environment variable MINIO_HOME pointing to this path.

To initialize the service, open an elevated command prompt, navigate to the installation directory, and execute:

cd "C:\Program Files\MinIO"
minio.exe server D:/data-store

The specified directory (D:/data-store) acts as the persistent data volume. Upon successful startup, the console outputs default credentials. Access the web interface at http://127.0.0.1:9000 using minioadmin/minioadmin. Once logged in, navigate to Buckets, click Create Bucket, and adjust the Access Policy under Manage if public read/write access is required.

Terminate the service by closing the terminal window or pressing Ctrl+C.

Spring Boot Configuration

Dependencies

Include the MinIO client library alongside its required HTTP transport dependency. Note that explicit version overrides may be necessary for newer OkHttp versions.

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.9</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

Application Properties

Configure the embedded Tomcat file limits and the storage endpoint credentials in application.yml:

server:
  port: 8080

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 100MB
      max-request-size: 250MB

storage:
  minio:
    endpoint: http://127.0.0.1:9000
    access-key: minioadmin
    secret-key: minioadmin

Configuration Class

Map the properties to a dedicated configuration component and instantiate the client bean:

import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "storage.minio")
public class MinIOConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;

    public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
    public void setAccessKey(String accessKey) { this.accessKey = accessKey; }
    public void setSecretKey(String secretKey) { this.secretKey = secretKey; }

    @Bean
    public MinioClient createClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

Core Storage Adapter

Consolidate all storage operations into a single utility class. The implementation replaces direct RuntimeException throws with custom business exceptions, standardizes builder patterns, and handles stream lengths correctly.

import io.minio.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@RequiredArgsConstructor
public class ObjectStorageService {

    private final MinioClient client;

    public void ensureBucketExists(String bucketName) {
        boolean exists;
        try {
            exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            log.error("Failed to check bucket existence", e);
            throw new StorageOperationException(e);
        }
        if (!exists) {
            try {
                client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            } catch (Exception e) {
                log.error("Failed to create bucket: {}", bucketName, e);
                throw new StorageOperationException(e);
            }
        }
    }

    public ObjectWriteResponse uploadFromMultipart(String bucketName, MultipartFile sourceFile, String targetKey, String mediaType) {
        try (InputStream contentStream = sourceFile.getInputStream()) {
            PutObjectArgs putArgs = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetKey)
                    .contentType(mediaType)
                    .stream(contentStream, -1, -1)
                    .build();
            return client.putObject(putArgs);
        } catch (Exception e) {
            throw new StorageOperationException("Upload failed for bucket: " + bucketName, e);
        }
    }

    public ObjectWriteResponse uploadFromFileSystem(String bucketName, String targetKey, String localFilePath) {
        try {
            UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetKey)
                    .filename(localFilePath)
                    .build();
            return client.uploadObject(uploadArgs);
        } catch (Exception e) {
            throw new StorageOperationException("Local file upload failed", e);
        }
    }

    public ObjectWriteResponse uploadFromRawStream(String bucketName, String targetKey, InputStream dataStream) {
        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetKey)
                    .stream(dataStream, -1, -1)
                    .build();
            return client.putObject(args);
        } catch (Exception e) {
            throw new StorageOperationException("Stream upload failed", e);
        }
    }

    public String generatePublicUrl(String bucketName, String targetKey, int validityMinutes) {
        try {
            GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(targetKey)
                    .expiry(validityMinutes, ChronoUnit.MINUTES)
                    .build();
            return client.getPresignedObjectUrl(urlArgs);
        } catch (Exception e) {
            throw new StorageOperationException("URL generation failed", e);
        }
    }

    public String generateSignedUrlExtended(String bucketName, String targetKey, long duration, TimeUnit timeUnit) {
        try {
            GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(targetKey)
                    .expiry(duration, timeUnit)
                    .build();
            return client.getPresignedObjectUrl(args);
        } catch (Exception e) {
            throw new StorageOperationException("Presigned URL creation error", e);
        }
    }

    public void createVirtualDirectory(String bucketName, String dirPath) {
        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(dirPath.endsWith("/") ? dirPath : dirPath + "/")
                    .stream(new ByteArrayInputStream(new byte[0]), 0, -1)
                    .build();
            client.putObject(args);
        } catch (Exception e) {
            throw new StorageOperationException("Directory creation failed", e);
        }
    }

    public boolean isObjectPresent(String bucketName, String targetKey) {
        try {
            client.statObject(StatObjectArgs.builder().bucket(bucketName).object(targetKey).build());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public boolean isVirtualFolderPresent(String bucketName, String folderPrefix) {
        try {
            Iterable<Result<Item>> objects = client.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(folderPrefix).recursive(false).build()
            );
            for (Result<Item> result : objects) {
                Item item = result.get();
                if (item.isDir() && item.objectName().startsWith(folderPrefix)) {
                    return true;
                }
            }
        } catch (Exception ignored) { /* Intentional */ }
        return false;
    }

    public StatObjectResponse fetchMetadata(String bucketName, String targetKey) {
        try {
            return client.statObject(StatObjectArgs.builder().bucket(bucketName).object(targetKey).build());
        } catch (Exception e) {
            throw new StorageOperationException("Metadata retrieval failed", e);
        }
    }

    public void deleteObject(String bucketName, String targetKey) {
        try {
            client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(targetKey).build());
        } catch (Exception e) {
            throw new StorageOperationException("Object deletion failed", e);
        }
    }

    public void deleteBucket(String bucketName) {
        try {
            client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            throw new StorageOperationException("Bucket removal failed", e);
        }
    }

    public static class StorageOperationException extends RuntimeException {
        public StorageOperationException(String message) { super(message); }
        public StorageOperationException(String message, Throwable cause) { super(message, cause); }
    }
}

REST Endpoint & Client Interface

Provide a simple HTML form to trigger uploads via a Spring MVC controller.

Client View

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>File Uploader</title></head>
<body>
<form action="/api/files/submit" method="post" enctype="multipart/form-data">
  <label>Select document:</label>
  <input type="file" name="document" required>
  <button type="submit">Transmit</button>
</form>
</body>
</html>

Server Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

@RestController
@RequestMapping("/api/files")
public class FileUploadController {

    private final ObjectStorageService storageService;

    @Autowired
    public FileUploadController(ObjectStorageService storageService) {
        this.storageService = storageService;
    }

    @PostMapping("/submit")
    public ResponseEntity<String> handleUpload(@RequestParam("document") MultipartFile uploadedPart) {
        String originalName = uploadedPart.getOriginalFilename();
        String extension = originalName != null ? originalName.substring(originalName.lastIndexOf(".")) : "";
        String uniqueKey = UUID.randomUUID().toString().replace("-", "") + extension;

        storageService.ensureBucketExists("user-uploads");
        storageService.uploadFromMultipart("user-uploads", uploadedPart, uniqueKey, "image/jpeg");
        
        String publicLink = storageService.generatePublicUrl("user-uploads", uniqueKey, 7);
        return ResponseEntity.ok(publicLink);
    }
}

Standard MIME Type Reference

When submitting forms or configuring storage buckets, mapping correct content types ensures proper browser rendering and server validation.

Category Content-Type Description
Text-based text/html Structured markup documents
Text-based text/plain Unformatted character data
Text-based text/xml, application/xml Extensible markup structures
Images image/jpeg Photographs and complex graphics
Images image/png Lossless raster images
Images image/gif Animated or indexed-color graphics
Documents application/pdf Portable document format
Documents application/json Lightweight data interchange
Documents application/octet-stream Generic binary streams
Forms application/x-www-form-urlencoded Default key-value encoding for HTML forms

Related Articles

Efficient Usage of HTTP Client in IntelliJ IDEA

IntelliJ IDEA incorporates a versatile HTTP client tool, enabling developres to interact with RESTful services and APIs effectively with in the editor. This functionality streamlines workflows, replac...

Installing CocoaPods on macOS Catalina (10.15) Using a User-Managed Ruby

System Ruby on macOS 10.15 frequently fails to build native gems required by CocoaPods (for example, ffi), leading to errors like: ERROR: Failed to build gem native extension checking for ffi.h... no...

Resolve PhpStorm "Interpreter is not specified or invalid" on WAMP (Windows)

Symptom PhpStorm displays: "Interpreter is not specified or invalid. Press ‘Fix’ to edit your project configuration." This occurs when the IDE cannot locate a valid PHP CLI executable or when the debu...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.