# MinIO

官网上说是世界上最快的对象存储。其开源协议是AGPLv3。也就是说,商用要收费,个人若将MinIO用在开源项目上是不收费的。

# MinIO部署

MinIO有多种部署方式,docker部署,简单又方便

# 拉取镜像
docker pull minio/minio

# 创建并启动容器
# 9000是minio服务端口,用于服务的链接和请求;9090是minio客户端端口,用于访问管理界面
docker run -p 9000:9000 -p 9090:9090 \
 --name minio \
 -d --restart=always \
 # 设置minio登录名,不少于3个字符
 -e "MINIO_ACCESS_KEY=admin" \
 # 设置minio登录密码,不少于8个字符
 -e "MINIO_SECRET_KEY=admin123" \
 # minio上传的文件默认存储在容器中的/data目录下
 -v /mydata/minio/data:/data \
 # 启动容器
 minio/minio server \
 # --console-address 指定客户端端口;-address 指定服务端端口
 /data --console-address ":9090" -address ":9000"

访问http://ip:9090,出现登录页面则部署成功

# SpringBoot整合MinIO

  • 创建一个Maven项目,添加坐标
<!--web-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--minio-->
<dependency>
	<groupId>io.minio</groupId>
	<artifactId>minio</artifactId>
	<version>8.4.3</version>
</dependency>
  • SpringBoot启动类搞起来
package com.nanfeng.minio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}
  • 创建application.properties
## minio 的UTL
minio.endpoint=http://localhost:9000
## minio 的账号密码
minio.accessKey=admin
minio.secretKey=admin123
## minio 的自己建的桶
minio.bucket.test=test
  • 创建MinIO的配置类 并放在config包下面
@Configuration
public class MinioConfig {

    //读取参数
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {

        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint(endpoint)
                        .credentials(accessKey, secretKey)
                        .build();
        return minioClient;
    }
}

实际上minioClient自带的API很丰富,到这一步就ok了。下面是锦上添花的功能

  • 弄一个操作类,相当于一个工具包
package com.nanfeng.minio.service;

import io.minio.*;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * minio 操作类
 */
@Service
public class MinIOService {

    // 我最喜欢用@Resource 强烈建议不要用@autowire
    @Resource
    private MinioClient minioClient;

    @Value("${minio.bucket.test}")
    private String bucket;

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

    /**
     * 本地文件上传
     * @param localPath 本地路径
     * @param remotePath 远程路径
     * @return 可访问地址
     */
    public String upload(String localPath,String remotePath) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(localPath)
                    .filename(remotePath)
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            return endpoint + "/" + bucket + "/" + remotePath;
    }

    /**
     * 用流上传
     * @param is 文件流
     * @param remotePath 远程路径
     * @return 可访问地址
     */
    public String upload(InputStream is, String remotePath) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        PutObjectArgs args = PutObjectArgs.builder()
                .bucket(bucket)
                .object(remotePath)
                .stream(is, -1, 10485760)
                .build();
        minioClient.putObject(args);
        return endpoint + "/" + bucket + "/" + remotePath;
    }

    /**
     * 删除文件
     * @param remotePath 远程路径
     * @return
     */
    public void delete(String remotePath) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        RemoveObjectArgs args = RemoveObjectArgs.builder()
                .bucket(bucket)
                .object(remotePath)
                .build();
        minioClient.removeObject(args);
    }

    /**
     * 获取流
     * @param remotePath 远程路径
     */
    public InputStream getInputStream(String remotePath) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(bucket)
                .object(remotePath)
                .build();
        return minioClient.getObject(args);
    }
}
  • 写一个controller用来测试一下
package com.nanfeng.minio.controller;


import com.nanfeng.minio.service.MinIOService;
import io.minio.errors.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;

@RestController
public class MinIOController {

    @Resource
    private MinIOService minIOService;

    @PostMapping("/upload")
    public HashMap<String, String> upload(@RequestParam(name = "file", required = false) MultipartFile file) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String originalFilename = file.getOriginalFilename();
        assert originalFilename != null;
        String fileName = System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String url = minIOService.upload(file.getInputStream(), fileName);
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("url",url);
        map.put("fileName",fileName);
        return map;
    }

    @GetMapping("/remove")
    public String remove(String fileName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        minIOService.delete(fileName);
        return "success";
    }
}

# FastDFS

FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。

FastDFS中的文件标识分为两个部分:卷名和文件名,二者缺一不可。

