完成新 File 的功能

This commit is contained in:
YunaiV
2022-03-16 23:31:26 +08:00
parent cdcecd0d4a
commit 87670d18fd
26 changed files with 277 additions and 205 deletions

View File

@ -4,8 +4,8 @@ import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
import cn.iocoder.yudao.module.infra.convert.file.FileConvert;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.service.file.FileService;
@ -50,9 +50,9 @@ public class FileController {
@DeleteMapping("/delete")
@ApiOperation("删除文件")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = String.class)
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('infra:file:delete')")
public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) {
fileService.deleteFile(id);
return success(true);
}
@ -60,19 +60,19 @@ public class FileController {
@GetMapping("/{configId}/get/{path}")
@ApiOperation("下载文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = Long.class),
@ApiImplicitParam(name = "path", value = "文件路径", required = true, dataTypeClass = String.class)
})
public void getFile(HttpServletResponse response,
@PathVariable("configId") String configId,
@PathVariable("path") String path) throws IOException {
FileDO file = fileService.getFile(path);
if (file == null) {
log.warn("[getFile][path({}) 文件不存在]", path);
public void getFileContent(HttpServletResponse response,
@PathVariable("configId") Long configId,
@PathVariable("path") String path) throws IOException {
byte[] content = fileService.getFileContent(configId, path);
if (content == null) {
log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
ServletUtils.writeAttachment(response, path, file.getContent());
ServletUtils.writeAttachment(response, path, content);
}
@GetMapping("/page")

View File

@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
@Data
public class FileRespVO {
@ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
private String id;
@ApiModelProperty(value = "文件类型", required = true, example = "jpg")
private String type;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.infra.controller.admin.file.vo;
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
@ -19,7 +19,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
public class FilePageReqVO extends PageParam {
@ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
private String id;
private String path;
@ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
private String type;

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@ApiModel(value = "管理后台 - 文件 Response VO", description = "不返回 content 字段,太大")
@Data
public class FileRespVO {
@ApiModelProperty(value = "文件编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
private String path;
@ApiModelProperty(value = "文件 URL", required = true, example = "https://www.iocoder.cn/yudao.jpg")
private String url;
@ApiModelProperty(value = "文件类型", example = "jpg")
private String type;
@ApiModelProperty(value = "文件大小", example = "2048", required = true)
private Integer size;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.infra.convert.file;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FileRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.file;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@ -27,8 +25,7 @@ public class FileDO extends BaseDO {
/**
* 编号,数据库自增
*/
@TableId(type = IdType.INPUT)
private String id;
private Long id;
/**
* 配置编号
*
@ -39,6 +36,10 @@ public class FileDO extends BaseDO {
* 路径,即文件名
*/
private String path;
/**
* 访问地址
*/
private String url;
/**
* 文件类型
*
@ -46,18 +47,9 @@ public class FileDO extends BaseDO {
*/
@TableField(value = "`type`")
private String type;
/**
* 访问地址
*/
private String url;
/**
* 文件大小
*/
private Integer size;
/**
* 文件内容
*/
@Deprecated
private byte[] content;
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.infra.dal.mysql.file;
import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository
public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
@Resource
private FileContentMapper fileContentMapper;
@Override
public void insert(Long configId, String path, byte[] content) {
FileContentDO entity = new FileContentDO().setConfigId(configId)
.setPath(path).setContent(content);
fileContentMapper.insert(entity);
}
@Override
public void delete(Long configId, String path) {
fileContentMapper.delete(buildQuery(configId, path));
}
@Override
public byte[] selectContent(Long configId, String path) {
FileContentDO fileContentDO = fileContentMapper.selectOne(
buildQuery(configId, path).select(FileContentDO::getContent));
return fileContentDO != null ? fileContentDO.getContent() : null;
}
private LambdaQueryWrapper<FileContentDO> buildQuery(Long configId, String path) {
return new LambdaQueryWrapper<FileContentDO>()
.eq(FileContentDO::getConfigId, configId)
.eq(FileContentDO::getPath, path);
}
}

View File

@ -0,0 +1,9 @@
package cn.iocoder.yudao.module.infra.dal.mysql.file;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileContentMapper extends BaseMapper<FileContentDO> {
}

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.dal.mysql.file;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import org.apache.ibatis.annotations.Mapper;
@ -17,25 +17,10 @@ public interface FileMapper extends BaseMapperX<FileDO> {
default PageResult<FileDO> selectPage(FilePageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<FileDO>()
.likeIfPresent("id", reqVO.getId())
.likeIfPresent("path", reqVO.getPath())
.likeIfPresent("type", reqVO.getType())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("create_time"));
}
default Long selectCountById(String id) {
return selectCount(FileDO::getId, id);
}
/**
* 基于 Path 获取文件
* 实际上,是基于 ID 查询
*
* @param path 路径
* @return 文件
*/
default FileDO selectByPath(String path) {
return selectById(path);
}
}

View File

@ -1,12 +0,0 @@
package cn.iocoder.yudao.module.infra.framework.file.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 文件 配置类
*/
@Configuration
@EnableConfigurationProperties(FileProperties.class)
public class FileConfiguration {
}

View File

@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.infra.framework.file.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "yudao.file")
@Validated
@Data
public class FileProperties {
/**
* 对应 FileController 的 getFile 方法
*/
@NotNull(message = "基础文件路径不能为空")
private String basePath;
// TODO 七牛、等等
}

View File

@ -1,16 +0,0 @@
/**
* 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务
*
* 在不采用云服务的情况下,我们有几种技术选型:
* 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等
* 方案 2. 使用服务器的文件系统存储
* 方案 3. 使用数据库进行存储
*
* 如果考虑额外在搭建服务,推荐方案 1。
* 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做
* 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件
*
* 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。
* 随着文件的量级大了之后,还是推荐采用云服务。
*/
package cn.iocoder.yudao.module.infra.framework.file;

View File

@ -36,7 +36,7 @@ public class SecurityConfiguration {
registry.antMatchers(adminSeverContextPath).anonymous()
.antMatchers(adminSeverContextPath + "/**").anonymous();
// 文件的获取接口,可匿名访问
registry.antMatchers(buildAdminApi("/infra/file/get/**"), buildAppApi("/infra/file/get/**")).anonymous();
registry.antMatchers(buildAdminApi("/infra/file/*/get/**"), buildAppApi("/infra/file/get/**")).permitAll();
}
};

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.infra.service.file;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
@ -83,4 +84,19 @@ public interface FileConfigService {
*/
String testFileConfig(Long id);
/**
* 获得指定编号的文件客户端
*
* @param id 配置编号
* @return 文件客户端
*/
FileClient getFileClient(Long id);
/**
* 获得 Master 文件客户端
*
* @return 文件客户端
*/
FileClient getMasterFileClient();
}

View File

@ -233,4 +233,9 @@ public class FileConfigServiceImpl implements FileConfigService {
return fileClientFactory.getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg");
}
@Override
public FileClient getFileClient(Long id) {
return fileClientFactory.getFileClient(id);
}
}

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.infra.service.file;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
@ -33,14 +33,15 @@ public interface FileService {
*
* @param id 编号
*/
void deleteFile(String id);
void deleteFile(Long id);
/**
* 获得文件
* 获得文件内容
*
* @param configId 配置编号
* @param path 文件路径
* @return 文件
* @return 文件内容
*/
FileDO getFile(String path);
byte[] getFileContent(Long configId, String path);
}

View File

@ -1,18 +1,19 @@
package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
/**
* 文件 Service 实现类
@ -23,10 +24,10 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
public class FileServiceImpl implements FileService {
@Resource
private FileMapper fileMapper;
private FileConfigService fileConfigService;
@Resource
private FileProperties fileProperties;
private FileMapper fileMapper;
@Override
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
@ -35,36 +36,49 @@ public class FileServiceImpl implements FileService {
@Override
public String createFile(String path, byte[] content) {
if (fileMapper.selectCountById(path) > 0) {
throw exception(FILE_PATH_EXISTS);
}
// 上传到文件存储器
FileClient client = fileConfigService.getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空");
String url = client.upload(content, path);
// 保存到数据库
FileDO file = new FileDO();
file.setId(path);
file.setConfigId(client.getId());
file.setPath(path);
file.setUrl(url);
file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
file.setContent(content);
file.setSize(content.length);
fileMapper.insert(file);
// 拼接路径返回
return fileProperties.getBasePath() + path;
return url;
}
@Override
public void deleteFile(String id) {
public void deleteFile(Long id) {
// 校验存在
this.validateFileExists(id);
// 更新
FileDO file = this.validateFileExists(id);
// 从文件存储器中删除
FileClient client = fileConfigService.getFileClient(file.getConfigId());
Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
client.delete(file.getPath());
// 删除记录
fileMapper.deleteById(id);
}
private void validateFileExists(String id) {
if (fileMapper.selectById(id) == null) {
private FileDO validateFileExists(Long id) {
FileDO fileDO = fileMapper.selectById(id);
if (fileDO == null) {
throw exception(FILE_NOT_EXISTS);
}
return fileDO;
}
@Override
public FileDO getFile(String path) {
return fileMapper.selectByPath(path);
public byte[] getFileContent(Long configId, String path) {
FileClient client = fileConfigService.getFileClient(configId);
Assert.notNull(client, "客户端({}) 不能为空", configId);
return client.getContent(path);
}
}

View File

@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.yudao.module.infra.framework.file.config.FileProperties;
import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
@ -17,47 +17,46 @@ import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.*;
@Import({FileServiceImpl.class, FileProperties.class})
@Import({FileServiceImpl.class})
public class FileServiceTest extends BaseDbUnitTest {
@Resource
private FileService fileService;
@MockBean
private FileProperties fileProperties;
@Resource
private FileMapper fileMapper;
@MockBean
private FileConfigService fileConfigService;
@Test
public void testGetFilePage() {
// mock 数据
FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到
o.setId("yunai");
o.setPath("yunai");
o.setType("jpg");
o.setCreateTime(buildTime(2021, 1, 15));
});
fileMapper.insert(dbFile);
// 测试 id 不匹配
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setId("tudou")));
// 测试 path 不匹配
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou")));
// 测试 type 不匹配
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
o.setId("yunai02");
o.setType("png");
}));
// 测试 createTime 不匹配
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
o.setId("yunai03");
o.setCreateTime(buildTime(2020, 1, 15));
}));
// 准备参数
FilePageReqVO reqVO = new FilePageReqVO();
reqVO.setId("yunai");
reqVO.setPath("yunai");
reqVO.setType("jp");
reqVO.setBeginCreateTime(buildTime(2021, 1, 10));
reqVO.setEndCreateTime(buildTime(2021, 1, 20));
@ -67,7 +66,7 @@ public class FileServiceTest extends BaseDbUnitTest {
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0), "content");
AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
}
@Test
@ -75,52 +74,68 @@ public class FileServiceTest extends BaseDbUnitTest {
// 准备参数
String path = randomString();
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
// mock Master 文件客户端
FileClient client = mock(FileClient.class);
when(fileConfigService.getMasterFileClient()).thenReturn(client);
String url = randomString();
when(client.upload(same(content), same(path))).thenReturn(url);
when(client.getId()).thenReturn(10L);
// 调用
String url = fileService.createFile(path, content);
String result = fileService.createFile(path, content);
// 断言
assertEquals(fileProperties.getBasePath() + path, url);
assertEquals(result, url);
// 校验数据
FileDO file = fileMapper.selectById(path);
assertEquals(path, file.getId());
FileDO file = fileMapper.selectOne(FileDO::getPath, path);
assertEquals(10L, file.getConfigId());
assertEquals(path, file.getPath());
assertEquals(url, file.getUrl());
assertEquals("jpg", file.getType());
assertArrayEquals(content, file.getContent());
}
@Test
public void testCreateFile_exists() {
// mock 数据
FileDO dbFile = randomPojo(FileDO.class);
fileMapper.insert(dbFile);
// 准备参数
String path = dbFile.getId(); // 模拟已存在
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
// 调用,并断言异常
assertServiceException(() -> fileService.createFile(path, content), FILE_PATH_EXISTS);
assertEquals(content.length, file.getSize());
}
@Test
public void testDeleteFile_success() {
// mock 数据
FileDO dbFile = randomPojo(FileDO.class);
FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg"));
fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据
// mock Master 文件客户端
FileClient client = mock(FileClient.class);
when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
// 准备参数
String id = dbFile.getId();
Long id = dbFile.getId();
// 调用
fileService.deleteFile(id);
// 校验数据不存在了
assertNull(fileMapper.selectById(id));
// 校验调用
verify(client).delete(eq("tudou.jpg"));
}
@Test
public void testDeleteFile_notExists() {
// 准备参数
String id = randomString();
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS);
}
@Test
public void testGetFileContent() {
// 准备参数
Long configId = 10L;
String path = "tudou.jpg";
// mock 方法
FileClient client = mock(FileClient.class);
when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
byte[] content = new byte[]{};
when(client.getContent(eq("tudou.jpg"))).thenReturn(content);
// 调用
byte[] result = fileService.getFileContent(configId, path);
// 断言
assertSame(result, content);
}
}

View File

@ -32,9 +32,12 @@ CREATE TABLE IF NOT EXISTS "infra_file_config" (
) COMMENT '文件配置表';
CREATE TABLE IF NOT EXISTS "infra_file" (
"id" varchar(188) NOT NULL,
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"config_id" bigint NOT NULL,
"path" varchar(512),
"url" varchar(1024),
"type" varchar(63) DEFAULT NULL,
"content" blob NOT NULL,
"size" bigint NOT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',