Integrating Spring Boot with Minio for Direct Client Uploads Using Presigned Policies

This article demonstrates how to integrate Spring Boot with Minio, covering two upload strategies—backend‑mediated storage and direct client uploads with presigned credentials—while providing complete configuration, Java code, and a Vue‑based frontend example that includes chunked, instant, and resumable upload techniques.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Integrating Spring Boot with Minio for Direct Client Uploads Using Presigned Policies

Hello everyone, I am Chen.

Environment Preparation

Deployed Minio instance: http://mylocalhost:9001

Spring Boot Integration with Minio

A quick overview of the integration steps.

Add Minio Dependency

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

Define Configuration Properties

# application.yml
minio:
  endpoint: http://mylocalhost:9001
  accessKey: minio
  secretKey: minio123
  bucket: demo

Define Property Class

@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
    /** Object storage service URL */
    private String endpoint;
    /** Access key (user ID) */
    private String accessKey;
    /** Secret key (password) */
    private String secretKey;
    /** Default bucket */
    private String bucket;
    // getters and setters omitted
    ...
}

Define Minio Configuration Class

@Configuration
public class MinioConfig {
    @Bean
    public MinioClient minioClient(MinioProperties properties) {
        try {
            MinioClient.Builder builder = MinioClient.builder();
            builder.endpoint(properties.getEndpoint());
            if (StringUtils.hasLength(properties.getAccessKey()) && StringUtils.hasLength(properties.getSecretKey())) {
                builder.credentials(properties.getAccessKey(), properties.getSecretKey());
            }
            return builder.build();
        } catch (Exception e) {
            return null;
        }
    }
}

Start the Spring Boot service after adding the above beans.

Upload Credential Endpoint

Write a controller method that returns a presigned upload policy.

@RequestMapping(value = "/presign", method = {RequestMethod.POST})
public Map<String, String> presign(@RequestBody PresignParam presignParam) {
    if (StringUtils.isEmpty(presignParam.getBucket())) {
        presignParam.setBucket("demo");
    }
    if (StringUtils.isEmpty(presignParam.getFilename())) {
        presignParam.setFilename(UUID.randomUUID().toString());
    }
    ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(10);
    PostPolicy policy = new PostPolicy(presignParam.getBucket(), presignParam.getFilename(), expirationDate);
    try {
        final Map<String, String> map = minioClient.presignedPostPolicy(policy);
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " = " + entry.getValue());
        }
        return map;
    } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return null;
}

The response contains fields such as bucket, x-amz-date, x-amz-signature, key, x-amz-algorithm, x-amz-credential, and policy, which the frontend uses to upload directly to Minio.

Frontend Upload Using the Credential

uploadFile(file, policy) {
    console.log("Preparing to upload file:");
    console.log("file: " + file);
    console.log("policy: " + policy);
    var formData = new FormData();
    formData.append('file', file);
    formData.append('key', policy['key']);
    formData.append('x-amz-algorithm', policy['x-amz-algorithm']);
    formData.append('x-amz-credential', policy['x-amz-credential']);
    formData.append('x-amz-signature', policy['x-amz-signature']);
    formData.append('x-amz-date', policy['x-amz-date']);
    formData.append('policy', policy['policy']);
    return new Promise((resolve, reject) => {
        $.ajax({
            method: 'POST',
            url: 'http://mylocalhost:9001/' + policy['bucket'],
            data: formData,
            dataType: 'json',
            contentType: false,
            processData: false,
            xhr: function () {
                var xhr = $.ajaxSettings.xhr();
                if (xhr.upload) {
                    xhr.upload.addEventListener('progress', function (e) {
                        var percentage = parseInt(e.loaded / e.total * 100);
                        vm.uploadResult = percentage + "%" + ":" + policy['key'];
                    }, false);
                }
                return xhr;
            },
            success: function (result) {
                vm.uploadResult = 'File uploaded successfully: ' + policy['key'];
                resolve(result);
            },
            error: function (e) {
                reject();
            }
        });
    });
}