# FastDFS主要内容

  • tracker:追踪者服务器,主要用于协调调度,可以起到负载均衡的作用,记录storage的相关状态信息
  • storage:存储服务器,用于保存文件以及文件的元数据信息
  • group:组,同组节点提供冗余备份,不同组用于扩容
  • mata data:文件的元数据信息,比如长宽信息,图片后缀,视频的帧数等

# FastDFS安装

  • 拉取镜像
docker pull delron/fastdfs
  • 构建tracker容器,端口为22122
docker run -d --name tracker -v /mydata/fastdfs/tracker:/var/fdfs -p 22122:22122  delron/fastdfs tracker
  • 构建storage容器,端口为23000,TRACKER_SERVER地址不要使用127.0.0.1
docker run -d --name storage -e TRACKER_SERVER=192.168.0.172:22122 -v /mydata/fastdfs/storage:/var/fdfs -p 23000:23000  delron/fastdfs storage
  • 配置http访问的端口,默认情况下在Storage服务中是帮我们安装了Nginx服务的,端口为:8888
# 进入storage容器,docker exec -it storage bash,配置文件在/etc/fdfs目录下的storage.conf中

# 将配置文件拷贝出来
docker cp storage:/fdfs_conf/storage.conf  ./

# 配置文件修改后再拷贝到容器内
docker cp ./storage.conf storage:/fdfs_conf/

# 需要修改的地方,将nginx端口改为正在使用的
http.server_port=8888

# nginx配置文件nginx.conf在/usr/local/nginx/conf目录下 

注意

如果宿主机也安装了nginx,就让宿主机端口和storage中nginx端口不一样,这样就相当于装了两个nginx,如果端口有限,可以利用宿主机nginx配置反向代理proxy_pass代理到storage中nginx,配置proxy_pass时不要使用127.0.0.1,会无法代理成功,使用本地ip即可

# 测试上传文件

  • 使用web模块进行文件的上传,将文件上传至FastDFS文件系统
  • 将一张照片(test.png)放置在/mydata/fastdfs/storage目录下,进入storage容器,进入/var/fdfs目录,运行下面命令:
/usr/bin/fdfs_upload_file /etc/fdfs/client.conf test.jpg
  • 此时将该图片已上传至文件系统,并在执行该语句后返回图片存储的uri:
通过url访问http://ip:8888/group1/M00/00/00/CgACD1z7SEuAXrIqAA1eBLGVLow043.png,即可查看到图片

# SpringBoot集成FastDFS

  • 引入依赖
<dependency>
    <groupId>net.oschina.zcx7878</groupId>
    <artifactId>fastdfs-client-java</artifactId>
    <version>1.27.0.0</version>
</dependency>
  • 配置tracker.conf文件
connect_timeout = 60 # 连接超时时间,单位为秒
network_timeout = 60 # 通信超时时间,单位为秒。
charset = UTF-8 # 字符集
http.tracker_http_port = 8080 # tracker的http端口
tracker_server = ip:22122 # tracker服务器IP和端口设置
  • 文件信息封装
@Data
public class FastDFSFile {
    //文件名称
    private String name;
    //文件内容
    private byte[] content;
    //文件扩展名
    private String ext;
    //文件MD5摘要值
    private String md5;
    //文件创建作者
    private String author;

    public FastDFSFile(String name, byte[] content, String ext, String height, String width, String author) {
        super();
        this.name = name;
        this.content = content;
        this.ext = ext;
        this.author = author;
    }

    public FastDFSFile(String name, byte[] content, String ext) {
        super();
        this.name = name;
        this.content = content;
        this.ext = ext;
    }

    public FastDFSFile(String name, byte[] content, String ext, String md5, String author) {
        this.name = name;
        this.content = content;
        this.ext = ext;
        this.md5 = md5;
        this.author = author;
    }
}
  • 文件操作工具类
@Slf4j
public class FastDFSClient {

    /***
     * 初始化加载FastDFS的TrackerServer配置
     */
    static {
        try {
            String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
            ClientGlobal.init(filePath);
        } catch (Exception e) {
            log.error("FastDFS Client Init Fail!", e);
        }
    }

    /***
     * 文件上传
     * @param file
     * @return 1.文件的组名  2.文件的路径信息
     */
    public static String[] upload(FastDFSFile file) {
        //获取文件的作者
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] = new NameValuePair("author", file.getAuthor());

