diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java index 7042a4b80c..6e4cee5caa 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/DefaultRepositoryDir.java @@ -114,6 +114,10 @@ public class DefaultRepositoryDir { return SYSTEM_TEMP_DIR; } + public static String getSystemTempCompressDir() { + return SYSTEM_TEMP_DIR + "/compress"; + } + public static String getApiScenarioDir(String projectId, String apiScenarioId) { return String.format(PROJECT_API_SCENARIO_DIR, projectId, apiScenarioId); } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java index 97659ce1bf..c350e6e74c 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/FunctionalCaseAttachmentController.java @@ -185,4 +185,10 @@ public class FunctionalCaseAttachmentController { return functionalCaseAttachmentService.uploadTemp(file); } + @GetMapping(value = "/download/file/{projectId}/{fileId}/{compressed}") + @Operation(summary = "用例管理-功能用例-预览上传的副文本里所需的文件资源原图") + public ResponseEntity downloadImgById(@PathVariable String projectId, @PathVariable String fileId, @PathVariable boolean compressed) throws Exception { + return functionalCaseAttachmentService.downloadImgById(projectId, fileId, compressed); + } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/ReviewFunctionalCaseController.java b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/ReviewFunctionalCaseController.java index ea1b68b220..aac385c76a 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/controller/ReviewFunctionalCaseController.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/controller/ReviewFunctionalCaseController.java @@ -78,4 +78,10 @@ public class ReviewFunctionalCaseController { return functionalCaseAttachmentService.downloadPreviewImgById(request); } + @GetMapping(value = "/download/file/{projectId}/{fileId}/{compressed}") + @Operation(summary = "用例管理-功能用例-预览上传的副文本里所需的文件资源原图") + public ResponseEntity downloadImgById(@PathVariable String projectId, @PathVariable String fileId, @PathVariable boolean compressed) throws Exception { + return functionalCaseAttachmentService.downloadImgById(projectId, fileId, compressed); + } + } diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java index b938269763..14a3a1aea7 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseAttachmentService.java @@ -383,6 +383,15 @@ public class FunctionalCaseAttachmentService { 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")); @@ -390,7 +399,7 @@ public class FunctionalCaseAttachmentService { return fileId; } - public void uploadMinioFile(String caseId, String projectId, List uploadFileIds, String userId, String fileSource){ + public void uploadMinioFile(String caseId, String projectId, List uploadFileIds, String userId, String fileSource) { String functionalCaseDir = DefaultRepositoryDir.getFunctionalCaseDir(projectId, caseId); // 处理本地上传文件 FileRepository defaultRepository = FileCenter.getDefaultRepository(); @@ -433,10 +442,9 @@ public class FunctionalCaseAttachmentService { */ public String getTempFileNameByFileId(String fileId) { FileRepository defaultRepository = FileCenter.getDefaultRepository(); - String systemTempDir = DefaultRepositoryDir.getSystemTempDir(); try { FileRequest fileRequest = new FileRequest(); - fileRequest.setFolder(systemTempDir + "/" + fileId); + fileRequest.setFolder(DefaultRepositoryDir.getSystemTempDir() + "/" + fileId); List folderFileNames = defaultRepository.getFolderFileNames(fileRequest); if (CollectionUtils.isEmpty(folderFileNames)) { return null; @@ -453,7 +461,7 @@ public class FunctionalCaseAttachmentService { /** * 上传用例管理相关的资源文件 * - * @param folder 用例管理文件路径 + * @param folder 用例管理文件路径 * @param addFileMap key:fileId value:fileName */ public void uploadFileResource(String folder, Map addFileMap, String projectId, String caseId) { @@ -482,7 +490,7 @@ public class FunctionalCaseAttachmentService { //图片文件自动生成预览图 byte[] file = defaultRepository.getFile(fileCopyRequest); byte[] previewImg = TempFileUtils.compressPic(file); - fileCopyRequest.setFolder(DefaultRepositoryDir.getFunctionalCasePreviewDir(projectId,caseId)+ "/" + fileId); + fileCopyRequest.setFolder(DefaultRepositoryDir.getFunctionalCasePreviewDir(projectId, caseId) + "/" + fileId); fileCopyRequest.setStorage(StorageType.MINIO.toString()); fileService.upload(previewImg, fileCopyRequest); } @@ -497,4 +505,90 @@ public class FunctionalCaseAttachmentService { } } + /** + * 预览图片 + * + * @param fileId 文件ID + */ + public ResponseEntity downloadImgById(String projectId, String fileId, boolean compressed) { + byte[] bytes; + String fileName; + FunctionalCaseAttachmentExample example = new FunctionalCaseAttachmentExample(); + example.createCriteria().andFileIdEqualTo(fileId); + List caseAttachments = functionalCaseAttachmentMapper.selectByExample(example); + if (CollectionUtils.isEmpty(caseAttachments)) { + //在临时文件获取 + fileName = getTempFileNameByFileId(fileId); + bytes = getPreviewImg(fileName, fileId, compressed); + } else { + //在正式目录获取 + FunctionalCaseAttachment attachment = caseAttachments.get(0); + fileName = attachment.getFileName(); + FileRequest fileRequest = new FileRequest(); + fileRequest.setFileName(attachment.getFileName()); + if (compressed) { + fileRequest.setFolder(DefaultRepositoryDir.getFunctionalCasePreviewDir(projectId, attachment.getCaseId()) + "/" + attachment.getFileId()); + } else { + fileRequest.setFolder(DefaultRepositoryDir.getFunctionalCaseDir(projectId, attachment.getCaseId()) + "/" + attachment.getFileId()); + } + fileRequest.setStorage(StorageType.MINIO.name()); + 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); + } + + 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); + } + } \ No newline at end of file diff --git a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java index 8dedbb1c34..015e2e587c 100644 --- a/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java +++ b/backend/services/case-management/src/main/java/io/metersphere/functional/service/FunctionalCaseModuleService.java @@ -265,7 +265,21 @@ public class FunctionalCaseModuleService extends ModuleTreeService { return new ArrayList<>(); } List moduleIds = functionalCases.stream().map(FunctionalCase::getModuleId).distinct().toList(); - List baseTreeNodes = extFunctionalCaseModuleMapper.selectBaseByIds(moduleIds); - return super.buildTreeAndCountResource(baseTreeNodes, false, Translator.get("default.module")); + List nodeByNodeIds = getNodeByNodeIds(moduleIds); + return super.buildTreeAndCountResource(nodeByNodeIds, true, Translator.get("default.module")); } + + public List getNodeByNodeIds(ListmoduleIds){ + List finalModuleIds = new ArrayList<>(moduleIds); + List totalList = new ArrayList<>(); + while (CollectionUtils.isNotEmpty(finalModuleIds)) { + List modules = extFunctionalCaseModuleMapper.selectBaseByIds(finalModuleIds); + totalList.addAll(modules); + List parentModuleIds = modules.stream().map(BaseTreeNode::getParentId).filter(parentId -> !StringUtils.equalsIgnoreCase(parentId,ModuleConstants.ROOT_NODE_PARENT_ID)).toList(); + finalModuleIds.clear(); + finalModuleIds = parentModuleIds; + } + return totalList.stream().distinct().toList(); + } + } \ No newline at end of file diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java index 262d0946af..be0e7e0fef 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseAttachmentControllerTests.java @@ -14,10 +14,14 @@ import io.metersphere.project.service.FileService; 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.FileRequest; import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.Translator; import io.metersphere.system.base.BaseTest; import io.metersphere.system.controller.handler.ResultHolder; +import io.metersphere.system.uid.IDGenerator; import jakarta.annotation.Resource; import org.apache.commons.collections.CollectionUtils; import org.junit.jupiter.api.*; @@ -29,6 +33,7 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.multipart.MultipartFile; import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils; import java.nio.charset.StandardCharsets; @@ -66,7 +71,7 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest { public static final String DELETE_FILE_URL = "/attachment/delete/file"; public static final String OPTIONS_URL = "/attachment/options/"; public static final String UPLOAD_TEMP = "/attachment/upload/temp/file"; - + public static final String UPLOAD_DOWNLOAD = "/attachment/download/file/%s/%s/%S"; @Test @Order(1) @@ -244,7 +249,7 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest { Assertions.assertTrue(StringUtils.isNotBlank(fileId)); file = getNoNameMockMultipartFile(); doUploadTempFileFalse(file); - functionalCaseAttachmentService.uploadMinioFile("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1","WX_TEST_PROJECT_ID", List.of(fileId),"admin", CaseFileSourceType.CASE_DETAIL.toString()); + functionalCaseAttachmentService.uploadMinioFile("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1", "WX_TEST_PROJECT_ID", List.of(fileId), "admin", CaseFileSourceType.CASE_DETAIL.toString()); FunctionalCaseSourceFileRequest request = new FunctionalCaseSourceFileRequest(); request.setProjectId("WX_TEST_PROJECT_ID"); request.setLocal(true); @@ -256,15 +261,61 @@ public class FunctionalCaseAttachmentControllerTests extends BaseTest { functionalCaseAttachmentExample.createCriteria().andCaseIdEqualTo("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1").andFileIdEqualTo(fileId).andFileSourceEqualTo(CaseFileSourceType.CASE_DETAIL.toString()); List functionalCaseAttachments = functionalCaseAttachmentMapper.selectByExample(functionalCaseAttachmentExample); Assertions.assertTrue(CollectionUtils.isNotEmpty(functionalCaseAttachments)); - functionalCaseAttachmentService.uploadMinioFile("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1","WX_TEST_PROJECT_ID",new ArrayList<>(),"admin", CaseFileSourceType.CASE_COMMENT.toString()); + functionalCaseAttachmentService.uploadMinioFile("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1", "WX_TEST_PROJECT_ID", new ArrayList<>(), "admin", CaseFileSourceType.CASE_COMMENT.toString()); String functionalCaseDir = DefaultRepositoryDir.getFunctionalCaseDir("WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); - functionalCaseAttachmentService.uploadFileResource(functionalCaseDir,new HashMap<>(),"WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); + functionalCaseAttachmentService.uploadFileResource(functionalCaseDir, new HashMap<>(), "WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); Map objectObjectHashMap = new HashMap<>(); - objectObjectHashMap.put(fileId,null); - functionalCaseAttachmentService.uploadFileResource(functionalCaseDir,objectObjectHashMap,"WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); + objectObjectHashMap.put(fileId, null); + functionalCaseAttachmentService.uploadFileResource(functionalCaseDir, objectObjectHashMap, "WX_TEST_PROJECT_ID", "TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1"); } + @Test + @Order(11) + public void downTemp() throws Exception { + MockMultipartFile file = getMockMultipartFile(); + String fileId = doUploadTempFile(file); + Assertions.assertTrue(StringUtils.isNotBlank(fileId)); + MvcResult compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", fileId, false)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", fileId, true)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + functionalCaseAttachmentService.uploadMinioFile("TEST_FUNCTIONAL_CASE_ATTACHMENT_ID_1", "WX_TEST_PROJECT_ID", List.of(fileId), "admin", CaseFileSourceType.CASE_DETAIL.toString()); + compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", fileId, false)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", fileId, true)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + MockMultipartFile newFile = getMockMultipartFile(); + String newFileId = uploadTemp(newFile); + compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", newFileId, true)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + compressedResult = this.downloadTempFile(String.format(UPLOAD_DOWNLOAD, "WX_TEST_PROJECT_ID", "1123586", true)); + Assertions.assertFalse(compressedResult.getResponse().getContentAsByteArray().length > 0); + } + + public String uploadTemp(MultipartFile file) { + String fileName = StringUtils.trim(file.getOriginalFilename()); + String fileId = IDGenerator.nextStr(); + FileRequest fileRequest = new FileRequest(); + fileRequest.setFileName(fileName); + String systemTempDir = DefaultRepositoryDir.getSystemTempDir(); + fileRequest.setFolder(systemTempDir + "/" + fileId); + try { + FileCenter.getDefaultRepository() + .saveFile(file, fileRequest); + } catch (Exception e) { + LogUtils.error(e); + throw new MSException(Translator.get("file_upload_fail")); + } + return fileId; + } + + protected MvcResult downloadTempFile(String url, Object... uriVariables) throws Exception { + return mockMvc.perform(getRequestBuilder(url, uriVariables)) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) + .andExpect(status().isOk()).andReturn(); + } + private static MockMultipartFile getMockMultipartFile() { MockMultipartFile file = new MockMultipartFile( "file", diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseModuleControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseModuleControllerTests.java index 74e593b773..597e9c0812 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseModuleControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/FunctionalCaseModuleControllerTests.java @@ -721,7 +721,7 @@ public class FunctionalCaseModuleControllerTests extends BaseTest { String contentAsString = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); ResultHolder resultHolder = JSON.parseObject(contentAsString, ResultHolder.class); List baseTreeNodes = JSON.parseArray(JSON.toJSONString(resultHolder.getData()), BaseTreeNode.class); - Assertions.assertTrue(CollectionUtils.isEmpty(baseTreeNodes)); + Assertions.assertEquals(1, baseTreeNodes.size()); //回收站有数据 FunctionalCaseModuleCreateRequest request = new FunctionalCaseModuleCreateRequest(); request.setProjectId(project.getId()); diff --git a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/ReviewFunctionalCaseControllerTests.java b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/ReviewFunctionalCaseControllerTests.java index 706cca52ad..de893147f7 100644 --- a/backend/services/case-management/src/test/java/io/metersphere/functional/controller/ReviewFunctionalCaseControllerTests.java +++ b/backend/services/case-management/src/test/java/io/metersphere/functional/controller/ReviewFunctionalCaseControllerTests.java @@ -58,6 +58,7 @@ public class ReviewFunctionalCaseControllerTests extends BaseTest { public static final String ATTACHMENT_DOWNLOAD_URL = "/review/functional/case/download"; public static final String UPLOAD_TEMP = "/review/functional/case/upload/temp/file"; public static final String ATTACHMENT_PREVIEW_URL = "/review/functional/case/preview"; + public static final String DOWNLOAD_FILE = "/review/functional/case/download/file/%s/%s/%s"; @Resource @@ -341,6 +342,29 @@ public class ReviewFunctionalCaseControllerTests extends BaseTest { this.downloadFile(ATTACHMENT_DOWNLOAD_URL, request); } + @Test + @Order(8) + public void downTemp() throws Exception { + MockMultipartFile file = getMockMultipartFile(); + String fileId = doUploadTempFile(file); + Assertions.assertTrue(org.testcontainers.shaded.org.apache.commons.lang3.StringUtils.isNotBlank(fileId)); + MvcResult compressedResult = this.downloadTempFile(String.format(DOWNLOAD_FILE, "project-review-case-test", fileId, false)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + compressedResult = this.downloadTempFile(String.format(DOWNLOAD_FILE, "project-review-case-test", fileId, true)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + functionalCaseAttachmentService.uploadMinioFile("gyqReviewCaseTest", "project-review-case-test", List.of(fileId), "admin", CaseFileSourceType.CASE_DETAIL.toString()); + compressedResult = this.downloadTempFile(String.format(DOWNLOAD_FILE, "project-review-case-test", fileId, false)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + compressedResult = this.downloadTempFile(String.format(DOWNLOAD_FILE, "project-review-case-test", fileId, true)); + Assertions.assertTrue(compressedResult.getResponse().getContentAsByteArray().length > 0); + } + + protected MvcResult downloadTempFile(String url, Object... uriVariables) throws Exception { + return mockMvc.perform(getRequestBuilder(url, uriVariables)) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) + .andExpect(status().isOk()).andReturn(); + } + public List getCaseReviewHistoryList(String caseId,String reviewId) throws Exception { MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(REVIEW_LIST +"/"+reviewId +"/"+ caseId).header(SessionConstants.HEADER_TOKEN, sessionId)