【SpringBoot框架篇】25.集成Minio实现文件的分布式对象存储

1.简介

是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等,相对于FastDFS部署起来要轻量很多。

中文文档: http://docs.minio.org.cn/docs/master/distributed-minio-quickstart-guide

2.安装使用

2.1.下载二进制文件安装

2.1.1 单机部署

# 下载 minio
wget https://dl.min.io/server/minio/release/linux-amd64/minio
# 添加可执行权限
chmod +x minio
# 设置登录minio的 access key
export MINIO_ACCESS_KEY=minioadmin
# 设置登录minio的 secret key
export MINIO_SECRET_KEY=minioadmin
# 启动 minio 并设置文件存储的路径为/data目录,如果需要退出命令行程序还在需要在最前面加上 nohup 命令
 ./minio server /data

启动后可以看到如下信息
在这里插入图片描述

  • 控制台(页面)访问地址: http://192.168.94.128:37788/
  • API(SDK)访问地址: http://192.168.94.128:9000/
    在这里插入图片描述
    在这里插入图片描述

2.1.2.集群安装

参考:http://docs.minio.org.cn/docs/master/distributed-minio-quickstart-guide

2.1.2.1 创建存储路径

创建minio文件存储路径

mkdir -p /data/minio

注意事项

  • 新创建的目录需要挂载到磁盘下要不然启动会报错
  • 分布式Minio里所有的节点需要有同样的access秘钥和secret秘钥,这样这些节点才能建立联接。为了实现这个,你需要在执行minio server命令之前,先将access秘钥和secret秘钥export成环境变量。
  • 分布式Minio使用的磁盘里必须是干净的,里面没有数据。
  • 分布式Minio里的节点时间差不能超过3秒,你可以使用NTP 来保证时间一致。

注意:需要将新建的目录挂在到对应的磁盘下,磁盘不挂载好,集群启动会报错 Waiting for a minimum of 2 disks to come online :找不到磁盘:

mount /dev/sda1  /data

查看是否挂载成功

lsblk

在这里插入图片描述

2.1.2.2.创建集群启动脚本
  • 集群环境建议把MINIO_ACCESS_KEY和MINIO_SECRET_KEY换成MINIO_ROOT_USER和MINIO_ROOT_PASSWORD
  • 需要把下面的ip地址换成你部署节点的
vi minio-run.sh
#!/bin/bash
export MINIO_ROOT_USER=minioadmin
export MINIO_ROOT_PASSWORD=minioadmin
/usr/local/minio/minio  server \
 http://192.168.94.128/data/minio \
 http://192.168.94.129/data/minio \
 http://192.168.94.131/data/minio \
 http://192.168.94.132/data/minio
2.1.2.3.分别启动4个节点
./minio-run.sh

在这里插入图片描述

访问任意一个节点的控制台可以查看到集群信息,表示集群环境搭建成功了。
在这里插入图片描述

2.2.使用docker安装

创建挂载目录

mkdir -p /docker/minio/data  /docker/minio/config

配置ip4转发

vim /etc/sysctl.conf
#配置转发
net.ipv4.ip_forward=1

#重启服务,让配置生效
systemctl restart network
#查看是否成功,如果返回为“net.ipv4.ip_forward = 1”则表示成功
sysctl net.ipv4.ip_forward

构建并运行容器

docker run -p 9000:9000 -p 9001:9001 --name minio \
-it -d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=minioadmin" \
-v /docker/minio/data:/data \
-v /docker/minio/config:/root/.minio \
minio/minio server /data \
--console-address ":9001" --address ":9090"

控制台用9001端口, API用9000端口
在这里插入图片描述

3.实战

3.1.引入依赖

      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
       <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.3.4</version>
            <exclusions>
                <exclusion>
                    <groupId>com.squareup.okhttp3</groupId>
                    <artifactId>okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
         <!--内置的okhttp版本太低会在上传文件的时候报错,所以需要导入高版本的okhttp-->
          <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okio</groupId>
            <artifactId>okio</artifactId>
            <version>2.8.0</version>
        </dependency>

3.2.配置Minio连接信息

server:
  port: 8025
minio:
  #minio部署的机器ip地址
  endpoint: 192.168.94.128
  #minio使用的端口
  port: 9000
  #唯一标识的账户
  accessKey: minioadmin
  #账户的密码
  secretKey: minioadmin
  #是否使用https
  secure: false
  #测试使用的桶名称
  defaultBucketName: test

3.3.编辑Minio配置类

