SpringBoot集成Minio文件服务

官网中文文档

1、安装Minio

Docker安装Minio并挂载数据到本地磁盘

2、pom文件添加依赖

<!-- 操作minio的java客户端-->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.2.1</version>
</dependency>

依赖可以官方文档里找: https://docs.min.io/docs/java-client-quickstart-guide.html

注意:版本太高可能会因为版本不匹配导致启动报错

3、yml添加配置

#minio配置
minio:
  endpoint: http://localhost:9000 #MinIO服务所在地址
  accessKey: xxxxxx     #key就是docker初始化是设置的,密钥相同
  secretKey: xxxxxxxx   #访问的秘钥
  bucketName: xxxxx   #存储桶名称

4、创建minio的配置类

使用配置属性绑定进行参数绑定,并初始化一个minio client对象放入容器中。

@Slf4j
@Configuration
public class MinioConfig {

    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.bucketName}")
    private String bucketName;
    @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean(name = "minioClient")
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }

}

5、创建操作api的工具类

@Slf4j
public class MinioUtil {

    private static MinioClient minioClient;

    static {
        minioClient = SpringUtil.getBean("minioClient", MinioClient.class);
    }

    /**
     * 创建一个桶,当bucket已存在时直接跳过,不存在时创建
     * @param bucket
     * @return void
     * @author zhongyufeng
     * @date 2022/7/28 16:51
     */
    public static void createBucket(String bucket) throws Exception {
        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
        }
    }

    /**
     * 删除一个桶
     */
    public static void deleteBucket(String bucket) throws Exception {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
    }

    /**
     * 上传一个文件
     */
    public static void uploadFile(InputStream stream, String bucket, String objectName) throws Exception {
        minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
                .stream(stream, -1, 10485760).build());
    }

    /**
     * 列出所有的桶
     */
    public static List<String> listBuckets() throws Exception {
        List<Bucket> list = minioClient.listBuckets();
        return Optional.ofNullable(list).orElse(new ArrayList<>(0)).stream().map(Bucket::name).collect(Collectors.toList());
    }

    /**
     * 列出一个桶中的所有文件和目录
     */
    public static List<FileInfo> listFiles(String bucket) {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucket).recursive(true).build());

        List<FileInfo> infos = new ArrayList<>();
        results.forEach(r->{
            FileInfo info = new FileInfo();
            try {
                Item item = r.get();
                info.setFilename(item.objectName());
                info.setDirectory(item.isDir());
                infos.add(info);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return infos;
    }

    /**
     * 下载一个文件
     */
    public static InputStream download(String bucket, String objectName) throws Exception {
        InputStream stream = minioClient.getObject(
                GetObjectArgs.builder().bucket(bucket).object(objectName).build());
        return stream;
    }

    /**
     * 删除一个对象
     */
    public static void deleteObject(String bucket, String objectName) throws Exception {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
    }
    
    /**
     * 获取minio文件的下载或者预览地址
     * @param bucket
     * @param objectName
     * @return java.lang.String
     * @author zhongyufeng
     * @date 2022/7/28 18:43
     */
    public static String getPreviewFileUrl(String bucket, String objectName) throws Exception {
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucket).object(objectName).method(Method.GET).build());
    }
}

FileInfo

@Data
public class FileInfo {

    private String filename;

    private Boolean directory;
}

6、接口测试

@CrossOrigin
@RestController
@RequestMapping("/demo/minio")
@AllArgsConstructor
@Slf4j
public class MinioController {


    /**
     * 获取所有的桶名称
     * @param
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:19
     */
    @GetMapping("/listBuckets")
    public R<?> listBuckets() throws Exception {
        List<String> list = MinioUtil.listBuckets();
        return R.ok(list);
    }

    /**
     * 创建桶
     * @param bucketName 桶名称
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:20
     */
    @PutMapping("/createBucket/{bucketName}")
    public R<?> createBucket(@PathVariable("bucketName") String bucketName) {

        try {
            MinioUtil.createBucket(bucketName);
        } catch (Exception e) {
            log.error("创建桶失败===========>{}", ExceptionUtil.getMessage(e));
            return R.error("创建桶失败");
        }

        return R.ok();
    }

