Integrating MinIO with Spring Boot: Full Guide to File Upload, Download, and Large‑File Handling
This article walks through the complete process of integrating MinIO into a Spring Boot project, covering why MinIO is chosen, environment setup, configuration, utility and controller implementation for simple and batch uploads, downloads, previews, large‑file multipart uploads, instant upload checks, and testing procedures.
Why Choose MinIO?
When selecting a file storage solution, we consider functionality, performance, cost, and scalability. MinIO stands out with rich S3‑compatible APIs, high performance, open‑source AGPL‑v3 licensing, easy installation via Docker, a simple web console, and strong security features such as encryption and access control.
Environment Preparation
1. Install MinIO
docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
-e "MINIO_ROOT_USER=minioadmin" \
-e "MINIO_ROOT_PASSWORD=minioadmin" \
minio/minio server /data --console-address ":9001"Access the console at http://localhost:9001 with username minioadmin and password minioadmin.
2. Create a Spring Boot Project
Generate a project via Spring Initializr and add the following dependencies:
Spring Web
Spring Boot DevTools
MinIO Client
Integrating MinIO
1. Add Dependency
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.5</version>
</dependency>2. Configure Connection
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: test-bucket3. Create MinIO Configuration Class
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("${minio.endpoint}") private String endpoint;
@Value("${minio.access-key}") private String accessKey;
@Value("${minio.secret-key}") private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}Utility Class (MinioUtil)
The utility encapsulates common operations such as bucket management, file upload/download, multipart upload, and hash generation.
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.*;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
public class MinioUtil {
@Autowired private MinioClient minioClient;
@Value("${minio.bucket-name}") private String defaultBucketName;
@SneakyThrows
public boolean bucketExists(String bucket) { return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); }
@SneakyThrows
public void makeBucket(String bucket) { if (!bucketExists(bucket)) minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); }
@SneakyThrows
public List<Bucket> listBuckets() { return minioClient.listBuckets(); }
@SneakyThrows
public Map<String,String> uploadFile(MultipartFile file, String bucket) {
if (file == null || file.isEmpty()) return null;
if (!bucketExists(bucket)) makeBucket(bucket);
String original = file.getOriginalFilename();
String name = UUID.randomUUID().toString() + original.substring(original.lastIndexOf('.'));
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(name)
.contentType(file.getContentType())
.stream(file.getInputStream(), file.getSize(), -1)
.build());
Map<String,String> map = new HashMap<>();
map.put("fileName", name);
map.put("originalFilename", original);
map.put("url", getObjectUrl(bucket, name, 7));
return map;
}
@SneakyThrows
public Map<String,String> uploadFile(MultipartFile file) { return uploadFile(file, defaultBucketName); }
@SneakyThrows
public List<Map<String,String>> uploadFiles(List<MultipartFile> files, String bucket) {
return files.stream()
.map(f -> uploadFile(f, bucket))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@SneakyThrows
public List<Map<String,String>> uploadFiles(List<MultipartFile> files) { return uploadFiles(files, defaultBucketName); }
@SneakyThrows
public InputStream downloadFile(String bucket, String object) { return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(object).build()); }
@SneakyThrows
public InputStream downloadFile(String object) { return downloadFile(defaultBucketName, object); }
@SneakyThrows
public String getObjectUrl(String bucket, String object, int expires) {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucket)
.object(object)
.expiry(expires, TimeUnit.DAYS)
.build());
}
@SneakyThrows
public String getObjectUrl(String object, int expires) { return getObjectUrl(defaultBucketName, object, expires); }
@SneakyThrows
public boolean objectExists(String bucket, String object) {
try { minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(object).build()); return true; }
catch (Exception e) { return false; }
}
@SneakyThrows
public boolean objectExists(String object) { return objectExists(defaultBucketName, object); }
@SneakyThrows
public List<Item> listObjects(String bucket) {
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucket).build());
List<Item> items = new ArrayList<>();
for (Result<Item> r : results) items.add(r.get());
return items;
}
@SneakyThrows
public List<Item> listObjects() { return listObjects(defaultBucketName); }
@SneakyThrows
public String createMultipartUpload(String bucket, String object) {
return minioClient.createMultipartUpload(CreateMultipartUploadArgs.builder().bucket(bucket).object(object).build()).result().uploadId();
}
@SneakyThrows
public String uploadPart(String bucket, String object, String uploadId, int partNumber, InputStream stream, long size) {
return minioClient.uploadPart(UploadPartArgs.builder()
.bucket(bucket)
.object(object)
.uploadId(uploadId)
.partNumber(partNumber)
.stream(stream, size, -1)
.build()).etag();
}
@SneakyThrows
public void completeMultipartUpload(String bucket, String object, String uploadId, List<String> etags) {
List<CompletePart> parts = new ArrayList<>();
for (int i = 0; i < etags.size(); i++) parts.add(new CompletePart(i + 1, etags.get(i)));
minioClient.completeMultipartUpload(CompleteMultipartUploadArgs.builder()
.bucket(bucket)
.object(object)
.uploadId(uploadId)
.parts(parts)
.build());
}
@SneakyThrows
public String generateFileHash(MultipartFile file) {
// Simplified hash: size + "-" + original filename. Replace with MD5/SHA‑1 in production.
return file.getSize() + "-" + file.getOriginalFilename();
}
}Controller (MinioController)
Provides REST endpoints for all file operations.
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;
@RestController
@RequestMapping("/api/minio")
public class MinioController {
@Autowired private MinioUtil minioUtil;
@PostMapping("/upload")
public ResponseEntity<Map<String,Object>> uploadFile(@RequestParam("file") MultipartFile file) {
Map<String,Object> result = new HashMap<>();
try {
Map<String,String> info = minioUtil.uploadFile(file);
if (info != null) {
result.put("code",200);
result.put("message","Upload successful");
result.put("data",info);
return ResponseEntity.ok(result);
} else {
result.put("code",500);
result.put("message","Upload failed");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
} catch (Exception e) {
result.put("code",500);
result.put("message","Upload exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/upload/batch")
public ResponseEntity<Map<String,Object>> uploadFiles(@RequestParam("files") List<MultipartFile> files) {
Map<String,Object> result = new HashMap<>();
try {
List<Map<String,String>> infos = minioUtil.uploadFiles(files);
result.put("code",200);
result.put("message","Batch upload successful");
result.put("data",infos);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code",500);
result.put("message","Batch upload exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@GetMapping("/download/{fileName}")
public ResponseEntity<byte[]> downloadFile(@PathVariable("fileName") String fileName) {
try {
InputStream in = minioUtil.downloadFile(fileName);
byte[] bytes = in.readAllBytes();
in.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", fileName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/preview/{fileName}")
public ResponseEntity<Map<String,Object>> previewFile(@PathVariable("fileName") String fileName) {
Map<String,Object> result = new HashMap<>();
try {
String url = minioUtil.getObjectUrl(fileName,1);
result.put("code",200);
result.put("message","URL retrieved");
result.put("url",url);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code",500);
result.put("message","Preview error: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@DeleteMapping("/delete/{fileName}")
public ResponseEntity<Map<String,Object>> deleteFile(@PathVariable("fileName") String fileName) {
Map<String,Object> result = new HashMap<>();
try {
minioUtil.deleteFile(fileName);
result.put("code",200);
result.put("message","Deletion successful");
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code",500);
result.put("message","Deletion exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@GetMapping("/list")
public ResponseEntity<Map<String,Object>> listFiles() {
Map<String,Object> result = new HashMap<>();
try {
List<Item> items = minioUtil.listObjects();
result.put("code",200);
result.put("message","List retrieved");
result.put("data",items);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code",500);
result.put("message","List error: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
// Multipart upload endpoints (init, upload part, complete) omitted for brevity but follow the same pattern.
@PostMapping("/check")
public ResponseEntity<Map<String,Object>> checkFile(@RequestParam("file") MultipartFile file) {
Map<String,Object> result = new HashMap<>();
try {
String hash = minioUtil.generateFileHash(file);
// In a real system, query DB/cache for existing hash.
boolean exists = false; // Simplified example.
result.put("code",200);
result.put("message","Check completed");
result.put("exists",exists);
result.put("fileHash",hash);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code",500);
result.put("message","Check exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
}Large File Multipart Upload and Instant Upload
1. Large File Multipart Upload
The client splits a big file into fixed‑size chunks (e.g., 1 MB), assigns a sequence number to each chunk, and uploads them sequentially. The server stores each chunk temporarily, records metadata (bucket, part number, ETag), and merges all parts once every chunk is received.
2. Instant Upload
Before uploading, the client computes a hash (MD5/SHA‑1) of the file and sends it to the server. The server checks whether a file with the same hash already exists; if it does, the server returns the existing file URL, avoiding a redundant upload.
Testing and Verification
Simple file upload: POST /api/minio/upload with a single file.
Batch upload: POST /api/minio/upload/batch with multiple files.
File download: GET /api/minio/download/{fileName}.
File preview: GET /api/minio/preview/{fileName} returns a presigned URL.
Large file multipart upload: use a front‑end library (e.g., WebUploader, Plupload) to call the multipart init, upload part, and complete endpoints.
Instant upload: upload a file, record its hash, then upload the same file again and verify that the server returns the existing URL without re‑uploading.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