@Configuration
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig {

	/**
	 * minio部署的机器ip地址
	 */
	private String endpoint;

	/**
	 * minio使用的端口
	 */
	private Integer port;

	/**
	 *唯一标识的账户
	 */
	private String accessKey;

	/**
	 * 账户的密码
	 */
	private String secretKey;

	/**
	 * 如果是true,则用的是https而不是http,默认值是true
	 */
	private Boolean secure;

	/**
	 * 默认使用的桶名称
	 */
	private String defaultBucketName;
	
	/**
	 * 对象交给spring管理
	 */
	@Bean
	public MinioClient getMinioClient() {
		MinioClient minioClient = MinioClient.builder()
				.endpoint(endpoint , port , secure)
				.credentials(accessKey, secretKey)
				.build();
		return minioClient;
	}
}

3.4 编写Mino工具类

@Component
@Slf4j
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    /**
     * 获取当前日期字符串格式
     * @return 2021/12/5
     */
    public String getDatePath() {
        LocalDateTime now = LocalDateTime.now();
        return String.format("/%s/%s/%s/", now.getYear(), now.getMonthValue(), now.getDayOfMonth());
    }


    /**
     * 判断桶是否存
     * @param bucketName 桶名称
     * @return
     */
    public boolean bucketExists(String bucketName) throws Exception {
        boolean flag = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (flag) {
            return true;
        }
        return false;
    }


    /**
     * 创建桶
     */
    public boolean createBucket(String bucketName) {
        try {
            //判断文件存储的桶对象是否存在
            boolean isExist = bucketExists(bucketName);
            if (isExist) {
                log.info("Bucket asiatrip already exists.");
                return false;
            } else {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                return true;
            }
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
    }

    /**
     * 列出桶里的所有对象
     * @param bucketName 桶名称
     */
    public Iterable<Result<Item>> listObjects(String bucketName) {
        return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 删除桶
     * @param bucketName 桶名称
     * @return 是否删除成功
     */
    public boolean removeBucket(String bucketName) {
        try {
            boolean flag = bucketExists(bucketName);
            if (flag) {
                Iterable<Result<Item>> myObjects = listObjects(bucketName);
                for (Result<Item> result : myObjects) {
                    Item item = result.get();
                    // 有对象文件,则删除失败
                    if (item.size() > 0) {
                        return false;
                    }
                }
                // 删除存储桶,注意,只有存储桶为空时才能删除成功。
                minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
                flag = bucketExists(bucketName);
                if (!flag) {
                    return true;
                }
            }
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
        return false;
    }

    /**
     * 获取所有桶信息
     */
    public List<Bucket> getAllBucket() {
        try {
            // 获取minio中所以的bucket
            List<Bucket> buckets = minioClient.listBuckets();
            for (Bucket bucket : buckets) {
                log.info("bucket 名称:  {}      bucket 创建时间: {}", bucket.name(), bucket.creationDate());
            }
            return buckets;
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return Collections.emptyList();
        }
    }


    /**
     * 上传本地文件到指定桶下
     * @param bucketName    桶名称
     * @param objectName    对象名称
     * @param localFileName 要上传的文件路径
     * @return
     */
    public boolean upload(String bucketName, String objectName, String localFileName) {
        try {
            File file = new File(localFileName);
            FileInputStream fileInputStream = new FileInputStream(file);
            minioClient.putObject(PutObjectArgs.builder()
                    .stream(fileInputStream, file.length(), PutObjectArgs.MIN_MULTIPART_SIZE)
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
            return true;
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
    }


    /**
     * 上传MultipartFile到指定桶下
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param file       文件流
     */
    public boolean upload(String bucketName, String objectName, MultipartFile file) {
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .stream(file.getInputStream(), file.getSize(), PutObjectArgs.MIN_MULTIPART_SIZE)
                    .object(objectName)
                    .build());
            return true;
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
    }

    /**
     * 下载文件到本地
     * @param bucketName    桶名称
     * @param objectName    对象名称
     * @param localFileName 本地文件存储路径
     */
    public boolean downLocal(String bucketName, String objectName, String localFileName) {
        try {
            minioClient.downloadObject(DownloadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .filename(localFileName)
                    .build());
            return true;
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
    }


    /**
     * 下载文件写入到HttpServletResponse
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param response   HttpServletResponse对象
     */
    @SneakyThrows
    public void downResponse(String bucketName, String objectName, HttpServletResponse response) {
        GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
        response.setHeader("Content-Disposition", "attachment;filename=" + objectName.substring(objectName.lastIndexOf("/") + 1));
        response.setContentType("application/force-download");
        response.setCharacterEncoding("UTF-8");
        IOUtils.copy(object, response.getOutputStream());
    }


    /**
     * 删除指定桶的指定文件对象
     * @param bucketName 桶名称
     * @param objectName 对象名称
     */
    public boolean delete(String bucketName, String objectName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
            return true;
        }catch (Exception e){
            log.error("errorMsg={}",e);
            return false;
        }
    }

    /**
     * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
     * @param bucketName  存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象
     * @return
     */
    public boolean deletes(String bucketName, List<String> objectNames) {
        try {
            List<String> deleteErrorNames = new ArrayList<>();

            List<DeleteObject> list = new LinkedList<>();
            objectNames.forEach(item -> list.add(new DeleteObject(item)));
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(list).build());
            for (Result<DeleteError> result : results) {
                DeleteError error = result.get();
                deleteErrorNames.add(error.objectName());
            }
            return deleteErrorNames.size() == 0 ? true : false;
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return false;
        }
    }

    /**
     * 获取文件带时效的访问链接   失效时间(以秒为单位),默认是7天不得大于七天
     * @param bucketName     桶名称
     * @param remoteFileName 对象名称
     * @param timeout        时间
     * @param unit           单位
     * @return 文件访问链接
     */
    public String getPresignedObjectUrl(String bucketName, String remoteFileName, long timeout, TimeUnit unit) {
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(remoteFileName)
                            .expiry((int) unit.toSeconds(timeout))
                            .build());
        } catch (Exception e) {
            log.error("errorMsg={}",e);
            return null;
        }
    }

}

