一二三应用开发平台文件处理设计与实现系列之4——文件存储框架设计与实现

背景

上篇介绍了平台统一使用附件实体来封装文件,今天来说下存储层面的封装与设计。

整体设计

作为平台,需考虑支持多种文件存储方案,对于中小型系统,可以使用直接存储到磁盘这种简单实用的方式;对一中大型系统,能支持使用对象存储组件。
基于上述考虑,建立抽象层,通过接口定义对于文件的上传、下载、查看等功能,对于对象存储或文件服务器存储,本质上都是具体的存储实现方式。
通过功能类的具体实现,来支持多种存储模式,通过更改配置,可以灵活选择具体的存储方式,业务应用无感知。
平台内置磁盘存储和主流对象存储组件minio两种实现方案,如对接其他对象存储系统,如阿里OSS,通过系统集成的方式,实现预置的抽象接口,适配到阿里OSS的API即可。

框架实现

服务接口

对于多种存储模式,定义统一的服务接口,如下所示:

/**
 * 对象存储服务接口
 *
 * @author wqliu
 * @date 2023-05-20
 */
public interface ObjectStoreService {

    /**
     * 上传文件块
     *
     * @param fileChunk 文件块
     */
    void uploadChunk(FileChunk fileChunk);

    /**
     * 合并文件块
     *
     * @param fileInfo 文件信息
     */
    void mergeChunks(FileInfo fileInfo);



    /**
     * 删除文件
     *
     * @param relativePath 文件相对路径
     * @return
     */
    void deleteFile(String relativePath);

    /**
     * 获取文件流
     *
     * @param relativePath 文件相对路径
     * @return 文件流
     */
    InputStream getFile(String relativePath);

    /**
     * 上传图片
     *
     * @param image 图像
     * @param id    id
     */
    void uploadImage(MultipartFile image, String id);


    /**
     * 获取文件全路径
     *
     * @param relativePath 相对路径
     * @return 完整路径
     */
    String getFullPath(String relativePath);


    /**
     * 生成相对存储路径
     *
     * @param moduleCode 模板编码
     * @param entityType 实体类型
     * @return 相对路径
     */
    String generateRelativePath(String moduleCode, String entityType);

}

上传文件块、合并文件块、删除文件、获取文件流、上传图片这5个服务接口是跟统一封装的附件服务对应的。在此基础上,增加了2个辅助接口,传入相对路径,获取文件全路径,以及根据模块编码和实体类型生成相对存储路径。

抽象基类

抽象基类是服务接口ObjectStoreService的默认实现,一方面,可以将公用方法提取到抽象基类中实现,提升代码的复用;另一方面,也能简化具体服务实现类的功能实现工作。

/**
 * 对象存储服务接口抽象实现类
 * @author wqliu
 * @date 2023-11-23
 */
public abstract class BaseObjectStoreService implements ObjectStoreService{

    @Autowired
    protected OssConfig ossConfig;

    @Override
    public String generateRelativePath(String moduleCode, String entityType) {
        // 生成附件上传路径 根路径/模块名/实体类型名/年份/月份
        Calendar calendar = Calendar.getInstance();
        StringBuilder sbRelativePath = new StringBuilder()
                .append(moduleCode).append("/")
                .append(entityType).append("/")
                .append(calendar.get(Calendar.YEAR)).append("/")
                // 月份左边补零到2位
                .append(StringUtils.leftPad(String.valueOf(calendar.get(Calendar.MONTH) + 1), 2, "0"))
                .append("/");
        return sbRelativePath.toString();
    }

    /**
     * 获取文件全路径
     *
     * @param relativePath
     * @return
     */
    @Override
    public String getFullPath(String relativePath) {
        String basePath = ossConfig.getBasePath();
        return basePath+relativePath;
    }
}

配置文件

在平台配置文件application.yml中,添加oss的配置节点,来指定具体的存储类和设置基存储根路径。

#平台配置
platform-config:
  system:
    enablePermission: false
    userInitPassword: 12345678
    tokenSecret: wqliu
    exportDataPageSize: 2
  notification:
    serverPort: 9997 
  oss:
    # 存储类
    storeClass: tech.abc.platform.oss.service.impl.LocalStoreServiceImpl   
    # 存储根路径
    basePath: c:/attachment/  
  mail:
    senderAddress: sealy321@126.com

配置类

通过配置类,加载application.yml中oss属性的值。

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

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

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

}

服务配置

/**
 * 对象存储服务配置
 *
 * @author wqliu
 * @date 2023-05-20
 */
