Integrating Spring Boot with Minio: Direct Upload, Presigned URLs, Chunked Upload and File Merging
This article demonstrates how to integrate Spring Boot with Minio, covering two upload strategies (backend‑mediated and presigned‑URL direct upload), environment setup, Maven dependency, configuration classes, generating upload credentials, front‑end upload logic, chunked/instant/resumable uploads, and merging file parts.
Overview
Spring Boot can integrate with Minio to provide object storage capabilities. Two main upload approaches are discussed: uploading files to the backend which then stores them in Minio, and obtaining a presigned upload credential from the backend to upload directly to Minio.
Upload Approaches
1. Backend‑mediated upload – The backend receives the file, performs authentication, permission checks, and can add business logic such as thumbnail generation. Drawbacks include higher latency, increased backend resource usage, and a single point of failure.
2. Direct upload with presigned credential – The backend generates a time‑limited upload token, allowing the front‑end to upload directly to Minio while still enforcing basic access control.
Environment Preparation
Minio instance running at http://mylocalhost:9001 .
Spring Boot Integration
Dependency
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>application.yml
# application.yml
minio:
endpoint: http://mylocalhost:9001
accessKey: minio
secretKey: minio123
bucket: demoProperties class
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
// ... getters & setters
}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;
}
}
}Generating Upload Credential (Presign)
Controller endpoint that returns a map of fields required for a POST upload to Minio.
@RequestMapping(value = "/presign", method = {RequestMethod.POST})
public Map
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 {
Map
map = minioClient.presignedPostPolicy(policy);
return map;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}The returned fields include bucket , x-amz-date , x-amz-signature , key , x-amz-algorithm , x-amz-credential and policy .
Front‑End Upload Logic
Using the obtained policy, the front‑end builds a FormData object and performs a POST request to Minio.
uploadFile(file, 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,
contentType: false,
processData: false,
xhr: function () { var xhr = $.ajaxSettings.xhr(); if (xhr.upload) { xhr.upload.addEventListener('progress', function (e) { var percent = parseInt(e.loaded / e.total * 100); vm.uploadResult = percent + "% :" + policy['key']; }, false); } return xhr; },
success: function (result) { vm.uploadResult = 'File uploaded successfully: ' + policy['key']; resolve(result); },
error: function () { reject(); }
});
});
}Chunked / Instant / Resumable Upload
For large files, the file can be split into 5 MB chunks, each uploaded with its own presigned credential. MD5 of each chunk can be calculated to support instant upload (skip already existing chunks). If a network interruption occurs, remaining chunks are uploaded and later merged.
Chunk splitting example
var chunkSize = 5 * 1024 * 1024;
var totalChunk = Math.ceil(file.size / chunkSize);
var chunks = [];
for (var i = 0; i < totalChunk; i++) {
var start = i * chunkSize;
var end = Math.min(file.size, start + chunkSize);
chunks.push(file.slice(start, end));
}Each chunk obtains a presigned policy via /presign and is uploaded with the same uploadFile function. After all chunks are uploaded, a compose request merges them.
Merge Endpoint
@GetMapping("/compose")
public void merge() {
List
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 (Exception e) { e.printStackTrace(); }
}Complete Front‑End Example
A minimal single‑page Vue + jQuery demo combines the above steps: obtaining a policy, uploading a file, performing chunked upload, and invoking the merge API.
<!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">
... (Vue component code omitted for brevity) ...
</div>
<script>
// Vue instance with methods getPolicyForm, uploadFileForm, sliceEvent, sliceComposeEvent, calculateMD5, etc.
</script>
</body>
</html>The article ends with a brief promotional note encouraging readers to share the content.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.