重源码中能看到getPresignedObjectUrl函数可以设置的外链的时效范围为 7天>= expiry >=1,默认值为7天
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.5.测试接口

@RestController
@RequestMapping("/file")
public class FileController {

    @Resource
    private MinioConfig minioConfig;

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 上传文件
     * @param file
     * @param bucketName 桶名称
     * @return 返回对象名称和外链地址
     */
    @PostMapping(value = "/")
    public ResponseEntity<HashMap<String, String>> uploadFile(MultipartFile file, @RequestParam(required = false) String bucketName) {
        bucketName = StringUtils.hasLength(bucketName) ? bucketName : minioConfig.getDefaultBucketName();
        String objectName = minioUtil.getDatePath() + file.getOriginalFilename();
        minioUtil.upload(bucketName, objectName, file);
        String viewPath = minioUtil.getPresignedObjectUrl(bucketName, objectName, 60, TimeUnit.SECONDS);
        HashMap<String, String> objectInfo = new HashMap<>();
        objectInfo.put("objectName", objectName);
        //只能预览图片、txt等部分文件
        objectInfo.put("viewPath", viewPath);
        return ResponseEntity.ok(objectInfo);
    }


    /**
     * 删除指定桶里的某个对象
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return
     */
    @DeleteMapping(value = "/")
    public ResponseEntity<String> deleteByPath(@RequestParam(required = false) String bucketName, String objectName) {
        bucketName = StringUtils.hasLength(bucketName) ? bucketName : minioConfig.getDefaultBucketName();
        minioUtil.delete(bucketName, objectName);
        return ResponseEntity.ok("删除成功");
    }

    /**
     * 下载文件
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param response  相应结果
     */
    @GetMapping("/")
    public void downLoad(@RequestParam(required = false) String bucketName, String objectName,HttpServletResponse response) {
        // 获取文件
        minioUtil.downResponse(bucketName,objectName,response);

    }
}

4.测试

4.1.上传文件

通过postman上传文件到minio上面,会返回一个对象名称和一个带时效的外链名称
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cl7OxkFJ-1638746493572)(https://note.youdao.com/yws/res/7/WEBRESOURCEbed9c59267c404acf1fb2e5816f12657)]

可以复制外链地址到浏览器上面进行下载该文件
在这里插入图片描述

因为代码里配置了链接的有效期为1分钟,超时后再访问就失效了
在这里插入图片描述

4.2.下载文件

浏览器访问接口参数为对象的名称可以下载对应的文件
http://localhost:8025/file/?objectName=/test/2021/12/5/booklist.txt
在这里插入图片描述

4.3.删除文件

通过postman设置DELETE请求类型调用删除文件接口
localhost:8025/file/?objectName=2021/12/5/booklist.txt
在这里插入图片描述

5.设置桶里的文件访问权限为公开的

在这里插入图片描述
设置完就可以通过浏览器直接访问文件了。
访问方式为http://192.168.94.128:API端口/桶名称/对象名称
例如: http://192.168.94.128:9000/test/2021/12/5/booklist.txt

6.项目配套代码

gitee代码地址

创作不易,要是觉得我写的对你有点帮助的话,麻烦在gitee上帮我点下 Star

【SpringBoot框架篇】其它文章如下,后续会继续更新。

  • 8
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皓亮君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值