@Configuration
@Slf4j
public class OssServiceConfig {

    @Autowired
    private OssConfig ossConfig;


    @Bean
    public ObjectStoreService instance() {

        String className = ossConfig.getStoreClass();
        try {
            return (ObjectStoreService) ClassUtils.getClass(className).newInstance();
        } catch (Exception e) {
            log.error("加载对象存储服务类出错", e);
            throw new CustomException(CommonException.CLASS_NOT_FOUND);
        }

    }

}

磁盘存储

文件存储框架搭建好了,接下来就是具体的实现了。按照目标,我们需要实现本地磁盘存储和集成minio对象存储组件两种模式。
先来说一下磁盘直接存储这种相对简单的模式实现。
新建本地存储服务类,继承抽象基类BaseObjectStoreService,并实现ObjectStoreService接口就行了。
处理逻辑在前面文章的设计中提到过,具体如下:
上传文件块,如文件体积较小,没有触发分块,则该文件块就是一个完整的文件,将该文件直接存储到磁盘。
若文件体积较大,触发了分块,则只将分块存到磁盘临时目录下;待前端检测到所有文件块均已上传完成,调用合并文件块操作,依据全局唯一的文件标识,去临时目录下找到所有的文件块,进行文件合并操作。

对于富文本编辑器中上传的图片,同样使用附件功能来进行统一封装,与普通文件不同的是,图片上传不分块,存放到预置的统一目录(image/)下,生成一个虚拟的实体标识,不对应具体的实体,该实体标识来存储图片及读取图片用来展示。

完整源码如下:

/**
 * 本地磁盘模式 对象存储服务
 *
 * @author wqliu
 * @date 2023-05-20
 */
@Slf4j
public class LocalStoreServiceImpl extends BaseObjectStoreService {

    @Autowired
    private OssConfig ossConfig;



    @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 {
            File file = new File(path + filePrefix + fileChunk.getFilename());
            FileUtils.copyInputStreamToFile(fileChunk.getFile().getInputStream(), file);
        } catch (IOException e) {
            log.error("存储文件块出错", e);
            throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
        }
    }

    @Override
    public void mergeChunks(FileInfo fileInfo) {
        // 生成目标文件
        String relativePath = generateRelativePath(fileInfo.getModuleCode(),fileInfo.getEntityType());
        String fullPath = getFullPath(relativePath);

        File targetFile = new File(fullPath + fileInfo.getIdentifier() + fileInfo.getFilename());
        // 获取临时文件全路径
        String tempFullPath = fullPath + FileConstant.TEMP_PATH;

        // 获取该路径下以id开始的文件
        File dir = FileUtils.getFile(tempFullPath);
        FilenameFilter filenameFilter = new PrefixFileFilter(fileInfo.getIdentifier());
        String[] fileList = dir.list(filenameFilter);
        Arrays.sort(fileList);
        try {
            // 合并文件
            for (String file : fileList) {
                Path chunkPath = Paths.get(tempFullPath, file);
                FileUtils.writeByteArrayToFile(targetFile, Files.readAllBytes(chunkPath), true);
            }
        } catch (IOException e) {
            log.error("合并文件块出错", e);
            throw new CustomException(FileExceptionEnum.FILE_CHUNK_MERGE_ERROR);
        } finally {
            // 删除临时文件
            for (String file : fileList) {
                FileUtils.deleteQuietly(new File(tempFullPath + file));
            }
        }
    }


    @Override
    public InputStream getFile(String relativePath) {
        String fullPath = getFullPath(relativePath);
        try {
            return FileUtils.openInputStream(new File(fullPath));
        } catch (IOException e) {
            log.error("读取附件出错", e);
            throw new CustomException(FileExceptionEnum.FILE_READ_ERROR);
        }
    }

    @Override
    public void deleteFile(String relativePath) {
        String fullPath = getFullPath(relativePath);
        FileUtils.deleteQuietly(new File(fullPath));
    }


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

        // 默认是正式目录
        String basePath = ossConfig.getBasePath();

        String path = FilenameUtils.concat(basePath, FileConstant.IMAGE_PATH);

        try {
            File file = new File(path + id + image.getOriginalFilename());
            FileUtils.copyInputStreamToFile(image.getInputStream(), file);
        } catch (IOException e) {
            log.error("存储文件块出错", e);
            throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
        }
    }


}

开源平台资料

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

你可能感兴趣的:(文件处理,附件上传下载,minio,文件存储方案,对象存储)