feat(系统设置): 处理模板中的富文本框图片

--story=1015605 --user=陈建星 【模版管理】缺陷模板&用例模板,可配置默认值 https://www.tapd.cn/55049933/s/1556744
This commit is contained in:
AgAngle 2024-07-31 13:47:11 +08:00 committed by Craftsman
parent 578fe9a157
commit 076b6c3ef2
37 changed files with 842 additions and 444 deletions

View File

@ -35,10 +35,38 @@ public class DefaultRepositoryDir {
/*------ end: 系统下资源目录 --------*/
/*------ start: 组织下资源目录 ------*/
/**
* 组织模板富文本图片存储目录
* 这里省略模板ID模板的图片不处理删除的情况
* 因为用例会引用图片模板删除后图片也需要能访问
*/
private static final String ORGANIZATION_TEMPLATE_IMG_DIR = ORGANIZATION_DIR + "/template-img";
/**
* 组织模板压缩图片存储目录
* 这里省略模板ID模板的图片不处理删除的情况
* 因为用例会引用图片模板删除后图片也需要能访问
*/
private static final String ORGANIZATION_TEMPLATE_IMG_PREVIEW_DIR = ORGANIZATION_DIR + "/template-img/preview";
/*------ end: 组织下资源目录 --------*/
/*------ start: 项目下资源目录 --------*/
/**
* 项目模板富文本图片存储目录
* 这里省略模板ID模板的图片不处理删除的情况
* 因为用例会引用图片模板删除后图片也需要能访问
*/
private static final String PROJECT_TEMPLATE_IMG_DIR = PROJECT_DIR + "/template-img";
/**
* 项目模板压缩图片存储目录
* 这里省略模板ID模板的图片不处理删除的情况
* 因为用例会引用图片模板删除后图片也需要能访问
*/
private static final String PROJECT_TEMPLATE_IMG_PREVIEW_DIR = PROJECT_DIR + "/template-img/preview";
/**
* 接口用例相关文件的存储目录
* project/{projectId}/apiCase/{apiCaseId}
@ -145,4 +173,20 @@ public class DefaultRepositoryDir {
public static String getProjectDir(String projectId) {
return String.format(PROJECT_DIR, projectId);
}
public static String getOrgTemplateImgDir(String orgId) {
return String.format(ORGANIZATION_TEMPLATE_IMG_DIR, orgId);
}
public static String getOrgTemplateImgPreviewDir(String orgId) {
return String.format(ORGANIZATION_TEMPLATE_IMG_PREVIEW_DIR, orgId);
}
public static String getProjectTemplateImgDir(String projectId) {
return String.format(PROJECT_TEMPLATE_IMG_DIR, projectId);
}
public static String getProjectTemplateImgPreviewDir(String projectId) {
return String.format(PROJECT_TEMPLATE_IMG_PREVIEW_DIR, projectId);
}
}

View File

@ -40,9 +40,9 @@ public class FilterChainUtils {
//mock-server
filterChainDefinitionMap.put("/mock-server/**", "anon");
//功能用例文本访问
//功能用例文本访问
filterChainDefinitionMap.put("/attachment/download/file/**", "anon");
//用例评审文本访问
//用例评审文本访问
filterChainDefinitionMap.put("/review/functional/case/download/file/**", "anon");
//缺陷管理富文本访问
filterChainDefinitionMap.put("/bug/attachment/preview/md/**", "anon");

View File

@ -9,7 +9,8 @@ import io.metersphere.api.mapper.ApiFileResourceMapper;
import io.metersphere.project.dto.filemanagement.FileLogRecord;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.project.service.FileMetadataService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
@ -22,7 +23,6 @@ import io.metersphere.sdk.util.Translator;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -48,6 +48,8 @@ public class ApiFileResourceService {
private FileMetadataService fileMetadataService;
@Resource
private FileService fileService;
@Resource
private CommonFileService commonFileService;
/**
* 上传接口相关的资源文件
@ -56,56 +58,14 @@ public class ApiFileResourceService {
* @param addFileMap key:fileId value:fileName
*/
public void uploadFileResource(String folder, Map<String, String> addFileMap) {
if (MapUtils.isEmpty(addFileMap)) {
return;
}
FileRepository defaultRepository = FileCenter.getDefaultRepository();
for (String fileId : addFileMap.keySet()) {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
try {
String fileName = addFileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 按ID建文件夹避免文件名重复
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setCopyFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setCopyfileName(fileName);
fileCopyRequest.setFileName(fileName);
fileCopyRequest.setFolder(folder + "/" + fileId);
// 将文件从临时目录复制到资源目录
defaultRepository.copyFile(fileCopyRequest);
// 删除临时文件
fileCopyRequest.setFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setFileName(fileName);
defaultRepository.delete(fileCopyRequest);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"));
}
}
commonFileService.saveFileFromTempFile(folder, addFileMap);
}
/**
* 根据文件ID查询minio中对应目录下的文件名称
*/
public String getTempFileNameByFileId(String fileId) {
FileRepository defaultRepository = FileCenter.getDefaultRepository();
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
try {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(systemTempDir + "/" + fileId);
List<String> folderFileNames = defaultRepository.getFolderFileNames(fileRequest);
if (CollectionUtils.isEmpty(folderFileNames)) {
return null;
}
String[] pathSplit = folderFileNames.getFirst().split("/");
return pathSplit[pathSplit.length - 1];
} catch (Exception e) {
LogUtils.error(e);
return null;
}
return commonFileService.getTempFileNameByFileId(fileId);
}
/**

View File

@ -11,7 +11,7 @@ import io.metersphere.project.dto.environment.EnvironmentConfig;
import io.metersphere.project.mapper.ExtEnvironmentMapper;
import io.metersphere.project.mapper.ExtProjectMapper;
import io.metersphere.project.service.EnvironmentService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.sdk.constants.ProjectApplicationType;
import io.metersphere.sdk.constants.StorageType;

View File

@ -17,6 +17,7 @@ import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.security.CheckOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.utils.Pager;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
@ -44,6 +45,8 @@ public class BugAttachmentController {
private BugAttachmentService bugAttachmentService;
@Resource
private FileAssociationService fileAssociationService;
@Resource
private CommonFileService commonFileService;
@GetMapping("/list/{bugId}")
@Operation(summary = "缺陷管理-附件-列表")
@ -142,7 +145,7 @@ public class BugAttachmentController {
@Operation(summary = "缺陷管理-富文本附件-上传")
@RequiresPermissions(logical = Logical.OR, value = {PermissionConstants.PROJECT_BUG_ADD, PermissionConstants.PROJECT_BUG_UPDATE, PermissionConstants.PROJECT_BUG_COMMENT})
public String upload(@RequestParam("file") MultipartFile file) throws Exception {
return bugAttachmentService.uploadMdFile(file);
return commonFileService.uploadTempImgFile(file);
}
@GetMapping(value = "/preview/md/{projectId}/{fileId}/{compressed}")

View File

@ -26,7 +26,6 @@ import io.metersphere.project.mapper.FileAssociationMapper;
import io.metersphere.project.mapper.FileMetadataMapper;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.project.service.FileMetadataService;
import io.metersphere.project.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.LocalRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
@ -39,20 +38,20 @@ import io.metersphere.sdk.util.*;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.mapper.BaseUserMapper;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.service.FileService;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@ -84,9 +83,8 @@ public class BugAttachmentService {
private FileAssociationService fileAssociationService;
@Resource
private BugLocalAttachmentMapper bugLocalAttachmentMapper;
@Value("50MB")
private DataSize maxFileSize;
@Resource
private CommonFileService commonFileService;
/**
* 查询缺陷的附件集合
@ -248,41 +246,6 @@ public class BugAttachmentService {
return upgradeFileId;
}
/**
* 上传MD文件
* @param file 文件
* @return 文件ID
*/
public String uploadMdFile(MultipartFile file) {
String fileName = StringUtils.trim(file.getOriginalFilename());
if (file.getSize() > maxFileSize.toBytes()) {
throw new MSException(Translator.get("file.size.is.too.large"));
}
if (StringUtils.isBlank(fileName)) {
throw new MSException(Translator.get("file.name.cannot.be.empty"));
}
String fileId = IDGenerator.nextStr();
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(file.getOriginalFilename());
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
try {
FileCenter.getDefaultRepository().saveFile(file, fileRequest);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
//图片文件自动生成预览图
byte[] previewImg = TempFileUtils.compressPic(file.getBytes());
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileRequest);
}
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(e.getMessage());
}
return fileId;
}
/**
* 同步平台附件到MS
* @param platform 平台对象
@ -657,7 +620,7 @@ public class BugAttachmentService {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
// 添加文件与功能用例的关联关系
Map<String, String> addFileMap = new HashMap<>();
LogUtils.info("开始上传文本里的附件");
LogUtils.info("开始上传文本里的附件");
List<BugLocalAttachment> localAttachments = fileIds.stream().map(fileId -> {
BugLocalAttachment localAttachment = new BugLocalAttachment();
String fileName = getTempFileNameByFileId(fileId);
@ -684,7 +647,9 @@ public class BugAttachmentService {
bugLocalAttachmentMapper.batchInsert(localAttachments);
// 上传文件到对象存储
LogUtils.info("upload to minio start");
uploadFileResource(DefaultRepositoryDir.getBugDir(projectId, bugId), addFileMap, projectId, bugId);
String bugDir = DefaultRepositoryDir.getBugDir(projectId, bugId);
String bugPreviewDir = DefaultRepositoryDir.getBugPreviewDir(projectId, bugId);
commonFileService.saveReviewImgFromTempFile(bugDir, bugPreviewDir, addFileMap);
LogUtils.info("upload to minio end");
}
@ -692,69 +657,7 @@ public class BugAttachmentService {
* 根据文件ID查询MINIO中对应目录下的文件名称
*/
public String getTempFileNameByFileId(String fileId) {
FileRepository defaultRepository = FileCenter.getDefaultRepository();
try {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
List<String> folderFileNames = defaultRepository.getFolderFileNames(fileRequest);
if (CollectionUtils.isEmpty(folderFileNames)) {
return null;
}
String[] pathSplit = folderFileNames.getFirst().split("/");
return pathSplit[pathSplit.length - 1];
} catch (Exception e) {
LogUtils.error(e);
return null;
}
}
/**
* 上传文件到资源目录
* @param folder 文件夹
* @param addFileMap 文件ID与文件名映射
* @param projectId 项目ID
* @param bugId 缺陷ID
*/
public void uploadFileResource(String folder, Map<String, String> addFileMap, String projectId, String bugId) {
if (MapUtils.isEmpty(addFileMap)) {
return;
}
FileRepository defaultRepository = FileCenter.getDefaultRepository();
for (String fileId : addFileMap.keySet()) {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
try {
String fileName = addFileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 按ID建文件夹避免文件名重复
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setCopyFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setCopyfileName(fileName);
fileCopyRequest.setFileName(fileName);
fileCopyRequest.setFolder(folder + "/" + fileId);
// 将文件从临时目录复制到资源目录
defaultRepository.copyFile(fileCopyRequest);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
//图片文件自动生成预览图
byte[] file = defaultRepository.getFile(fileCopyRequest);
byte[] previewImg = TempFileUtils.compressPic(file);
fileCopyRequest.setFolder(DefaultRepositoryDir.getBugPreviewDir(projectId, bugId) + "/" + fileId);
fileCopyRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileCopyRequest);
}
// 删除临时文件
fileCopyRequest.setFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setFileName(fileName);
defaultRepository.delete(fileCopyRequest);
} catch (Exception e) {
LogUtils.error("上传副文本文件失败:{}",e);
throw new MSException(Translator.get("file_upload_fail"));
}
}
return commonFileService.getTempFileNameByFileId(fileId);
}
public ResponseEntity<byte[]> previewMd(String projectId, String fileId, boolean compressed) {
@ -766,7 +669,7 @@ public class BugAttachmentService {
if (CollectionUtils.isEmpty(bugAttachments)) {
//在临时文件获取
fileName = getTempFileNameByFileId(fileId);
bytes = getPreviewImg(fileName, fileId, compressed);
bytes = commonFileService.downloadTempImg(fileId, fileName, compressed);
} else {
//在正式目录获取
BugLocalAttachment attachment = bugAttachments.getFirst();
@ -783,51 +686,4 @@ public class BugAttachmentService {
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(bytes);
}
public byte[] getPreviewImg(String fileName, String fileId, boolean isCompressed) {
String systemTempDir;
if (isCompressed) {
systemTempDir = DefaultRepositoryDir.getSystemTempCompressDir();
} else {
systemTempDir = DefaultRepositoryDir.getSystemTempDir();
}
FileRequest previewRequest = new FileRequest();
previewRequest.setFileName(fileName);
previewRequest.setStorage(StorageType.MINIO.name());
previewRequest.setFolder(systemTempDir + "/" + fileId);
byte[] previewImg = null;
try {
previewImg = fileService.download(previewRequest);
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
if (previewImg == null || previewImg.length == 0) {
try {
if (isCompressed) {
previewImg = this.compressPicWithFileMetadata(fileName, fileId);
previewRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
fileService.upload(previewImg, previewRequest);
}
return previewImg;
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
}
return previewImg;
}
//获取文件并压缩的方法需要上锁防止并发超过一定数量时内存溢出
private synchronized byte[] compressPicWithFileMetadata(String fileName, String fileId) throws Exception {
byte[] fileBytes = this.getFile(fileName, fileId);
return TempFileUtils.compressPic(fileBytes);
}
public byte[] getFile(String fileName, String fileId) throws Exception {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
return fileService.download(fileRequest);
}
}

View File

@ -13,7 +13,7 @@ import io.metersphere.project.domain.FileAssociationExample;
import io.metersphere.project.dto.ProjectUserDTO;
import io.metersphere.project.mapper.FileAssociationMapper;
import io.metersphere.project.request.ProjectMemberRequest;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.project.service.ProjectMemberService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;

View File

@ -20,7 +20,7 @@ import io.metersphere.project.mapper.FileAssociationMapper;
import io.metersphere.project.mapper.FileMetadataMapper;
import io.metersphere.project.mapper.ProjectApplicationMapper;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.StorageType;

View File

@ -4,7 +4,9 @@ import io.metersphere.bug.dto.response.BugFileDTO;
import io.metersphere.plugin.platform.spi.Platform;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.service.CommonFileService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
@ -38,6 +40,8 @@ public class BugSyncExtraServiceTests extends BaseTest {
MinioRepository minioMock;
@Resource
private BugAttachmentService bugAttachmentService;
@Resource
private CommonFileService commonFileService;
@Test
@Order(1)
@ -47,8 +51,8 @@ public class BugSyncExtraServiceTests extends BaseTest {
// Mock minio upload exception
MockMultipartFile file = new MockMultipartFile("file", "test.txt", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
Mockito.doThrow(new MSException("save minio error!")).when(minioMock).saveFile(Mockito.eq(file), Mockito.any());
MSException uploadException = assertThrows(MSException.class, () -> bugAttachmentService.uploadMdFile(file));
assertEquals(uploadException.getMessage(), "save minio error!");
MSException uploadException = assertThrows(MSException.class, () -> commonFileService.uploadTempImgFile(file));
assertEquals(uploadException.getMessage(), Translator.get("file_upload_fail"));
// Mock minio delete exception
Mockito.doThrow(new MSException("delete minio error!")).when(minioMock).delete(Mockito.any());
MSException deleteException = assertThrows(MSException.class, () ->

View File

@ -23,6 +23,7 @@ import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.security.CheckOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.utils.Pager;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
@ -59,6 +60,9 @@ public class FunctionalCaseAttachmentController {
@Resource
private FileModuleService fileModuleService;
@Resource
private CommonFileService commonFileService;
@PostMapping("/page")
@Operation(summary = "用例管理-功能用例-附件-关联文件列表分页接口")
@ -70,7 +74,7 @@ public class FunctionalCaseAttachmentController {
@PostMapping("/preview")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件预览")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件预览")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public ResponseEntity<byte[]> preview(@Validated @RequestBody FunctionalCaseFileRequest request) throws Exception {
@ -84,7 +88,7 @@ public class FunctionalCaseAttachmentController {
}
@PostMapping("/download")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件下载")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件下载")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public ResponseEntity<byte[]> download(@Validated @RequestBody FunctionalCaseFileRequest request) throws Exception {
@ -178,14 +182,14 @@ public class FunctionalCaseAttachmentController {
}
@PostMapping("/upload/temp/file")
@Operation(summary = "用例管理-功能用例-上传文本里所需的文件资源并返回文件ID")
@Operation(summary = "用例管理-功能用例-上传文本里所需的文件资源并返回文件ID")
@RequiresPermissions(logical = Logical.OR, value = {PermissionConstants.FUNCTIONAL_CASE_READ_ADD, PermissionConstants.FUNCTIONAL_CASE_READ_UPDATE, PermissionConstants.FUNCTIONAL_CASE_READ_COMMENT})
public String upload(@RequestParam("file") MultipartFile file) throws Exception {
return functionalCaseAttachmentService.uploadTemp(file);
return commonFileService.uploadTempImgFile(file);
}
@GetMapping(value = "/download/file/{projectId}/{fileId}/{compressed}")
@Operation(summary = "用例管理-功能用例-预览上传的文本里所需的文件资源原图")
@Operation(summary = "用例管理-功能用例-预览上传的文本里所需的文件资源原图")
public ResponseEntity<byte[]> downloadImgById(@PathVariable String projectId, @PathVariable String fileId, @Schema(description = "查看压缩图片", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable("compressed") boolean compressed) {
return functionalCaseAttachmentService.downloadImgById(projectId, fileId, compressed);

View File

@ -7,6 +7,7 @@ import io.metersphere.functional.service.FunctionalCaseAttachmentService;
import io.metersphere.functional.service.ReviewFunctionalCaseService;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.security.CheckOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -26,9 +27,12 @@ public class ReviewFunctionalCaseController {
@Resource
private ReviewFunctionalCaseService reviewFunctionalCaseService;
@Resource
private FunctionalCaseAttachmentService functionalCaseAttachmentService;
@Resource
private CommonFileService commonFileService;
@PostMapping("/save")
@Operation(summary = "用例管理-用例评审-评审功能用例-提交评审")
@ -46,14 +50,14 @@ public class ReviewFunctionalCaseController {
}
@PostMapping("/upload/temp/file")
@Operation(summary = "用例管理-用例评审-上传文本里所需的文件资源并返回文件ID")
@Operation(summary = "用例管理-用例评审-上传文本里所需的文件资源并返回文件ID")
@RequiresPermissions(PermissionConstants.CASE_REVIEW_REVIEW)
public String upload(@RequestParam("file") MultipartFile file) throws Exception {
return functionalCaseAttachmentService.uploadTemp(file);
return commonFileService.uploadTempImgFile(file);
}
@PostMapping("/preview")
@Operation(summary = "用例管理-用例评审-附件/文本(原图/文件)-文件预览")
@Operation(summary = "用例管理-用例评审-附件/文本(原图/文件)-文件预览")
@RequiresPermissions(PermissionConstants.CASE_REVIEW_REVIEW)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public ResponseEntity<byte[]> preview(@Validated @RequestBody FunctionalCaseFileRequest request) throws Exception {
@ -62,7 +66,7 @@ public class ReviewFunctionalCaseController {
}
@PostMapping("/download")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件下载")
@Operation(summary = "用例管理-功能用例-附件/文本(原图/文件)-文件下载")
@RequiresPermissions(PermissionConstants.CASE_REVIEW_REVIEW)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public ResponseEntity<byte[]> download(@Validated @RequestBody FunctionalCaseFileRequest request) throws Exception {
@ -70,7 +74,7 @@ public class ReviewFunctionalCaseController {
}
@GetMapping(value = "/download/file/{projectId}/{fileId}/{compressed}")
@Operation(summary = "用例管理-功能用例-预览上传的文本里所需的文件资源原图")
@Operation(summary = "用例管理-功能用例-预览上传的文本里所需的文件资源原图")
public ResponseEntity<byte[]> downloadImgById(@PathVariable String projectId, @PathVariable String fileId, @PathVariable boolean compressed) throws Exception {
return functionalCaseAttachmentService.downloadImgById(projectId, fileId, compressed);
}

View File

@ -23,7 +23,7 @@ public class BatchReviewFunctionalCaseRequest extends BaseReviewCaseBatchRequest
@Schema(description = "评论@的人的Id, 多个以';'隔开")
private String notifier;
@Schema(description = "用例评审评论文本的文件id集合")
@Schema(description = "用例评审评论文本的文件id集合")
private List<String> reviewCommentFileIds;
}

View File

@ -16,7 +16,7 @@ public class FunctionalCaseEditRequest extends FunctionalCaseAddRequest {
@NotBlank(message = "{functional_case.id.not_blank}")
private String id;
@Schema(description = "删除本地上传(文本里)的文件id")
@Schema(description = "删除本地上传(文本里)的文件id")
private List<String> deleteFileMetaIds;
@Schema(description = "取消关联的文件id")

View File

@ -35,6 +35,6 @@ public class ReviewFunctionalCaseRequest {
@Schema(description = "评论@的人的Id, 多个以';'隔开")
private String notifier;
@Schema(description = "用例评审评论文本的文件id集合")
@Schema(description = "用例评审评论文本的文件id集合")
private List<String> reviewCommentFileIds;
}

View File

@ -15,7 +15,8 @@ import io.metersphere.project.domain.FileAssociation;
import io.metersphere.project.dto.filemanagement.FileInfo;
import io.metersphere.project.dto.filemanagement.FileLogRecord;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
@ -64,6 +65,9 @@ public class FunctionalCaseAttachmentService {
@Resource
private FileAssociationService fileAssociationService;
@Resource
private CommonFileService commonFileService;
private static final String UPLOAD_FILE = "/attachment/upload/file";
private static final String DELETED_FILE = "/attachment/delete/file";
@ -364,39 +368,6 @@ public class FunctionalCaseAttachmentService {
}
}
public String uploadTemp(MultipartFile file) {
String fileName = StringUtils.trim(file.getOriginalFilename());
if (file.getSize() > maxFileSize.toBytes()) {
throw new MSException(Translator.get("file.size.is.too.large"));
}
if (StringUtils.isBlank(fileName)) {
throw new MSException(Translator.get("file.name.cannot.be.empty"));
}
String fileId = IDGenerator.nextStr();
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(file.getOriginalFilename());
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
fileRequest.setFolder(systemTempDir + "/" + fileId);
try {
FileCenter.getDefaultRepository()
.saveFile(file, fileRequest);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
//图片文件自动生成预览图
byte[] previewImg = TempFileUtils.compressPic(file.getBytes());
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileRequest);
}
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"));
}
return fileId;
}
public void uploadMinioFile(String caseId, String projectId, List<String> uploadFileIds, String userId, String fileSource) {
if (CollectionUtils.isEmpty(uploadFileIds)) {
return;
@ -416,7 +387,7 @@ public class FunctionalCaseAttachmentService {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
// 添加文件与功能用例的关联关系
Map<String, String> addFileMap = new HashMap<>();
LogUtils.info("开始上传文本里的附件");
LogUtils.info("开始上传文本里的附件");
List<FunctionalCaseAttachment> functionalCaseAttachments = filIds.stream().map(fileId -> {
FunctionalCaseAttachment functionalCaseAttachment = new FunctionalCaseAttachment();
String fileName = getTempFileNameByFileId(fileId);
@ -445,69 +416,40 @@ public class FunctionalCaseAttachmentService {
// 上传文件到对象存储
LogUtils.info("上传文件到对象存储");
uploadFileResource(functionalCaseDir, addFileMap, projectId, caseId);
LogUtils.info("上传文本里的附件结束");
LogUtils.info("上传文本里的附件结束");
}
/**
* 根据文件ID查询minio中对应目录下的文件名称
*/
public String getTempFileNameByFileId(String fileId) {
FileRepository defaultRepository = FileCenter.getDefaultRepository();
try {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
List<String> folderFileNames = defaultRepository.getFolderFileNames(fileRequest);
if (CollectionUtils.isEmpty(folderFileNames)) {
return null;
}
String[] pathSplit = folderFileNames.getFirst().split("/");
return pathSplit[pathSplit.length - 1];
} catch (Exception e) {
LogUtils.error(e);
return null;
}
return commonFileService.getTempFileNameByFileId(fileId);
}
/**
* 上传用例管理相关的资源文件
*
* @param folder 用例管理文件路径
* @param addFileMap key:fileId value:fileName
* @param fileMap key:fileId value:fileName
*/
public void uploadFileResource(String folder, Map<String, String> addFileMap, String projectId, String caseId) {
if (MapUtils.isEmpty(addFileMap)) {
public void uploadFileResource(String folder, Map<String, String> fileMap, String projectId, String caseId) {
if (MapUtils.isEmpty(fileMap)) {
return;
}
FileRepository defaultRepository = FileCenter.getDefaultRepository();
for (String fileId : addFileMap.keySet()) {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
for (String fileId : fileMap.keySet()) {
try {
String fileName = addFileMap.get(fileId);
String fileName = fileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 按ID建文件夹避免文件名重复
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setCopyFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setCopyfileName(fileName);
fileCopyRequest.setFileName(fileName);
fileCopyRequest.setFolder(folder + "/" + fileId);
// 将文件从临时目录复制到资源目录
defaultRepository.copyFile(fileCopyRequest);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
//图片文件自动生成预览图
byte[] file = defaultRepository.getFile(fileCopyRequest);
byte[] previewImg = TempFileUtils.compressPic(file);
fileCopyRequest.setFolder(DefaultRepositoryDir.getFunctionalCasePreviewDir(projectId, caseId) + "/" + fileId);
fileCopyRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileCopyRequest);
}
// 将临时文件移动到指定文件夹
commonFileService.moveTempFileToFolder(fileId, fileName, folder);
// 将文件从临时目录移动到指定的图片预览目录
commonFileService.moveTempFileToImgReviewFolder(DefaultRepositoryDir.getFunctionalCasePreviewDir(projectId, caseId), fileId, fileName);
// 这里不删除临时文件批量评审需要保留copy多次文件到正式目录
} catch (Exception e) {
LogUtils.error("上传副文本文件失败:{}",e);
throw new MSException(Translator.get("file_upload_fail"));
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"), e);
}
}
}
@ -526,7 +468,7 @@ public class FunctionalCaseAttachmentService {
if (CollectionUtils.isEmpty(caseAttachments)) {
//在临时文件获取
fileName = getTempFileNameByFileId(fileId);
bytes = getPreviewImg(fileName, fileId, compressed);
bytes = commonFileService.downloadTempImg(fileId, fileName, compressed);
} else {
//在正式目录获取
FunctionalCaseAttachment attachment = caseAttachments.getFirst();
@ -550,52 +492,4 @@ public class FunctionalCaseAttachmentService {
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(bytes);
}
public byte[] getPreviewImg(String fileName, String fileId, boolean isCompressed) {
String systemTempDir;
if (isCompressed) {
systemTempDir = DefaultRepositoryDir.getSystemTempCompressDir();
} else {
systemTempDir = DefaultRepositoryDir.getSystemTempDir();
}
FileRequest previewRequest = new FileRequest();
previewRequest.setFileName(fileName);
previewRequest.setStorage(StorageType.MINIO.name());
previewRequest.setFolder(systemTempDir + "/" + fileId);
byte[] previewImg = null;
try {
previewImg = fileService.download(previewRequest);
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
if (previewImg == null || previewImg.length == 0) {
try {
if (isCompressed) {
previewImg = this.compressPicWithFileMetadata(fileName, fileId);
previewRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
fileService.upload(previewImg, previewRequest);
}
return previewImg;
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
}
return previewImg;
}
//获取文件并压缩的方法需要上锁防止并发超过一定数量时内存溢出
private synchronized byte[] compressPicWithFileMetadata(String fileName, String fileId) throws Exception {
byte[] fileBytes = this.getFile(fileName, fileId);
return TempFileUtils.compressPic(fileBytes);
}
public byte[] getFile(String fileName, String fileId) throws Exception {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
return fileService.download(fileRequest);
}
}

View File

@ -189,7 +189,7 @@ public class FunctionalCaseService {
//上传文件
List<String> uploadFileIds = functionalCaseAttachmentService.uploadFile(request.getProjectId(), caseId, files, true, userId);
//上传文本里的文件
//上传文本里的文件
functionalCaseAttachmentService.uploadMinioFile(caseId, request.getProjectId(), request.getCaseDetailFileIds(), userId, CaseFileSourceType.CASE_DETAIL.toString());
//关联附件
@ -562,7 +562,7 @@ public class FunctionalCaseService {
//上传新文件
functionalCaseAttachmentService.uploadFile(request.getProjectId(), request.getId(), files, true, userId);
//上传文本文件
//上传文本文件
functionalCaseAttachmentService.uploadMinioFile(request.getId(), request.getProjectId(), request.getCaseDetailFileIds(), userId, CaseFileSourceType.CASE_DETAIL.toString());
//关联新附件

View File

@ -95,7 +95,7 @@ public class ReviewFunctionalCaseService {
extCaseReviewFunctionalCaseMapper.updateStatus(caseId, reviewId, functionalCaseStatus);
caseReviewHistoryMapper.insert(caseReviewHistory);
//保存文本评论附件
//保存文本评论附件
functionalCaseAttachmentService.uploadMinioFile(caseId, request.getProjectId(), request.getReviewCommentFileIds(), userId, CaseFileSourceType.REVIEW_COMMENT.toString());
//检查是否有@发送@通知

View File

@ -10,7 +10,7 @@ import io.metersphere.functional.utils.FileBaseUtils;
import io.metersphere.project.dto.filemanagement.request.FileMetadataTableRequest;
import io.metersphere.project.dto.filemanagement.request.FileUploadRequest;
import io.metersphere.project.service.FileMetadataService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
@ -260,7 +260,6 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest {
Map<String, String> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put(fileId, null);
functionalCaseAttachmentService.uploadFileResource(functionalCaseDir, objectObjectHashMap, "WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1");
}
@Test

View File

@ -18,7 +18,7 @@ import io.metersphere.functional.service.ReviewFunctionalCaseService;
import io.metersphere.functional.utils.FileBaseUtils;
import io.metersphere.project.dto.filemanagement.request.FileUploadRequest;
import io.metersphere.project.service.FileMetadataService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.constants.StorageType;

View File

@ -10,6 +10,7 @@ import io.metersphere.system.dto.sdk.request.TemplateUpdateRequest;
import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.security.CheckProjectOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.utils.SessionUtils;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
@ -17,9 +18,12 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@ -35,6 +39,8 @@ public class ProjectTemplateController {
@Resource
private ProjectTemplateService projectTemplateservice;
@Resource
private CommonFileService commonFileService;
@GetMapping("/list/{projectId}/{scene}")
@Operation(summary = "获取模版列表")
@ -94,4 +100,20 @@ public class ProjectTemplateController {
public Map<String, Boolean> getProjectTemplateEnableConfig(@PathVariable String projectId) {
return projectTemplateservice.getProjectTemplateEnableConfig(projectId);
}
@PostMapping("/upload/temp/img")
@Operation(summary = "上传富文本图片并返回文件ID")
@RequiresPermissions(value = {PermissionConstants.PROJECT_TEMPLATE_UPDATE, PermissionConstants.PROJECT_TEMPLATE_ADD}, logical = Logical.OR)
public String upload(@RequestParam("file") MultipartFile file) {
return commonFileService.uploadTempImgFile(file);
}
@GetMapping(value = "/img/preview/{organizationId}/{fileId}/{compressed}")
@Operation(summary = "富文本图片-预览")
public ResponseEntity<byte[]> previewImg(@PathVariable String organizationId,
@PathVariable String fileId,
@Schema(description = "是否是压缩图片")
@PathVariable("compressed") boolean compressed) {
return projectTemplateservice.previewImg(organizationId, fileId, compressed);
}
}

View File

@ -16,6 +16,7 @@ import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.TempFileUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.service.FileService;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

View File

@ -24,6 +24,7 @@ import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.util.*;
import io.metersphere.system.mapper.BaseUserMapper;
import io.metersphere.system.service.FileService;
import io.metersphere.system.uid.IDGenerator;
import io.metersphere.system.utils.PageUtils;
import io.metersphere.system.utils.Pager;

View File

@ -23,6 +23,7 @@ import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginWrapper;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -311,7 +312,9 @@ public class ProjectTemplateService extends BaseTemplateService {
checkProjectTemplateEnable(template.getScopeId(), template.getScene());
template.setScopeType(TemplateScopeType.PROJECT.name());
template.setRefId(null);
return super.add(template, request.getCustomFields(), request.getSystemFields());
template = super.add(template, request.getCustomFields(), request.getSystemFields());
saveUploadImages(request);
return template;
}
public void checkProjectResourceExist(Template template) {
@ -330,7 +333,9 @@ public class ProjectTemplateService extends BaseTemplateService {
template.setScopeId(originTemplate.getScopeId());
template.setScene(originTemplate.getScene());
checkProjectResourceExist(originTemplate);
return super.update(template, request.getCustomFields(), request.getSystemFields());
template = super.update(template, request.getCustomFields(), request.getSystemFields());
saveUploadImages(request);
return template;
}
@Override
@ -438,4 +443,29 @@ public class ProjectTemplateService extends BaseTemplateService {
}
return new ArrayList<>();
}
/**
* 保存上传的文件
* 将文件从临时目录移动到正式目录
*
* @param request
*/
private void saveUploadImages(TemplateUpdateRequest request) {
String projectTemplateDir = DefaultRepositoryDir.getProjectTemplateImgDir(request.getScopeId());
String projectTemplatePreviewDir = DefaultRepositoryDir.getProjectTemplateImgPreviewDir(request.getScopeId());
commonFileService.saveReviewImgFromTempFile(projectTemplateDir, projectTemplatePreviewDir, request.getUploadImgFileIds());
}
/**
* 富文本框图片预览
* @param projectId
* @param fileId
* @param compressed
* @return
*/
public ResponseEntity<byte[]> previewImg(String projectId, String fileId, boolean compressed) {
String projectTemplateDir = DefaultRepositoryDir.getProjectTemplateImgDir(projectId);
String projectTemplatePreviewDir = DefaultRepositoryDir.getProjectTemplateImgPreviewDir(projectId);
return super.previewImg(fileId, projectTemplateDir, projectTemplatePreviewDir, compressed);
}
}

View File

@ -4,15 +4,16 @@ import io.metersphere.project.dto.ProjectTemplateDTO;
import io.metersphere.project.dto.ProjectTemplateOptionDTO;
import io.metersphere.project.service.ProjectTemplateLogService;
import io.metersphere.project.service.ProjectTemplateService;
import io.metersphere.sdk.constants.OrganizationParameterConstants;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.constants.TemplateScopeType;
import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.base.BasePluginTestService;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.OrganizationTemplateControllerTests;
import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.controller.param.TemplateUpdateRequestDefinition;
import io.metersphere.system.domain.CustomField;
import io.metersphere.system.domain.OrganizationParameter;
@ -29,6 +30,7 @@ import io.metersphere.system.service.BaseCustomFieldService;
import io.metersphere.system.service.BaseTemplateCustomFieldService;
import io.metersphere.system.service.BaseTemplateService;
import io.metersphere.system.service.UserLoginService;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
@ -36,6 +38,8 @@ import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
@ -51,6 +55,7 @@ import static io.metersphere.project.enums.result.ProjectResultCode.PROJECT_TEMP
import static io.metersphere.sdk.constants.InternalUserRole.ADMIN;
import static io.metersphere.system.controller.handler.result.CommonResultCode.*;
import static io.metersphere.system.controller.handler.result.MsHttpResultCode.NOT_FOUND;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author jianxing
@ -63,7 +68,9 @@ public class ProjectTemplateControllerTests extends BaseTest {
private static final String BASE_PATH = "/project/template/";
private static final String LIST = "list/{0}/{1}";
private static final String SET_DEFAULT = "set-default/{0}/{1}";
protected static final String PROJECT_TEMPLATE_ENABLE_CONFIG = "enable/config/{0}";
protected static final String ENABLE_CONFIG = "enable/config/{0}";
protected static final String UPLOAD_TEMP_IMG = "upload/temp/img";
protected static final String IMG_PREVIEW = "/img/preview/{0}/{1}/{2}";
@Resource
private TemplateMapper templateMapper;
@ -103,6 +110,35 @@ public class ProjectTemplateControllerTests extends BaseTest {
this.requestGetWithOkAndReturn(LIST, DEFAULT_PROJECT_ID, TemplateScene.BUG.name());
}
@Test
@Order(0)
public void uploadTempFile() throws Exception {
// 准备数据上传文件管理文件
MockMultipartFile file = new MockMultipartFile("file", IDGenerator.nextStr() + "_file_upload.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
// @@请求成功
String fileId = doUploadTempFile(file);
// 校验文件存在
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
fileRequest.setFileName(file.getOriginalFilename());
Assertions.assertNotNull(FileCenter.getDefaultRepository().getFile(fileRequest));
requestUploadPermissionTest(PermissionConstants.PROJECT_TEMPLATE_UPDATE, UPLOAD_TEMP_IMG, file);
requestUploadPermissionTest(PermissionConstants.PROJECT_TEMPLATE_ADD, UPLOAD_TEMP_IMG, file);
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, fileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, fileId, false)).andExpect(status().isOk());
}
private String doUploadTempFile(MockMultipartFile file) throws Exception {
return JSON.parseObject(requestUploadFileWithOkAndReturn(UPLOAD_TEMP_IMG, file)
.getResponse()
.getContentAsString(), ResultHolder.class)
.getData().toString();
}
@Test
@Order(1)
public void add() throws Exception {
@ -153,8 +189,15 @@ public class ProjectTemplateControllerTests extends BaseTest {
request.setScopeId(DEFAULT_PROJECT_ID);
request.setCustomFields(null);
request.setSystemFields(null);
String uploadFileId = doUploadTempFile(OrganizationTemplateControllerTests.getMockMultipartFile("api-add-file_upload.JPG"));
request.setUploadImgFileIds(List.of(uploadFileId));
MvcResult anotherMvcResult = this.requestPostWithOkAndReturn(DEFAULT_ADD, request);
this.anotherTemplateField = templateMapper.selectByPrimaryKey(getResultData(anotherMvcResult, Template.class).getId());
assertUploadFile(uploadFileId, "api-add-file_upload.JPG");
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, uploadFileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, uploadFileId, false)).andExpect(status().isOk());
request.setUploadImgFileIds(null);
// @@校验日志
checkLog(this.addTemplate.getId(), OperationLogType.ADD);
@ -164,6 +207,19 @@ public class ProjectTemplateControllerTests extends BaseTest {
requestPostPermissionTest(PermissionConstants.PROJECT_TEMPLATE_ADD, DEFAULT_ADD, request);
}
/**
* 校验上传的文件
*
*/
public static void assertUploadFile(String fileId, String fileName) throws Exception {
String projectTemplateImgDir = DefaultRepositoryDir.getProjectTemplateImgDir(DEFAULT_PROJECT_ID);
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(projectTemplateImgDir + "/" + fileId);
fileRequest.setFileName(fileName);
Assertions.assertNotNull(FileCenter.getDefaultRepository().getFile(fileRequest));
}
private TemplateCustomFieldRequest getTemplateCustomFieldRequest(String scene) {
List<CustomField> customFields = baseCustomFieldService.getByScopeIdAndScene(DEFAULT_PROJECT_ID, scene);
CustomField customField = customFields.stream()
@ -225,8 +281,16 @@ public class ProjectTemplateControllerTests extends BaseTest {
// 不更新字段
request.setCustomFields(null);
String uploadFileId = doUploadTempFile(OrganizationTemplateControllerTests.getMockMultipartFile("api-add-file_upload.JPG"));
request.setUploadImgFileIds(List.of(uploadFileId));
request.setScopeId(DEFAULT_PROJECT_ID);
this.requestPostWithOk(DEFAULT_UPDATE, request);
Assertions.assertEquals(baseTemplateCustomFieldService.getByTemplateId(template.getId()).size(), 3);
assertUploadFile(uploadFileId, "api-add-file_upload.JPG");
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, uploadFileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_PROJECT_ID, uploadFileId, false)).andExpect(status().isOk());
request.setUploadImgFileIds(null);
// @校验是否开启项目模板
changeOrgTemplateEnable(true);
@ -427,20 +491,20 @@ public class ProjectTemplateControllerTests extends BaseTest {
public void getProjectTemplateEnableConfig() throws Exception {
changeOrgTemplateEnable(true);
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(PROJECT_TEMPLATE_ENABLE_CONFIG, DEFAULT_PROJECT_ID);
MvcResult mvcResult = this.requestGetWithOkAndReturn(ENABLE_CONFIG, DEFAULT_PROJECT_ID);
Map resultData = getResultData(mvcResult, Map.class);
Assertions.assertEquals(resultData.size(), TemplateScene.values().length);
Assertions.assertFalse((Boolean) resultData.get(TemplateScene.FUNCTIONAL.name()));
changeOrgTemplateEnable(false);
mvcResult = this.requestGetWithOkAndReturn(PROJECT_TEMPLATE_ENABLE_CONFIG, DEFAULT_PROJECT_ID);
mvcResult = this.requestGetWithOkAndReturn(ENABLE_CONFIG, DEFAULT_PROJECT_ID);
Assertions.assertTrue((Boolean) getResultData(mvcResult, Map.class).get(TemplateScene.FUNCTIONAL.name()));
changeOrgTemplateEnable(true);
// @@校验 NOT_FOUND 异常
assertErrorCode(this.requestGet(PROJECT_TEMPLATE_ENABLE_CONFIG,"1111"), NOT_FOUND);
assertErrorCode(this.requestGet(ENABLE_CONFIG,"1111"), NOT_FOUND);
// @@校验权限
requestGetPermissionTest(PermissionConstants.PROJECT_TEMPLATE_READ, PROJECT_TEMPLATE_ENABLE_CONFIG, DEFAULT_PROJECT_ID);
requestGetPermissionTest(PermissionConstants.PROJECT_TEMPLATE_READ, ENABLE_CONFIG, DEFAULT_PROJECT_ID);
}
private void assertSetDefaultTemplate(Template template) {

View File

@ -14,7 +14,7 @@ import io.metersphere.project.mapper.FileModuleMapper;
import io.metersphere.project.service.FileAssociationService;
import io.metersphere.project.service.FileMetadataService;
import io.metersphere.project.service.FileModuleService;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.FileService;
import io.metersphere.project.utils.FileManagementBaseUtils;
import io.metersphere.project.utils.FileManagementRequestUtils;
import io.metersphere.project.utils.FileMetadataUtils;

View File

@ -7,6 +7,7 @@ import io.metersphere.system.dto.sdk.request.TemplateUpdateRequest;
import io.metersphere.system.log.annotation.Log;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.security.CheckOrgOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.service.OrganizationTemplateLogService;
import io.metersphere.system.service.OrganizationTemplateService;
import io.metersphere.system.utils.SessionUtils;
@ -16,9 +17,12 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@ -34,6 +38,8 @@ public class OrganizationTemplateController {
@Resource
private OrganizationTemplateService organizationTemplateService;
@Resource
private CommonFileService commonFileService;
@GetMapping("/list/{organizationId}/{scene}")
@Operation(summary = "获取模版列表")
@ -93,4 +99,20 @@ public class OrganizationTemplateController {
public Map<String, Boolean> getOrganizationTemplateEnableConfig(@PathVariable String organizationId) {
return organizationTemplateService.getOrganizationTemplateEnableConfig(organizationId);
}
@PostMapping("/upload/temp/img")
@Operation(summary = "上传富文本图片并返回文件ID")
@RequiresPermissions(value = {PermissionConstants.ORGANIZATION_TEMPLATE_UPDATE, PermissionConstants.ORGANIZATION_TEMPLATE_ADD}, logical = Logical.OR)
public String upload(@RequestParam("file") MultipartFile file) {
return commonFileService.uploadTempImgFile(file);
}
@GetMapping(value = "/img/preview/{organizationId}/{fileId}/{compressed}")
@Operation(summary = "富文本图片-预览")
public ResponseEntity<byte[]> previewImg(@PathVariable String organizationId,
@PathVariable String fileId,
@Schema(description = "查看压缩图片", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable("compressed") boolean compressed) {
return organizationTemplateService.previewImg(organizationId, fileId, compressed);
}
}

View File

@ -49,4 +49,7 @@ public class TemplateUpdateRequest {
@Valid
@Schema(title = "系统字段列表")
private List<TemplateSystemCustomFieldRequest> systemFields;
@Schema(description = "模板中新上传的文件ID列表")
private List<String> uploadImgFileIds;
}

View File

@ -1,8 +1,10 @@
package io.metersphere.system.service;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.constants.TemplateScopeType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
@ -20,6 +22,9 @@ import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -48,6 +53,10 @@ public class BaseTemplateService {
protected UserLoginService userLoginService;
@Resource
protected BaseCustomFieldService baseCustomFieldService;
@Resource
protected CommonFileService commonFileService;
@Resource
protected FileService fileService;
@Resource
private BaseCustomFieldOptionService baseCustomFieldOptionService;
@ -394,4 +403,30 @@ public class BaseTemplateService {
templateMapper.insert(template);
return template;
}
public ResponseEntity<byte[]> previewImg(String fileId, String imgFolder, String previewImgFolder, boolean compressed) {
byte[] bytes;
String fileName;
// 在临时文件获取文件名
fileName = commonFileService.getTempFileNameByFileId(fileId);
if (fileName != null) {
bytes = commonFileService.downloadTempImg(fileId, fileName, compressed);
} else {
// 没有则在正式目录获取
FileRequest fileRequest = new FileRequest();
String folder = compressed ? imgFolder : previewImgFolder;
fileRequest.setFolder(folder + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
fileRequest.setFileName(commonFileService.getFileNameByFileId(fileId, folder));
try {
bytes = fileService.download(fileRequest);
} catch (Exception e) {
throw new MSException("get file error");
}
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(bytes);
}
}

View File

@ -0,0 +1,295 @@
package io.metersphere.system.service;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileCopyRequest;
import io.metersphere.sdk.file.FileRepository;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.TempFileUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: jianxing
* @CreateTime: 2024-07-30 10:07
*/
@Service
public class CommonFileService {
@Value("50MB")
private DataSize maxFileSize;
@Resource
private FileService fileService;
/**
* 将图片文件上传到临时目录
* @param file 文件
* @return 文件ID
*/
public String uploadTempImgFile(MultipartFile file) {
String fileName = StringUtils.trim(file.getOriginalFilename());
if (file.getSize() > maxFileSize.toBytes()) {
throw new MSException(Translator.get("file.size.is.too.large"));
}
if (StringUtils.isBlank(fileName)) {
throw new MSException(Translator.get("file.name.cannot.be.empty"));
}
String fileId = IDGenerator.nextStr();
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
try {
FileCenter.getDefaultRepository().saveFile(file, fileRequest);
uploadTempReviewImg(file, fileId);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"), e);
}
return fileId;
}
private void uploadTempReviewImg(MultipartFile file, String fileId) throws Exception {
uploadReviewImg(file, fileId, DefaultRepositoryDir.getSystemTempCompressDir());
}
/**
* 上传预览的图片
* @param file
* @param fileId
* @param folder
* @throws Exception
*/
public void uploadReviewImg(MultipartFile file, String fileId, String folder) throws Exception {
String fileName = StringUtils.trim(file.getOriginalFilename());
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
// 图片文件自动生成预览图
byte[] previewImg = TempFileUtils.compressPic(file.getBytes());
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(folder + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileRequest);
}
}
/**
* 从临时文件夹中保存文件到指定文件夹
* 并删除临时文件
*
* @param folder 文件夹
* @param fileIds 临时文件ID列表
*/
public void saveReviewImgFromTempFile(String folder, String reviewFolder, List<String> fileIds) {
if (CollectionUtils.isEmpty(fileIds)) {
return;
}
Map<String, String> fileMap = new HashMap<>();
fileIds.forEach(fileId -> {
String fileName = getTempFileNameByFileId(fileId);
fileMap.put(fileId, fileName);
});
saveReviewImgFromTempFile(folder, reviewFolder, fileMap);
}
/**
* 从临时文件夹中保存文件到指定文件夹
* 并删除临时文件
*
* @param folder 文件夹
* @param fileMap key:fileId value:fileName
*/
public void saveReviewImgFromTempFile(String folder, String reviewFolder, Map<String, String> fileMap) {
if (MapUtils.isEmpty(fileMap)) {
return;
}
for (String fileId : fileMap.keySet()) {
try {
String fileName = fileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 将临时文件移动到指定文件夹
moveTempFileToFolder(fileId, fileName, folder);
// 将文件从临时目录移动到指定的图片预览目录
moveTempFileToImgReviewFolder(reviewFolder, fileId, fileName);
// 删除临时文件
deleteTempFile(fileId, fileName);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"), e);
}
}
}
/**
* 从临时文件夹中保存文件到指定文件夹
* 并删除临时文件
*
* @param folder 文件夹
* @param fileMap key:fileId value:fileName
*/
public void saveFileFromTempFile(String folder, Map<String, String> fileMap) {
if (MapUtils.isEmpty(fileMap)) {
return;
}
for (String fileId : fileMap.keySet()) {
try {
String fileName = fileMap.get(fileId);
if (StringUtils.isEmpty(fileName)) {
continue;
}
// 将临时文件移动到指定文件夹
moveTempFileToFolder(fileId, fileName, folder);
// 删除临时文件
deleteTempFile(fileId, fileName);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(Translator.get("file_upload_fail"), e);
}
}
}
/**
* 将文件从临时目录移动到指定的图片预览目录
* @param reviewFolder
* @param fileId
* @param fileName
* @throws Exception
*/
public void moveTempFileToImgReviewFolder(String reviewFolder, String fileId, String fileName) throws Exception {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
FileRepository defaultRepository = FileCenter.getDefaultRepository();
// 按ID建文件夹避免文件名重复
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(systemTempDir + "/" + fileId);
String fileType = StringUtils.substring(fileName, fileName.lastIndexOf(".") + 1);
if (TempFileUtils.isImage(fileType)) {
// 图片文件自动生成预览图
byte[] file = defaultRepository.getFile(fileRequest);
byte[] previewImg = TempFileUtils.compressPic(file);
fileRequest.setFolder(reviewFolder + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.toString());
fileService.upload(previewImg, fileRequest);
}
}
/**
* 将文件从临时目录移动到指定目录
* @param fileId
* @param fileName
* @param folder
* @throws Exception
*/
public void moveTempFileToFolder(String fileId, String fileName, String folder) throws Exception {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
FileRepository defaultRepository = FileCenter.getDefaultRepository();
// 按ID建文件夹避免文件名重复
FileCopyRequest fileCopyRequest = new FileCopyRequest();
fileCopyRequest.setCopyFolder(systemTempDir + "/" + fileId);
fileCopyRequest.setCopyfileName(fileName);
fileCopyRequest.setFileName(fileName);
fileCopyRequest.setFolder(folder + "/" + fileId);
// 将文件从临时目录复制到资源目录
defaultRepository.copyFile(fileCopyRequest);
}
/**
* 删除临时文件
* @param fileId
* @param fileName
* @throws Exception
*/
public void deleteTempFile(String fileId, String fileName) throws Exception {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
FileRepository defaultRepository = FileCenter.getDefaultRepository();
FileCopyRequest fileRequest = new FileCopyRequest();
fileRequest.setFolder(systemTempDir + "/" + fileId);
fileRequest.setFileName(fileName);
defaultRepository.delete(fileRequest);
}
/**
* 根据文件ID查询临时文件的文件名称
*/
public String getTempFileNameByFileId(String fileId) {
return getFileNameByFileId(fileId, DefaultRepositoryDir.getSystemTempDir());
}
/**
* 根据文件ID查询minio中对应目录下的文件名称
*/
public String getFileNameByFileId(String fileId, String folder) {
FileRepository defaultRepository = FileCenter.getDefaultRepository();
try {
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(folder + "/" + fileId);
List<String> folderFileNames = defaultRepository.getFolderFileNames(fileRequest);
if (CollectionUtils.isEmpty(folderFileNames)) {
return null;
}
String[] pathSplit = folderFileNames.getFirst().split("/");
return pathSplit[pathSplit.length - 1];
} catch (Exception e) {
LogUtils.error(e);
return null;
}
}
/**
* 从临时文件夹中下载图片
* @param fileId
* @param isCompressed
* @return
*/
public byte[] downloadTempImg(String fileId, String fileName, boolean isCompressed) {
String systemTempDir;
if (isCompressed) {
systemTempDir = DefaultRepositoryDir.getSystemTempCompressDir();
} else {
systemTempDir = DefaultRepositoryDir.getSystemTempDir();
}
FileRequest previewRequest = new FileRequest();
previewRequest.setFileName(fileName);
previewRequest.setStorage(StorageType.MINIO.name());
previewRequest.setFolder(systemTempDir + "/" + fileId);
byte[] previewImg = null;
try {
previewImg = fileService.download(previewRequest);
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
if (previewImg == null || previewImg.length == 0) {
try {
if (isCompressed) {
// 如果压缩文件夹没有图片则重新复制一份
moveTempFileToImgReviewFolder(systemTempDir, fileId, fileName);
previewImg = fileService.download(previewRequest);
}
return previewImg;
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
}
return previewImg;
}
}

View File

@ -1,4 +1,4 @@
package io.metersphere.project.service;
package io.metersphere.system.service;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRequest;

View File

@ -1,15 +1,16 @@
package io.metersphere.system.service;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.constants.TemplateScopeType;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.dto.sdk.request.TemplateCustomFieldRequest;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.SubListUtils;
import io.metersphere.system.domain.OrganizationParameter;
import io.metersphere.system.domain.Template;
import io.metersphere.system.domain.TemplateExample;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.dto.sdk.request.TemplateCustomFieldRequest;
import io.metersphere.system.dto.sdk.request.TemplateSystemCustomFieldRequest;
import io.metersphere.system.dto.sdk.request.TemplateUpdateRequest;
import io.metersphere.system.mapper.BaseProjectMapper;
@ -17,6 +18,7 @@ import io.metersphere.system.mapper.ExtOrganizationTemplateMapper;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -66,6 +68,7 @@ public class OrganizationTemplateService extends BaseTemplateService {
template = super.add(template, request.getCustomFields(), request.getSystemFields());
// 同步创建项目级别模板
addRefProjectTemplate(template, request.getCustomFields(), request.getSystemFields());
saveUploadImages(request);
return template;
}
@ -95,7 +98,6 @@ public class OrganizationTemplateService extends BaseTemplateService {
OrganizationService.checkResourceExist(template.getScopeId());
}
public Template update(TemplateUpdateRequest request) {
Template template = new Template();
BeanUtils.copyBean(template, request);
@ -110,7 +112,21 @@ public class OrganizationTemplateService extends BaseTemplateService {
checkOrgResourceExist(originTemplate);
updateRefProjectTemplate(template, request.getCustomFields(), request.getSystemFields());
template.setRefId(null);
return super.update(template, request.getCustomFields(), request.getSystemFields());
template = super.update(template, request.getCustomFields(), request.getSystemFields());
saveUploadImages(request);
return template;
}
/**
* 保存上传的文件
* 将文件从临时目录移动到正式目录
*
* @param request
*/
private void saveUploadImages(TemplateUpdateRequest request) {
String orgTemplateDir = DefaultRepositoryDir.getOrgTemplateImgDir(request.getScopeId());
String orgTemplatePreviewDir = DefaultRepositoryDir.getOrgTemplateImgPreviewDir(request.getScopeId());
commonFileService.saveReviewImgFromTempFile(orgTemplateDir, orgTemplatePreviewDir, request.getUploadImgFileIds());
}
/**
@ -195,6 +211,7 @@ public class OrganizationTemplateService extends BaseTemplateService {
/**
* 一个接口返回各个模板是否启用组织模板
*
* @param organizationId
* @return
*/
@ -206,4 +223,17 @@ public class OrganizationTemplateService extends BaseTemplateService {
templateEnableConfig.put(scene.name(), isOrganizationTemplateEnable(organizationId, scene.name())));
return templateEnableConfig;
}
/**
* 富文本框图片预览
* @param organizationId
* @param fileId
* @param compressed
* @return
*/
public ResponseEntity<byte[]> previewImg(String organizationId, String fileId, boolean compressed) {
String orgTemplateImgDir = DefaultRepositoryDir.getOrgTemplateImgDir(organizationId);
String orgTemplateImgPreviewDir = DefaultRepositoryDir.getOrgTemplateImgPreviewDir(organizationId);
return super.previewImg(fileId, orgTemplateImgDir, orgTemplateImgPreviewDir, compressed);
}
}

View File

@ -1,31 +1,36 @@
package io.metersphere.system.controller;
import io.metersphere.sdk.constants.OrganizationParameterConstants;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.TemplateScene;
import io.metersphere.sdk.constants.TemplateScopeType;
import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.base.BaseCustomFieldTestService;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.controller.param.TemplateUpdateRequestDefinition;
import io.metersphere.system.domain.*;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.dto.sdk.request.TemplateCustomFieldRequest;
import io.metersphere.system.dto.sdk.request.TemplateSystemCustomFieldRequest;
import io.metersphere.system.dto.sdk.request.TemplateUpdateRequest;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.system.base.BaseCustomFieldTestService;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.param.TemplateUpdateRequestDefinition;
import io.metersphere.system.domain.*;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.mapper.OrganizationParameterMapper;
import io.metersphere.system.mapper.TemplateMapper;
import io.metersphere.system.service.*;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MvcResult;
import java.util.ArrayList;
@ -36,6 +41,8 @@ import static io.metersphere.sdk.constants.InternalUserRole.ADMIN;
import static io.metersphere.system.controller.handler.result.CommonResultCode.*;
import static io.metersphere.system.controller.handler.result.MsHttpResultCode.NOT_FOUND;
import static io.metersphere.system.controller.result.SystemResultCode.ORGANIZATION_TEMPLATE_PERMISSION;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author jianxing
@ -48,7 +55,9 @@ public class OrganizationTemplateControllerTests extends BaseTest {
private static final String BASE_PATH = "/organization/template/";
private static final String LIST = "list/{0}/{1}";
private static final String DISABLE_ORG_TEMPLATE = "disable/{0}/{1}";
protected static final String ORGANIZATION_TEMPLATE_ENABLE_CONFIG = "enable/config/{0}";
protected static final String ENABLE_CONFIG = "enable/config/{0}";
protected static final String UPLOAD_TEMP_IMG = "upload/temp/img";
protected static final String IMG_PREVIEW = "/img/preview/{0}/{1}/{2}";
@Resource
private TemplateMapper templateMapper;
@ -89,6 +98,36 @@ public class OrganizationTemplateControllerTests extends BaseTest {
this.defaultTemplate = templates.getFirst();
}
@Test
@Order(0)
public void uploadTempFile() throws Exception {
// 准备数据上传文件管理文件
MockMultipartFile file = new MockMultipartFile("file", IDGenerator.nextStr() + "_file_upload.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
// @@请求成功
String fileId = doUploadTempFile(file);
// 校验文件存在
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
fileRequest.setFileName(file.getOriginalFilename());
Assertions.assertNotNull(FileCenter.getDefaultRepository().getFile(fileRequest));
requestUploadPermissionTest(PermissionConstants.ORGANIZATION_TEMPLATE_UPDATE, UPLOAD_TEMP_IMG, file);
requestUploadPermissionTest(PermissionConstants.ORGANIZATION_TEMPLATE_ADD, UPLOAD_TEMP_IMG, file);
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, fileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, fileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, "no id", false));
}
private String doUploadTempFile(MockMultipartFile file) throws Exception {
return JSON.parseObject(requestUploadFileWithOkAndReturn(UPLOAD_TEMP_IMG, file)
.getResponse()
.getContentAsString(), ResultHolder.class)
.getData().toString();
}
@Test
@Order(1)
public void add() throws Exception {
@ -144,8 +183,15 @@ public class OrganizationTemplateControllerTests extends BaseTest {
request.setScopeId(DEFAULT_ORGANIZATION_ID);
request.setCustomFields(null);
request.setSystemFields(null);
String uploadFileId = doUploadTempFile(getMockMultipartFile("api-add-file_upload.JPG"));
request.setUploadImgFileIds(List.of(uploadFileId));
MvcResult anotherMvcResult = this.requestPostWithOkAndReturn(DEFAULT_ADD, request);
this.anotherTemplateField = templateMapper.selectByPrimaryKey(getResultData(anotherMvcResult, Template.class).getId());
assertUploadFile(uploadFileId, "api-add-file_upload.JPG");
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, uploadFileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, uploadFileId, false)).andExpect(status().isOk());
request.setUploadImgFileIds(null);
// @@校验日志
checkLog(this.addTemplate.getId(), OperationLogType.ADD);
@ -155,6 +201,27 @@ public class OrganizationTemplateControllerTests extends BaseTest {
requestPostPermissionTest(PermissionConstants.ORGANIZATION_TEMPLATE_ADD, DEFAULT_ADD, request);
}
/**
* 校验上传的文件
*
*/
public static void assertUploadFile(String fileId, String fileName) throws Exception {
String orgTemplateImgDir = DefaultRepositoryDir.getOrgTemplateImgDir(DEFAULT_ORGANIZATION_ID);
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(orgTemplateImgDir + "/" + fileId);
fileRequest.setFileName(fileName);
Assertions.assertNotNull(FileCenter.getDefaultRepository().getFile(fileRequest));
}
public static MockMultipartFile getMockMultipartFile(String fileName) {
return new MockMultipartFile(
"file",
fileName,
MediaType.APPLICATION_OCTET_STREAM_VALUE,
"Hello, World!".getBytes()
);
}
public static List<TemplateSystemCustomFieldRequest> getTemplateSystemCustomFieldRequests() {
TemplateSystemCustomFieldRequest nameField = new TemplateSystemCustomFieldRequest();
nameField.setFieldId("name");
@ -287,8 +354,16 @@ public class OrganizationTemplateControllerTests extends BaseTest {
// 不更新字段
request.setCustomFields(null);
String uploadFileId = doUploadTempFile(getMockMultipartFile("api-add-file_upload.JPG"));
request.setUploadImgFileIds(List.of(uploadFileId));
request.setScopeId(DEFAULT_ORGANIZATION_ID);
this.requestPostWithOk(DEFAULT_UPDATE, request);
Assertions.assertEquals(baseTemplateCustomFieldService.getByTemplateId(template.getId()).size(), 3);
assertUploadFile(uploadFileId, "api-add-file_upload.JPG");
// 图片预览
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, uploadFileId, false)).andExpect(status().isOk());
mockMvc.perform(getRequestBuilder(IMG_PREVIEW, DEFAULT_ORGANIZATION_ID, uploadFileId, false)).andExpect(status().isOk());
request.setUploadImgFileIds(null);
// @校验是否开启组织模板
changeOrgTemplateEnable(false);
@ -444,20 +519,20 @@ public class OrganizationTemplateControllerTests extends BaseTest {
@Order(8)
public void getOrganizationTemplateEnableConfig() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(ORGANIZATION_TEMPLATE_ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
MvcResult mvcResult = this.requestGetWithOkAndReturn(ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
Map resultData = getResultData(mvcResult, Map.class);
Assertions.assertEquals(resultData.size(), TemplateScene.values().length);
Assertions.assertTrue((Boolean) resultData.get(TemplateScene.FUNCTIONAL.name()));
changeOrgTemplateEnable(false);
mvcResult = this.requestGetWithOkAndReturn(ORGANIZATION_TEMPLATE_ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
mvcResult = this.requestGetWithOkAndReturn(ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
Assertions.assertFalse((Boolean) getResultData(mvcResult, Map.class).get(TemplateScene.FUNCTIONAL.name()));
changeOrgTemplateEnable(true);
// @@校验 NOT_FOUND 异常
assertErrorCode(this.requestGet(ORGANIZATION_TEMPLATE_ENABLE_CONFIG,"1111"), NOT_FOUND);
assertErrorCode(this.requestGet(ENABLE_CONFIG,"1111"), NOT_FOUND);
// @@校验权限
requestGetPermissionTest(PermissionConstants.ORGANIZATION_TEMPLATE_READ, ORGANIZATION_TEMPLATE_ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
requestGetPermissionTest(PermissionConstants.ORGANIZATION_TEMPLATE_READ, ENABLE_CONFIG, DEFAULT_ORGANIZATION_ID);
}
/**

View File

@ -0,0 +1,97 @@
package io.metersphere.system.service;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: jianxing
* @CreateTime: 2024-07-31 16:25
*/
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CommonFileServiceTest extends BaseTest {
@Resource
private CommonFileService commonFileService;
@Test
public void uploadTempImgFile() {
MockMultipartFile file = new MockMultipartFile("file", "", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
Assertions.assertThrows(MSException.class, () -> commonFileService.uploadTempImgFile(file));
}
@Test
public void saveFileFromTempFile() throws Exception {
// 测试方法正确
MockMultipartFile file = new MockMultipartFile("file", IDGenerator.nextStr() + "_file_upload.JPG", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
String fileId = commonFileService.uploadTempImgFile(file);
String orgTemplateImgDir = DefaultRepositoryDir.getOrgTemplateImgDir(DEFAULT_ORGANIZATION_ID);
Map<String, String> fileMap = new HashMap<>();
fileMap.put(fileId, file.getOriginalFilename());
commonFileService.saveFileFromTempFile(orgTemplateImgDir, fileMap);
// 校验文件是否存在
assertUploadFile(fileId, file.getOriginalFilename());
// 增加 key null 覆盖率
fileMap = new HashMap<>();
fileMap.put(fileId, null);
commonFileService.saveFileFromTempFile(orgTemplateImgDir, fileMap);
commonFileService.saveFileFromTempFile(orgTemplateImgDir, new HashMap<>());
commonFileService.saveReviewImgFromTempFile(orgTemplateImgDir, orgTemplateImgDir, fileMap);
commonFileService.saveReviewImgFromTempFile(orgTemplateImgDir, orgTemplateImgDir, new HashMap<>());
// 校验文件不存在异常
fileMap = new HashMap<>();
fileMap.put("no id", file.getOriginalFilename());
Map<String, String> finalFileMap = fileMap;
Assertions.assertThrows(MSException.class, () -> commonFileService.saveFileFromTempFile(orgTemplateImgDir, finalFileMap));
Map<String, String> finalFileMap2 = fileMap;
Assertions.assertThrows(MSException.class, () -> commonFileService.saveReviewImgFromTempFile(orgTemplateImgDir, orgTemplateImgDir, finalFileMap2));
}
@Test
public void moveTempFileToImgReviewFolder() throws Exception {
// 增加非图片类型文件的覆盖率
commonFileService.moveTempFileToImgReviewFolder("" , "", "test.txt");
}
@Test
public void getFileNameByFileId() {
// 增加文件不存在覆盖率
commonFileService.getFileNameByFileId("111" , "");
}
@Test
public void uploadReviewImg() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "test.txt", MediaType.APPLICATION_OCTET_STREAM_VALUE, "aa".getBytes());
commonFileService.uploadReviewImg(file, "", "");
}
/**
* 校验上传的文件
*
*/
public static void assertUploadFile(String fileId, String fileName) throws Exception {
String orgTemplateImgDir = DefaultRepositoryDir.getOrgTemplateImgDir(DEFAULT_ORGANIZATION_ID);
FileRequest fileRequest = new FileRequest();
fileRequest.setFolder(orgTemplateImgDir + "/" + fileId);
fileRequest.setFileName(fileName);
Assertions.assertNotNull(FileCenter.getDefaultRepository().getFile(fileRequest));
}
}

View File

@ -3,7 +3,6 @@ package io.metersphere.plan.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.bug.dto.response.BugDTO;
import io.metersphere.bug.service.BugAttachmentService;
import io.metersphere.plan.constants.AssociateCaseType;
import io.metersphere.plan.constants.TestPlanResourceConfig;
import io.metersphere.plan.domain.TestPlanReportComponent;
@ -19,6 +18,7 @@ import io.metersphere.system.log.constants.OperationLogType;
import io.metersphere.system.notice.annotation.SendNotice;
import io.metersphere.system.notice.constants.NoticeConstants;
import io.metersphere.system.security.CheckOwner;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.utils.PageUtils;
import io.metersphere.system.utils.Pager;
import io.metersphere.system.utils.SessionUtils;
@ -40,15 +40,14 @@ import java.util.List;
@RequestMapping("/test-plan/report")
@Tag(name = "测试计划-报告")
public class TestPlanReportController {
@Resource
private BugAttachmentService bugAttachmentService;
@Resource
private TestPlanManagementService testPlanManagementService;
@Resource
private TestPlanReportService testPlanReportService;
@Resource
private TestPlanService testPlanService;
@Resource
private CommonFileService commonFileService;
@PostMapping("/page")
@Operation(summary = "测试计划-报告-表格分页查询")
@ -128,7 +127,7 @@ public class TestPlanReportController {
@Operation(summary = "测试计划-报告-详情-上传富文本(图片)")
@RequiresPermissions(PermissionConstants.TEST_PLAN_REPORT_READ_UPDATE)
public String upload(@RequestParam("file") MultipartFile file) {
return bugAttachmentService.uploadMdFile(file);
return commonFileService.uploadTempImgFile(file);
}
@PostMapping("/detail/edit")

View File

@ -25,7 +25,7 @@ public class TestPlanCaseBatchRunRequest extends BasePlanCaseBatchRequest {
@Schema(description = "评论@的人的Id, 多个以';'隔开")
private String notifier;
@Schema(description = "测试计划执行评论文本的文件id集合")
@Schema(description = "测试计划执行评论文本的文件id集合")
private List<String> planCommentFileIds;

View File

@ -41,7 +41,7 @@ public class TestPlanCaseRunRequest {
@Schema(description = "评论@的人的Id, 多个以';'隔开")
private String notifier;
@Schema(description = "测试计划执行评论文本的文件id集合")
@Schema(description = "测试计划执行评论文本的文件id集合")
private List<String> planCommentFileIds;
}

View File

@ -14,7 +14,8 @@ import io.metersphere.plan.mapper.*;
import io.metersphere.plan.utils.CountUtils;
import io.metersphere.plan.utils.RateCalculateUtils;
import io.metersphere.plugin.platform.dto.SelectOption;
import io.metersphere.project.service.FileService;
import io.metersphere.system.service.CommonFileService;
import io.metersphere.system.service.FileService;
import io.metersphere.sdk.constants.*;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileCenter;
@ -103,6 +104,8 @@ public class TestPlanReportService {
private ExtTestPlanCaseExecuteHistoryMapper extTestPlanCaseExecuteHistoryMapper;
@Resource
private TestPlanReportComponentMapper componentMapper;
@Resource
private CommonFileService commonFileService;
/**
* 分页查询报告列表
@ -936,7 +939,7 @@ public class TestPlanReportService {
String systemTempDir = DefaultRepositoryDir.getSystemTempDir();
// 添加文件与测试计划报告的关联关系
Map<String, String> addFileMap = new HashMap<>(fileIds.size());
LogUtils.info("开始上传文本里的附件");
LogUtils.info("开始上传文本里的附件");
List<TestPlanReportAttachment> attachments = fileIds.stream().map(fileId -> {
TestPlanReportAttachment attachment = new TestPlanReportAttachment();
String fileName = getTempFileNameByFileId(fileId);
@ -1014,7 +1017,7 @@ public class TestPlanReportService {
fileCopyRequest.setFileName(fileName);
defaultRepository.delete(fileCopyRequest);
} catch (Exception e) {
LogUtils.error("上传文本文件失败:{}", e);
LogUtils.error("上传文本文件失败:{}", e);
throw new MSException(Translator.get("file_upload_fail"));
}
}
@ -1105,7 +1108,7 @@ public class TestPlanReportService {
if (CollectionUtils.isEmpty(reportAttachments)) {
//在临时文件获取
fileName = getTempFileNameByFileId(fileId);
bytes = getPreviewImg(fileName, fileId, compressed);
bytes = commonFileService.downloadTempImg(fileId, fileName, compressed);
} else {
//在正式目录获取
TestPlanReportAttachment attachment = reportAttachments.getFirst();
@ -1143,51 +1146,4 @@ public class TestPlanReportService {
fileRequest.setStorage(StorageType.MINIO.name());
return fileRequest;
}
public byte[] getPreviewImg(String fileName, String fileId, boolean isCompressed) {
String systemTempDir;
if (isCompressed) {
systemTempDir = DefaultRepositoryDir.getSystemTempCompressDir();
} else {
systemTempDir = DefaultRepositoryDir.getSystemTempDir();
}
FileRequest previewRequest = new FileRequest();
previewRequest.setFileName(fileName);
previewRequest.setStorage(StorageType.MINIO.name());
previewRequest.setFolder(systemTempDir + "/" + fileId);
byte[] previewImg = null;
try {
previewImg = fileService.download(previewRequest);
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
if (previewImg == null || previewImg.length == 0) {
try {
if (isCompressed) {
previewImg = this.compressPicWithFileMetadata(fileName, fileId);
previewRequest.setFolder(DefaultRepositoryDir.getSystemTempCompressDir() + "/" + fileId);
fileService.upload(previewImg, previewRequest);
}
return previewImg;
} catch (Exception e) {
LogUtils.error("获取预览图失败:{}", e);
}
}
return previewImg;
}
//获取文件并压缩的方法需要上锁防止并发超过一定数量时内存溢出
private synchronized byte[] compressPicWithFileMetadata(String fileName, String fileId) throws Exception {
byte[] fileBytes = this.getFile(fileName, fileId);
return TempFileUtils.compressPic(fileBytes);
}
public byte[] getFile(String fileName, String fileId) throws Exception {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId);
fileRequest.setStorage(StorageType.MINIO.name());
return fileService.download(fileRequest);
}
}