Chunked Upload, Instant Upload, and Resume Upload

Chunked Upload

Large files can be split into smaller chunks (e.g., a 100 MB file into ten 10 MB parts) to reduce per‑node pressure in a Minio cluster and to enable multi‑threaded uploading.

Instant Upload

Before uploading, compute the file’s MD5 hash (e.g., 3cc1f3c3c2d1a29ecf60ffad4de278c7) and concatenate it with the filename. If the backend detects that a file with the same hash already exists, it can skip credential generation and report the file as already uploaded.

Resume Upload

By combining chunked upload and instant‑upload logic, a client can resume an interrupted upload: already‑uploaded chunks are skipped, and only the missing parts are sent.

File Merge

After all chunks are uploaded to a temporary bucket (e.g., slice), the backend merges them into the final object.

@GetMapping("/compose")
public void merge() {
    List<ComposeSource> sources = new ArrayList<>();
    sources.add(ComposeSource.builder().bucket("slice").object("0寂寞的季节.mp4").build());
    sources.add(ComposeSource.builder().bucket("slice").object("1寂寞的季节.mp4").build());
    sources.add(ComposeSource.builder().bucket("slice").object("2寂寞的季节.mp4").build());
    ComposeObjectArgs args = ComposeObjectArgs.builder()
        .bucket("demo")
        .object("寂寞的季节.mp4")
        .sources(sources)
        .build();
    try {
        minioClient.composeObject(args);
    } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
}

Frontend Example Code

A minimal single‑page application (Vue 2 + jQuery) that obtains a presigned policy, uploads files, and demonstrates chunked upload, merge, and progress reporting.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Minio Test</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<div id="app">
    <h1>{{title}}</h1>
    <form @submit.prevent="getPolicyForm">
        <label>Bucket<input type="text" v-model="policyParams.bucket"/></label><br/>
        <label>Filename<input type="text" v-model="policyParams.filename"/></label><br/>
        <button type="submit">Get Upload Credential</button>
    </form>
    <form @submit.prevent="uploadFileForm" v-show="policy != null">
        <label>File<input type="file" @change="fileChange"/></label><br/>
        <button type="submit" v-show="file != null">Upload File</button>
    </form>
    <div v-show="file != null">
        <button @click="sliceEvent">Test Chunked Upload</button>
        <button @click="sliceComposeEvent">Merge Chunks</button>
    </div>
    <p>{{uploadResult}}</p>
    <ul>
        <li v-for="(item, index) in sliceUploadResult" :key="index">{{item}}</li>
    </ul>
</div>
<script>
var vm = new Vue({
    el: "#app",
    data() {
        return {
            title: "Minio Test",
            policyParams: { bucket: null, filename: null },
            policy: null,
            file: null,
            uploadResult: null,
            sliceUploadResult: null,
            // other state omitted for brevity
        };
    },
    methods: {
        getPolicyForm() {
            this.policyParams.bucket = "demo";
            this.policyParams.filename = "寂寞的季节.mp4";
            this.requestPolicy(this.policyParams);
        },
        requestPolicy(params) {
            return new Promise((resolve, reject) => {
                $.ajax({
                    type: "POST",
                    url: "http://localhost:8888/presign",
                    contentType: "application/json",
                    data: JSON.stringify(params),
                    success: result => { console.log(result); vm.policy = result; resolve(result); },
                    error: () => reject()
                });
            });
        },
        fileChange(event) { this.file = event.target.files[0]; },
        uploadFileForm() { this.uploadFile(this.file, this.policy); },
        // uploadFile, sliceEvent, sliceComposeEvent, calculateMD5 implementations omitted for brevity
    }
});
</script>
</body>
</html>

Final Note

If this article helped you, please like, watch, share, and bookmark. Your support keeps the content coming!

Join my Knowledge Planet for ¥199 to access premium projects such as Spring full‑stack practice, massive data sharding, DDD micro‑services, and more.

Follow the public account "码猿技术专栏" and reply with "加群" to join the discussion group.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentSpring Bootfile uploadMinioPresigned Upload
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.