diff --git a/backend/src/main/java/io/metersphere/commons/utils/FileUtils.java b/backend/src/main/java/io/metersphere/commons/utils/FileUtils.java index cafcbc1254..adeb0c9878 100644 --- a/backend/src/main/java/io/metersphere/commons/utils/FileUtils.java +++ b/backend/src/main/java/io/metersphere/commons/utils/FileUtils.java @@ -27,8 +27,7 @@ public class FileUtils { public static final String MD_IMAGE_DIR = "/opt/metersphere/data/image/markdown"; public static final String UI_IMAGE_DIR = "/opt/metersphere/data/image/ui/screenshots"; public static final String ATTACHMENT_DIR = "/opt/metersphere/data/attachment"; - public static final String TEST_CASE_ATTACHMENT_DIR = "/opt/metersphere/data/attachment/testcase"; - public static final String ISSUE_ATTACHMENT_DIR = "/opt/metersphere/data/attachment/issue"; + public static final String ATTACHMENT_TMP_DIR = "/opt/metersphere/data/attachment/tmp"; public static byte[] listBytesToZip(Map mapReport) { diff --git a/backend/src/main/java/io/metersphere/service/FileService.java b/backend/src/main/java/io/metersphere/service/FileService.java index 6c6c043f63..20ed439b95 100644 --- a/backend/src/main/java/io/metersphere/service/FileService.java +++ b/backend/src/main/java/io/metersphere/service/FileService.java @@ -14,8 +14,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -137,6 +136,38 @@ public class FileService { return fileAttachmentMetadata; } + public FileAttachmentMetadata saveAttachmentByBytes(byte[] bytes, String attachmentType, String belongId, String attachmentName) { + String uploadPath = FileUtils.ATTACHMENT_DIR + "/" + attachmentType + "/" + belongId; + File parentFile = new File(uploadPath); + if (!parentFile.exists()) { + parentFile.mkdir(); + } + try (OutputStream os = new FileOutputStream(uploadPath + "/" + attachmentName)){ + InputStream in = new ByteArrayInputStream(bytes); + int len = 0; + byte[] buf = new byte[1024]; + while ((len = in.read(buf)) != -1) { + os.write(buf, 0, len); + } + os.flush(); + + final FileAttachmentMetadata fileAttachmentMetadata = new FileAttachmentMetadata(); + fileAttachmentMetadata.setId(UUID.randomUUID().toString()); + fileAttachmentMetadata.setName(attachmentName); + fileAttachmentMetadata.setType(getFileType(attachmentName).name()); + fileAttachmentMetadata.setSize(Integer.valueOf(bytes.length).longValue()); + fileAttachmentMetadata.setCreateTime(System.currentTimeMillis()); + fileAttachmentMetadata.setUpdateTime(System.currentTimeMillis()); + fileAttachmentMetadata.setCreator(SessionUtils.getUser().getName()); + fileAttachmentMetadata.setFilePath(uploadPath); + fileAttachmentMetadataMapper.insert(fileAttachmentMetadata); + return fileAttachmentMetadata; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + public void deleteAttachment(List ids) { for (String id : ids) { FileAttachmentMetadata fileAttachmentMetadata = fileAttachmentMetadataMapper.selectByPrimaryKey(id); diff --git a/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java b/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java index 12b94f3343..84441020e6 100644 --- a/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import io.metersphere.base.domain.*; +import io.metersphere.base.mapper.IssueFileMapper; import io.metersphere.base.mapper.IssuesMapper; import io.metersphere.base.mapper.ProjectMapper; import io.metersphere.base.mapper.TestCaseIssuesMapper; @@ -64,6 +65,9 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform { protected boolean isThirdPartTemplate; protected CustomFieldIssuesService customFieldIssuesService; protected CustomFieldService customFieldService; + protected IssuesService issuesService; + protected FileService fileService; + protected IssueFileMapper issueFileMapper; public String getKey() { return key; @@ -90,6 +94,9 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform { this.testCaseIssueService = CommonBeanFactory.getBean(TestCaseIssueService.class); this.customFieldIssuesService = CommonBeanFactory.getBean(CustomFieldIssuesService.class); this.customFieldService = CommonBeanFactory.getBean(CustomFieldService.class); + this.issuesService = CommonBeanFactory.getBean(IssuesService.class); + this.fileService = CommonBeanFactory.getBean(FileService.class); + this.issueFileMapper = CommonBeanFactory.getBean(IssueFileMapper.class); } protected String getPlatformConfig(String platform) { diff --git a/backend/src/main/java/io/metersphere/track/issue/IssuesPlatform.java b/backend/src/main/java/io/metersphere/track/issue/IssuesPlatform.java index 57b1d84dd7..4aee7446ac 100644 --- a/backend/src/main/java/io/metersphere/track/issue/IssuesPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/IssuesPlatform.java @@ -11,6 +11,7 @@ import io.metersphere.track.request.testcase.EditTestCaseRequest; import io.metersphere.track.request.testcase.IssuesRequest; import io.metersphere.track.request.testcase.IssuesUpdateRequest; import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -31,13 +32,13 @@ public interface IssuesPlatform { * * @param issuesRequest issueRequest */ - IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest); + IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest, List files); /** * 更新缺陷 * @param request */ - void updateIssue(IssuesUpdateRequest request); + void updateIssue(IssuesUpdateRequest request, List files); /** * 删除缺陷平台缺陷 diff --git a/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java b/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java index f07d5ae064..5f95f0fcb9 100644 --- a/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java @@ -2,15 +2,15 @@ package io.metersphere.track.issue; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; -import io.metersphere.base.domain.IssuesDao; -import io.metersphere.base.domain.IssuesWithBLOBs; -import io.metersphere.base.domain.Project; +import io.metersphere.base.domain.*; import io.metersphere.base.domain.ext.CustomFieldResource; +import io.metersphere.commons.constants.AttachmentType; import io.metersphere.commons.constants.CustomFieldType; import io.metersphere.commons.constants.IssuesManagePlatform; import io.metersphere.commons.constants.IssuesStatus; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.CommonBeanFactory; +import io.metersphere.commons.utils.FileUtils; import io.metersphere.commons.utils.LogUtil; import io.metersphere.dto.CustomFieldDao; import io.metersphere.dto.CustomFieldItemDTO; @@ -31,8 +31,10 @@ import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -225,7 +227,7 @@ public class JiraPlatform extends AbstractIssuePlatform { } @Override - public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest) { + public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest, List files) { setUserConfig(); Project project = getProject(); @@ -237,6 +239,20 @@ public class JiraPlatform extends AbstractIssuePlatform { // 上传附件 imageFiles.forEach(img -> jiraClientV2.uploadAttachment(result.getKey(), img)); + if (files != null) { + files.forEach(multipartFile -> { + try { + File file = new File(FileUtils.ATTACHMENT_TMP_DIR + "/" + multipartFile.getOriginalFilename()); + org.apache.commons.io.FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file); + jiraClientV2.uploadAttachment(result.getKey(), file); + if (file.exists()) { + file.delete(); + } + } catch (IOException e) { + LogUtil.error(e); + } + }); + } String status = getStatus(issues.getFields()); issuesRequest.setPlatformStatus(status); @@ -463,7 +479,7 @@ public class JiraPlatform extends AbstractIssuePlatform { } @Override - public void updateIssue(IssuesUpdateRequest request) { + public void updateIssue(IssuesUpdateRequest request, List files) { setUserConfig(); Project project = getProject(); List imageFiles = getImageFiles(request); @@ -471,18 +487,27 @@ public class JiraPlatform extends AbstractIssuePlatform { JSONObject param = buildUpdateParam(request, getIssueType(project.getIssueConfig()), project.getJiraKey()); jiraClientV2.updateIssue(request.getPlatformId(), JSONObject.toJSONString(param)); + List updatedFiles = request.getUpdatedFileList(); + List updatedFileIds = updatedFiles.stream().map(FileMetadata::getId).collect(Collectors.toList()); + List originFiles = fileService.getFileAttachmentMetadataByIssueId(request.getId()); + List deleteAttachmentNames = new ArrayList(); + originFiles.forEach(originFile -> { + if (!updatedFileIds.contains(originFile.getId())) { + deleteAttachmentNames.add(originFile.getName()); + } + }); Set attachmentNames = new HashSet<>(); // 更新附件 JiraIssue jiraIssue = jiraClientV2.getIssues(request.getPlatformId()); JSONObject fields = jiraIssue.getFields(); JSONArray attachments = fields.getJSONArray("attachment"); if (!attachments.isEmpty() && attachments.size() > 0) { - // 删除旧附件,若缺陷描述中存在则不删除 + // 删除旧附件,若缺陷描述中不存在且附件上传的删除列表中存在则删除 for (int i = 0; i < attachments.size(); i++) { JSONObject attachment = attachments.getJSONObject(i); String filename = attachment.getString("filename"); attachmentNames.add(filename); - if (!request.getDescription().contains(filename)) { + if (!request.getDescription().contains(filename) && deleteAttachmentNames.contains(filename)) { String fileId = attachment.getString("id"); jiraClientV2.deleteAttachment(fileId); } @@ -496,6 +521,20 @@ public class JiraPlatform extends AbstractIssuePlatform { jiraClientV2.uploadAttachment(request.getPlatformId(), img); } }); + if (files != null) { + files.forEach(multipartFile -> { + try { + File file = new File(FileUtils.ATTACHMENT_TMP_DIR + "/" + multipartFile.getOriginalFilename()); + org.apache.commons.io.FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file); + jiraClientV2.uploadAttachment(request.getPlatformId(), file); + if (file.exists()) { + file.delete(); + } + } catch (IOException e) { + LogUtil.error(e); + } + }); + } if (request.getTransitions() != null) { try { @@ -554,6 +593,8 @@ public class JiraPlatform extends AbstractIssuePlatform { List customFieldResource = customFieldService.getCustomFieldResource(customFields); customFieldMap.put(item.getId(), customFieldResource); issuesMapper.updateByPrimaryKeySelective(item); + // 同步第三方平台系统附件字段 + syncJiraIssueAttachments(item, jiraClientV2.getIssues(item.getPlatformId())); } catch (HttpClientErrorException e) { if (e.getRawStatusCode() == 404) { // 标记成删除 @@ -854,4 +895,23 @@ public class JiraPlatform extends AbstractIssuePlatform { public ResponseEntity proxyForGet(String url, Class responseEntityClazz) { return jiraClientV2.proxyForGet(url, responseEntityClazz); } + + public void syncJiraIssueAttachments(IssuesWithBLOBs issue, JiraIssue jiraIssue) { + issuesService.deleteIssueAttachments(issue.getId()); + JSONArray attachments = jiraIssue.getFields().getJSONArray("attachment"); + if (CollectionUtils.isEmpty(attachments)) { + return; + } + for (int i = 0; i < attachments.size(); i++) { + JSONObject attachment = attachments.getJSONObject(i); + String id = attachment.getString("id"); + byte[] content = jiraClientV2.getAttachmentContent(id); + String filename = attachment.getString("filename"); + FileAttachmentMetadata fileAttachmentMetadata = fileService.saveAttachmentByBytes(content, AttachmentType.ISSUE.type(), issue.getId(), filename); + IssueFile issueFile = new IssueFile(); + issueFile.setIssueId(issue.getId()); + issueFile.setFileId(fileAttachmentMetadata.getId()); + issueFileMapper.insert(issueFile); + } + } } diff --git a/backend/src/main/java/io/metersphere/track/issue/LocalPlatform.java b/backend/src/main/java/io/metersphere/track/issue/LocalPlatform.java index fe16dd1b1e..c517ffdcad 100644 --- a/backend/src/main/java/io/metersphere/track/issue/LocalPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/LocalPlatform.java @@ -13,6 +13,7 @@ import io.metersphere.track.request.testcase.IssuesRequest; import io.metersphere.track.request.testcase.IssuesUpdateRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.UUID; @@ -41,7 +42,7 @@ public class LocalPlatform extends LocalAbstractPlatform { } @Override - public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest) { + public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest, List files) { String issueStatus = "new"; if (StringUtils.isNotBlank(issuesRequest.getCustomFields())) { List customFields = issuesRequest.getRequestFields(); @@ -75,7 +76,7 @@ public class LocalPlatform extends LocalAbstractPlatform { } @Override - public void updateIssue(IssuesUpdateRequest request) { + public void updateIssue(IssuesUpdateRequest request, List files) { handleIssueUpdate(request); } } diff --git a/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java b/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java index 2ca0daf3cc..978cdf0187 100644 --- a/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java @@ -28,6 +28,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; import java.util.*; import java.util.stream.Collectors; @@ -88,7 +89,7 @@ public class TapdPlatform extends AbstractIssuePlatform { } @Override - public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest) { + public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest, List files) { MultiValueMap param = buildUpdateParam(issuesRequest); TapdBug bug = tapdClient.addIssue(param); @@ -109,7 +110,7 @@ public class TapdPlatform extends AbstractIssuePlatform { } @Override - public void updateIssue(IssuesUpdateRequest request) { + public void updateIssue(IssuesUpdateRequest request, List files) { MultiValueMap param = buildUpdateParam(request); param.add("id", request.getPlatformId()); handleIssueUpdate(request); diff --git a/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java b/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java index b950a3b708..29f6228c43 100644 --- a/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java @@ -37,6 +37,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; import java.net.URLDecoder; import java.net.URLEncoder; @@ -197,7 +198,7 @@ public class ZentaoPlatform extends AbstractIssuePlatform { } @Override - public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest) { + public IssuesWithBLOBs addIssue(IssuesUpdateRequest issuesRequest, List files) { setUserConfig(); MultiValueMap param = buildUpdateParam(issuesRequest); @@ -228,7 +229,7 @@ public class ZentaoPlatform extends AbstractIssuePlatform { } @Override - public void updateIssue(IssuesUpdateRequest request) { + public void updateIssue(IssuesUpdateRequest request, List files) { setUserConfig(); MultiValueMap param = buildUpdateParam(request); if (request.getTransitions() != null) { diff --git a/backend/src/main/java/io/metersphere/track/issue/client/JiraAbstractClient.java b/backend/src/main/java/io/metersphere/track/issue/client/JiraAbstractClient.java index 33c9da18be..2145a96618 100644 --- a/backend/src/main/java/io/metersphere/track/issue/client/JiraAbstractClient.java +++ b/backend/src/main/java/io/metersphere/track/issue/client/JiraAbstractClient.java @@ -257,6 +257,14 @@ public abstract class JiraAbstractClient extends BaseClient { return (JiraIssueListResponse)getResultForObject(JiraIssueListResponse.class, responseEntity); } + public byte[] getAttachmentContent(String contentId) { + ResponseEntity responseEntity; + String url = getBaseUrl() + "/attachment/content/{1}"; + responseEntity = restTemplate.exchange(url, + HttpMethod.GET, getAuthHttpEntity(), byte[].class, contentId); + return responseEntity.getBody(); + } + public JiraIssueListResponse getProjectIssuesAttachment(int startAt, int maxResults, String projectKey, String issueType) { return getProjectIssues(startAt, maxResults, projectKey, issueType, "attachment"); diff --git a/backend/src/main/java/io/metersphere/track/service/IssuesService.java b/backend/src/main/java/io/metersphere/track/service/IssuesService.java index cd09f3bd03..77c7f66db7 100644 --- a/backend/src/main/java/io/metersphere/track/service/IssuesService.java +++ b/backend/src/main/java/io/metersphere/track/service/IssuesService.java @@ -112,7 +112,7 @@ public class IssuesService { List platformList = getAddPlatforms(issuesRequest); IssuesWithBLOBs issues = null; for (AbstractIssuePlatform platform : platformList) { - issues = platform.addIssue(issuesRequest); + issues = platform.addIssue(issuesRequest, null); } if (issuesRequest.getIsPlanEdit()) { issuesRequest.getAddResourceIds().forEach(l -> { @@ -131,7 +131,7 @@ public class IssuesService { issuesRequest.getId(); List platformList = getUpdatePlatforms(issuesRequest); platformList.forEach(platform -> { - platform.updateIssue(issuesRequest); + platform.updateIssue(issuesRequest, null); }); customFieldIssuesService.editFields(issuesRequest.getId(), issuesRequest.getEditFields()); customFieldIssuesService.addFields(issuesRequest.getId(), issuesRequest.getAddFields()); @@ -142,7 +142,7 @@ public class IssuesService { List platformList = getAddPlatforms(issuesRequest); IssuesWithBLOBs issues = null; for (AbstractIssuePlatform platform : platformList) { - issues = platform.addIssue(issuesRequest); + issues = platform.addIssue(issuesRequest, files); } if (issuesRequest.getIsPlanEdit()) { issuesRequest.getAddResourceIds().forEach(l -> { @@ -187,7 +187,7 @@ public class IssuesService { issuesRequest.getId(); List platformList = getUpdatePlatforms(issuesRequest); platformList.forEach(platform -> { - platform.updateIssue(issuesRequest); + platform.updateIssue(issuesRequest, files); }); customFieldIssuesService.editFields(issuesRequest.getId(), issuesRequest.getEditFields()); customFieldIssuesService.addFields(issuesRequest.getId(), issuesRequest.getAddFields()); @@ -929,4 +929,17 @@ public class IssuesService { return platformStatusDTOS; } + + public void deleteIssueAttachments(String issueId) { + fileService.deleteAttachment(AttachmentType.ISSUE.type(), issueId); + IssueFileExample example = new IssueFileExample(); + example.createCriteria().andIssueIdEqualTo(issueId); + List issueFiles = issueFileMapper.selectByExample(example); + if (issueFiles.size() == 0) { + return; + } + List ids = issueFiles.stream().map(IssueFile::getFileId).collect(Collectors.toList()); + fileService.deleteFileAttachmentByIds(ids); + issueFileMapper.deleteByExample(example); + } }