    /**
     * 删除桶
     * @param bucketName 桶名称
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:20
     */
    @DeleteMapping("/deleteBucket/{bucketName}")
    public R<?> deleteBucket(@PathVariable("bucketName") String bucketName) {

        try {
            MinioUtil.deleteBucket(bucketName);
        } catch (Exception e) {
            log.error("删除桶失败===========>{}", ExceptionUtil.getMessage(e));
            return R.error("删除桶失败");
        }

        return R.ok();
    }

    /**
     * 递归列出一个桶中的所有文件和目录
     * @param bucket 桶名称
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:24
     */
    @GetMapping("/listFiles/{bucket}")
    public R<?> listFiles(@PathVariable("bucket") String bucket){
        return R.ok(MinioUtil.listFiles(bucket));
    }

    /**
     * 上传文件
     * @param bucket 桶名称
     * @param url 文件路径
     * @param uploadFile 文件
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:20
     */
    @PostMapping("/uploadFile/{bucket}")
    public R<?> uploadFile(@PathVariable("bucket") String bucket,
                           @RequestParam("url") String url,
                           @RequestParam("file") MultipartFile uploadFile) {

        try {
            //先创建(桶不存在时才会创建,存在则不作操作)
            MinioUtil.createBucket(bucket);
            MinioUtil.uploadFile(uploadFile.getInputStream(), bucket, url + "/" + uploadFile.getOriginalFilename());
        } catch (Exception e) {
            log.error("上传文件失败===========>{}", ExceptionUtil.getMessage(e));
            return R.error("上传文件失败");
        }

        return R.ok();
    }


    /**
     * 下载一个文件
     * @param bucket 桶名称
     * @param objectName 文件名(全路径)
     * @param response
     * @return void
     * @author zhongyufeng
     * @date 2022/7/28 17:32
     */
    @GetMapping("/downloadFile/{bucket}")
    public void downloadFile(@PathVariable("bucket") String bucket,
                            @RequestParam("objectName") String objectName,
            HttpServletResponse response) throws Exception {
        InputStream stream = MinioUtil.download(bucket, objectName);
        ServletOutputStream output = response.getOutputStream();
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(objectName.substring(objectName.lastIndexOf("/") + 1), "UTF-8"));
        response.setContentType("application/octet-stream");
        response.setCharacterEncoding("UTF-8");
        IOUtils.copy(stream, output);
    }
    
    
    /**
     * 获取minio文件的下载或者预览地址
     * 取决于调用本方法的方法中的PutObjectOptions对象有没有设置contentType
     *
     * @param bucket: 桶名
     * @param objectName:  文件名
     */
    @GetMapping("/getPreviewFileUrl/{bucket}")
    public R<?> getPreviewFileUrl(@PathVariable("bucket") String bucket, @RequestParam("objectName") String objectName) {
        String previewFileUrl = null;
        try {
            previewFileUrl = MinioUtil.getPreviewFileUrl(bucket, objectName);
        } catch (Exception e) {
            log.error("获取文件预览地址失败======>{}", ExceptionUtil.getMessage(e));
            return R.error("获取文件预览地址失败");
        }

        return R.ok(previewFileUrl);
    }


    /**
     * 删除一个文件
     * @param bucket 桶名称
     * @param objectName 文件名称(全路径)
     * @return com.zyf.zboot.common.core.result.R<?>
     * @author zhongyufeng
     * @date 2022/7/28 17:35
     */
    @DeleteMapping("/deleteFile/{bucket}")
    @ResponseBody
    public R<?> deleteFile(@PathVariable("bucket") String bucket, @RequestParam("objectName") String objectName) throws Exception {
        MinioUtil.deleteObject(bucket, objectName);
        return R.ok();
    }
}

7、关于预览

如果桶的权限设置为public 则可以直接通过 http://IP:端口/bucket/objectName 预览/下载文件
但是如果桶的权限设置为private时,则无法直接访问,需要通过这个方法获取访问的url,并且有方式时间限制,默认为7天,最大时间也是7天

String url = minioClient.getPresignedObjectUrl(
                        GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucket)
                            .object(objectName)
                            .expiry(2, TimeUnit.HOURS)
                            .build());