一二三应用开发平台文件处理设计与实现系列之6——集成minio实现文件存储

发布时间:2024年01月22日

背景

前面完成了minio的技术预研,今天基于文件存储框架,集成minio,实现平台使用对象存储组件来存储文件的功能。

配置文件

相比于直接磁盘存储,只需要指定存储类和根路径,minio还需要指定服务地址、账号、密码、桶名,因此在配置文件的oss节点下新增minio属性,存放这几个配置信息。

  oss:   
    # 使用minio做文件存储
    storeClass: tech.abc.platform.oss.service.impl.MinioStoreServiceImpl
    # minio因使用桶作为逻辑存储,无根路径,留空即可
    basePath:
    minio:
      server: http://127.0.0.1:9000
      accessKey: admin
      secretKey: 12345678
      bucketName: abc
  

配置类

新增配置类,加载application.yml中minio属性的值。

/**
 * miniio配置属性
 *
 * @author wqliu
 * @date 2023-11-21
 */
@Data
public class MinioConfig {

    /**
     * 服务
     */
    private String server = "http://127.0.0.1:9000";
    /**
     * 账号
     */
    private String accessKey = "admin";

    /**
     * 密钥
     */
    private String secretKey = "12345678";

    /**
     * 桶名
     */
    private String bucketName = "abc";


}

修改OssConfig配置类,增加MinioConfig配置。

/**
 * 对象存储配置文件
 *
 * @author wqliu
 * @date 2023-05-20
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "platform-config.oss")
public class OssConfig {

    /**
     * 对象存储类名
     */
    private String storeClass = "";

    /**
     * 存储根路径
     */
    private String basePath = "";


    /**
     * minio配置
     */
    private MinioConfig minioConfig=new MinioConfig();

}

服务实现

同本地磁盘存储的服务类似,新建minio存储服务类,继承抽象基类BaseObjectStoreService,并实现ObjectStoreService接口就行了,部分方法,需要使用minio的API来替换掉磁盘的IO操作,完整源码如下:

/**
 * 使用minio 对象存储服务
 *
 * @author wqliu
 * @date 2023-11-21
 */
@Slf4j
public class MinioStoreServiceImpl extends BaseObjectStoreService {


    @Autowired
    private MinioClient minioClient;



    @Override
    public void uploadChunk(FileChunk fileChunk) {
        // 默认前缀使用唯一性编号id
        String filePrefix = fileChunk.getIdentifier();
        // 默认是正式目录
        String relativePath = generateRelativePath(fileChunk.getModuleCode(),fileChunk.getEntityType());
        String path = getFullPath(relativePath);
        // 如进行了分块
        if (fileChunk.getTotalChunks() > 1) {
            // 路径附加临时目录
            path = path + FileConstant.TEMP_PATH;
            // 前缀附加块编号
            filePrefix = filePrefix +  StringUtils.leftPad(fileChunk.getChunkNumber().toString(), 3, "0");
        }

        try {
            storeFile(fileChunk.getFile(),path + filePrefix + fileChunk.getFilename());
        } catch (Exception e) {

            throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
        }
    }

    @Override
    public InputStream getFile(String relativePath) {

        String fullPath = getFullPath(relativePath);

        try {

            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .object(fullPath)
                    .build();
            GetObjectResponse response = minioClient.getObject(args);
            return response;
        }catch (Exception ex){
            log.error(ex.getMessage());
            return null;
        }
    }

    @Override
    public void deleteFile(String relativePath) {
        try {
            RemoveObjectArgs args = RemoveObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .object(relativePath)
                    .build();
            minioClient.removeObject(args);
        }catch (Exception e){
            log.error("删除文件出错", e);

        }

    }

    @Override
    public void mergeChunks(FileInfo fileInfo) {
        TreeSet<String> objectList = new TreeSet<>();
        // 获取临时文件全路径
        String relativePath = generateRelativePath(fileInfo.getModuleCode(),fileInfo.getEntityType());
        String tempPath = relativePath+ FileConstant.TEMP_PATH;
        String tempFullPath = getFullPath(tempPath);
        try {
            // 获取该路径下以id开始的文件
            Iterable<Result<Item>> fileList = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(ossConfig.getMinioConfig().getBucketName())
                            .prefix(tempFullPath+fileInfo.getIdentifier())
                            .build());
            //定义合并数据源
            List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
            Iterator<Result<Item>> it = fileList.iterator();
            while (it.hasNext()) {
                Item item = it.next().get();
                objectList.add(item.objectName());
            }
            //合并文件
            String fileStoreName = fileInfo.getIdentifier() + fileInfo.getFilename();
            String fullPath = getFullPath(relativePath);
            // 添加合并数据源
            for (String object : objectList) {
                sourceObjectList.add(
                        ComposeSource.builder().bucket(ossConfig.getMinioConfig().getBucketName()).object(object).build());
            }
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(ossConfig.getMinioConfig().getBucketName())
                            .object(fullPath + fileStoreName)
                            .sources(sourceObjectList)
                            .build());


        } catch (Exception e) {
            log.error("合并文件块出错", e);
            throw new CustomException(FileExceptionEnum.FILE_CHUNK_MERGE_ERROR);
        } finally {
            // 删除临时文件
            for (String object : objectList) {
                deleteFile(object);
            }
        }
    }


    @Override
    public void uploadImage(MultipartFile image, String id) {

        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .contentType(image.getContentType())
                    .object(FileConstant.IMAGE_PATH+id + image.getOriginalFilename())
                    .stream(image.getInputStream(),image.getSize(), -1)
                    .build();
            minioClient.putObject(args);
        }catch (Exception e){
            log.error("存储文件块出错", e);
            throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
        }

    }

    /**
     * 存储文件
     * @param file 文件
     * @param filePath 文件路径
     */
    private void storeFile(MultipartFile file, String filePath) {
        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .contentType(file.getContentType())
                    .object(filePath)
                    .stream(file.getInputStream(),file.getSize(),-1)
                    .build();
            minioClient.putObject(args);

        } catch (Exception e) {
            log.error("文件存储出错", e);
            throw new CustomException(FileExceptionEnum.FILE_STORE_ERROR);
        }
    }
}

