diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java index 9d53c89..23dfebe 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java @@ -27,12 +27,13 @@ import com.datamate.datamanagement.interfaces.dto.ImportKnowledgeItemsRequest; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse; import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,8 +47,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -63,8 +66,9 @@ public class KnowledgeItemApplicationService { private static final Set SUPPORTED_TEXT_EXTENSIONS = Set.of("txt", "md", "markdown"); private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private static final String EXPORT_FILE_PREFIX = "knowledge_set_"; - private static final String EXPORT_FILE_SUFFIX = ".json"; - private static final String EXPORT_CONTENT_TYPE = "application/json"; + private static final String EXPORT_FILE_SUFFIX = ".zip"; + private static final String EXPORT_CONTENT_TYPE = "application/zip"; + private static final int MAX_FILE_BASE_LENGTH = 120; private final KnowledgeItemRepository knowledgeItemRepository; private final KnowledgeSetRepository knowledgeSetRepository; @@ -228,16 +232,25 @@ public class KnowledgeItemApplicationService { BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR); KnowledgeSet knowledgeSet = requireKnowledgeSet(setId); List items = knowledgeItemRepository.findAllBySetId(setId); - List responses = KnowledgeConverter.INSTANCE.convertItemResponses(items); response.setContentType(EXPORT_CONTENT_TYPE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + buildExportFileName(knowledgeSet.getId()) + "\""); - ObjectMapper objectMapper = new ObjectMapper(); - try { - objectMapper.writeValue(response.getOutputStream(), responses); + try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(response.getOutputStream())) { + zos.setEncoding(StandardCharsets.UTF_8.name()); + Map nameCounter = new HashMap<>(); + for (KnowledgeItem item : items) { + String entryName = buildItemEntryName(item, nameCounter); + ZipArchiveEntry entry = new ZipArchiveEntry(entryName); + zos.putArchiveEntry(entry); + byte[] contentBytes = (item.getContent() == null ? "" : item.getContent()) + .getBytes(StandardCharsets.UTF_8); + zos.write(contentBytes); + zos.closeArchiveEntry(); + } + zos.finish(); } catch (IOException e) { log.error("export knowledge items error, setId: {}", setId, e); throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); @@ -254,6 +267,50 @@ public class KnowledgeItemApplicationService { return EXPORT_FILE_PREFIX + setId + "_" + LocalDateTime.now().format(EXPORT_TIME_FORMATTER) + EXPORT_FILE_SUFFIX; } + private String buildItemEntryName(KnowledgeItem item, Map nameCounter) { + String rawTitle = StringUtils.isNotBlank(item.getTitle()) ? item.getTitle() : "item-" + item.getId(); + String baseName = sanitizeFileName(rawTitle); + if (StringUtils.isBlank(baseName)) { + baseName = "item-" + item.getId(); + } + baseName = trimToLength(baseName, MAX_FILE_BASE_LENGTH); + + String extension = item.getContentType() == KnowledgeContentType.MARKDOWN ? "md" : "txt"; + String baseKey = baseName + "." + extension; + int count = nameCounter.getOrDefault(baseKey, 0); + nameCounter.put(baseKey, count + 1); + if (count == 0) { + return baseKey; + } + return buildIndexedFileName(baseName, extension, count); + } + + private String buildIndexedFileName(String baseName, String extension, int index) { + String suffix = "_" + index; + int maxBaseLength = Math.max(1, MAX_FILE_BASE_LENGTH - suffix.length()); + String safeBase = trimToLength(baseName, maxBaseLength); + return safeBase + suffix + "." + extension; + } + + private String sanitizeFileName(String name) { + if (name == null) { + return ""; + } + String normalized = name.replaceAll("[\\\\/:*?\"<>|]", "_"); + normalized = normalized.replaceAll("\\s+", " ").trim(); + return normalized; + } + + private String trimToLength(String value, int maxLength) { + if (value == null) { + return ""; + } + if (value.length() <= maxLength) { + return value; + } + return value.substring(0, maxLength); + } + private KnowledgeContentType resolveContentType(DatasetFile datasetFile) { String extension = getFileExtension(datasetFile); if (StringUtils.isBlank(extension)) {