        //接收返回数据
        String[] uploadResults = null;
        StorageClient storageClient = null;
        try {
            //创建StorageClient客户端对象
            storageClient = getTrackerClient();

            /***
             * 文件上传
             * 1)文件字节数组
             * 2)文件扩展名
             * 3)文件作者
             */
            uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
        } catch (Exception e) {
            log.error("Exception when uploadind the file:" + file.getName(), e);
        }

        if (uploadResults == null && storageClient != null) {
            log.error("upload file fail, error code:" + storageClient.getErrorCode());
        }
        //获取组名
        String groupName = uploadResults[0];
        //获取文件存储路径
        String remoteFileName = uploadResults[1];
        return uploadResults;
    }

    /***
     * 获取文件信息
     * @param groupName:组名
     * @param remoteFileName:文件存储完整名
     * @return
     */
    public static FileInfo getFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getTrackerClient();
            return storageClient.get_file_info(groupName, remoteFileName);
        } catch (Exception e) {
            log.error("Exception: Get File from Fast DFS failed", e);
        }
        return null;
    }

    /***
     * 文件下载
     * @param groupName
     * @param remoteFileName
     * @return
     */
    public static InputStream downFile(String groupName, String remoteFileName) {
        try {
            //创建StorageClient
            StorageClient storageClient = getTrackerClient();

            //下载文件
            byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
            InputStream ins = new ByteArrayInputStream(fileByte);
            return ins;
        } catch (Exception e) {
            log.error("Exception: Get File from Fast DFS failed", e);
        }
        return null;
    }

    /***
     * 文件删除
     * @param groupName
     * @param remoteFileName
     * @throws Exception
     */
    public static void deleteFile(String groupName, String remoteFileName)
            throws Exception {
        //创建StorageClient
        StorageClient storageClient = getTrackerClient();

        //删除文件
        int i = storageClient.delete_file(groupName, remoteFileName);
    }

    /***
     * 获取Storage组
     * @param groupName
     * @return
     * @throws IOException
     */
    public static StorageServer[] getStoreStorages(String groupName)
            throws IOException {
        //创建TrackerClient
        TrackerClient trackerClient = new TrackerClient();
        //获取TrackerServer
        TrackerServer trackerServer = trackerClient.getConnection();
        //获取Storage组
        return trackerClient.getStoreStorages(trackerServer, groupName);
    }

    /***
     * 获取Storage信息,IP和端口
     * @param groupName
     * @param remoteFileName
     * @return
     * @throws IOException
     */
    public static ServerInfo[] getFetchStorages(String groupName,
                                                String remoteFileName) throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
    }

    /***
     * 获取Tracker服务地址
     * @return
     * @throws IOException
     */
    public static String getTrackerUrl() throws IOException {
        return "http://" + getTrackerServer().getInetSocketAddress().getHostString() + ":" + ClientGlobal.getG_tracker_http_port() + "/";
    }

    /***
     * 获取Storage客户端
     * @return
     * @throws IOException
     */
    private static StorageClient getTrackerClient() throws IOException {
        TrackerServer trackerServer = getTrackerServer();
        StorageClient storageClient = new StorageClient(trackerServer, null);
        return storageClient;
    }

    /***
     * 获取Tracker
     * @return
     * @throws IOException
     */
    private static TrackerServer getTrackerServer() throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        return trackerServer;
    }
}
  • 文件上传
@RestController
@RequestMapping("/file")
public class FileController {

    @PostMapping("/upload")
    public Result uploadFile(MultipartFile file) {
        try {
            //判断文件是否存在
            if (file == null) {
                throw new BusinessException("文件不存在");
            }
            //获取文件的完整名称
            String originalFilename = file.getOriginalFilename();
            if (StringUtils.isEmpty(originalFilename)) {
                throw new BusinessException("文件不存在");
            }

            //获取文件的扩展名称  abc.jpg   jpg
            String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

            //获取文件内容
            byte[] content = file.getBytes();

            //创建文件上传的封装实体类
            FastDFSFile fastDFSFile = new FastDFSFile(originalFilename, content, extName);

            //基于工具类进行文件上传,并接受返回参数  String[]
            String[] uploadResult = FastDFSClient.upload(fastDFSFile);

            //封装返回结果
            String url = FastDFSClient.getTrackerUrl() + uploadResult[0] + "/" + uploadResult[1];
            return new Result(true, StatusCode.OK, "文件上传成功", url);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new Result(false, StatusCode.ERROR, "文件上传失败");
    }
}