相关API

上传文件

在技术验证环节,使用uploadObject来上传文件,此时文件实际来源于本地磁盘:

/**
 * 上传文件
 */
@GetMapping("/upload")
public void upload() {
    try {
        MinioClient client = getClient();
        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
        .bucket("abc")
        .object("image/123/1.png")
        .filename("e:/1.png")
        .build();
        client.uploadObject(uploadObjectArgs);
    }catch (Exception ex){
        log.error(ex.getMessage());
    }

}

这种方式适用于C/S架构下的客户端应用程序。
对于大多数B/S结构,实际后端服务收到的是MultipartFile类型的文件,这时候需要更换API,使用的是putObject方法。

  /**
     * 存储文件
     * @param file 文件
     * @param filePath 文件路径
     */
    private void storeFile(MultipartFile file, String filePath) {
        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .contentType(file.getContentType())
                    .object(filePath)
                    .stream(file.getInputStream(),file.getSize(),-1)
                    .build();
            minioClient.putObject(args);

        } catch (Exception e) {
            log.error("文件存储出错", e);
            throw new CustomException(FileExceptionEnum.FILE_STORE_ERROR);
        }
    }

该方法有几个关键参数:
bucket:桶名
contentType:文件类型
object:对象名
stream:内含了三个参数,第一个是文件流,第二个是文件大小,第三个是块大小,后两个参数,如不确定大小可以传-1,minio会自动检测大小,但同时只能一个传-1,不能两个同时都是-1。
其中object可以携带路径,例如image/2023/1.png,若目录不存在minio内部处理会自动创建目录。

获取文件流

获取文件流比较简单,调用的是GetObjectArgs方法,传入文件的路径,拿到文件流。
注意API返回的类型GetObjectResponse,实际是InputStream的子类,因此可以用InputStream来接收。

 public InputStream getFile(String relativePath) {

        String fullPath = getFullPath(relativePath);

        try {

            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(ossConfig.getMinioConfig().getBucketName())
                    .object(fullPath)
                    .build();
            GetObjectResponse response = minioClient.getObject(args);
            return response;
        }catch (Exception ex){
            log.error(ex.getMessage());
            return null;
        }
    }

检索文件

这是技术验证未涉及的一个方法,在文件块合并的逻辑处理中需要用到,调用的是listObjects方法,通过prefix方法来指定前缀。

// 获取该路径下以id开始的文件
Iterable<Result<Item>> fileList = minioClient.listObjects(
        ListObjectsArgs.builder()
                .bucket(ossConfig.getMinioConfig().getBucketName())
                .prefix(tempFullPath+fileInfo.getIdentifier())
                .build());

合并文件块

合并文件块,需要使用composeObject方法,入参是文件块的集合。


            //定义合并数据源
            List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
             ……

            // 添加合并数据源
            for (String object : objectList) {
                sourceObjectList.add(
                        ComposeSource.builder().bucket(ossConfig.getMinioConfig().getBucketName()).object(object).build());
            }
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(ossConfig.getMinioConfig().getBucketName())
                            .object(fullPath + fileStoreName)
                            .sources(sourceObjectList)
                            .build());

创建目录

因为minio会自动创建目录,上面的服务实现类实际没有用到单独创建目录的操作,作为探索项,也列在这里供参考吧。
如果需要单独创建目录,例如在系统初始化阶段,将默认图片存放目录先创建起来,使用的也是putObject方法,只是用法比较特殊,stream方法中,构建一个空的字节数组,转换为字节流,文件大小赋值0,文件块大小赋值为0(官方实例是赋值了-1,代表自动检测,也可以),同时注意,一定要以“/"结尾,否则minio会将其作为文件处理,如下所示:

/**
 * 创建目录
 */
@GetMapping("/createFolder")
public void createFolder() {
    try {
        MinioClient client = getClient();
        PutObjectArgs args = PutObjectArgs.builder()
        .bucket("abc")
        .object("image/123/")
        .stream(new ByteArrayInputStream(new byte[] {}),0,0)
        .build();
        client.putObject(args);
    }catch (Exception ex){
        log.error(ex.getMessage());
    }

}

开源平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。

文章来源:https://blog.csdn.net/seawaving/article/details/135044044
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。