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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Integrating MinIO with Spring Boot: Full Guide to File Upload, Download, and Large‑File Handling

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-bucket

3. 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.

QR code
QR code
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.

Spring Bootfile uploadFile DownloadMiniolarge filesInstant Upload
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.