package ru.soune.nocopy.service.file; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.moderation.ModerationPassportFile; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.file.moderation.ModerationFileService; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @Service @Slf4j @RequiredArgsConstructor public class ZipService { private final ModerationFileService moderationFileService; private final CloudStorageService cloudStorageService; private static final int MAX_SIZE_MB = 20; private static final int FIRST_PART = 1; private static final int SECOND_PART = 2; @Value("${yandex.cloud.bucket-cold}") private String bucketCold; public void createAndSplitZip(Long userId, List files) throws IOException { byte[] zipBytes = createZip(files); String base64String = Base64.getEncoder().encodeToString(zipBytes); String[] parts = splitBase64(base64String); saveSplitFiles(userId, parts[0], parts[1]); } public byte[] assembleZipFromSplitFiles(Long userId) throws IOException { List activeFiles = moderationFileService.getActiveFilesByUserId(userId); String part1Key = null; String part2Key = null; for (ModerationPassportFile file : activeFiles) { if (file.getPart() == FIRST_PART) { part1Key = file.getPath(); } else if (file.getPart() == SECOND_PART) { part2Key = file.getPath(); } } if (part1Key == null || part2Key == null) { throw new IOException("Missing part 1 or part 2 for user: " + userId); } String part1Content = cloudStorageService.downloadPartFromBucket(bucketCold, part1Key); String part2Content = cloudStorageService.downloadPartFromBucket(bucketCold, part2Key); String fullBase64 = part1Content + part2Content; return Base64.getDecoder().decode(fullBase64); } private byte[] createZip(List files) throws IOException { long totalSize = files.stream() .map(FileEntity::getFileSize) .reduce(0L, Long::sum); if (totalSize > MAX_SIZE_MB * 1024 * 1024) { throw new IllegalArgumentException( String.format("Total size %.2f MB exceeds %d MB limit", totalSize / (1024.0 * 1024.0), MAX_SIZE_MB) ); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { for (FileEntity file: files) { String filePath = file.getFilePath(); String fileName = file.getStoredFileName(); Path path = Paths.get(filePath); ZipEntry zipEntry = new ZipEntry(fileName); zos.putNextEntry(zipEntry); Files.copy(path, zos); zos.closeEntry(); log.info("Added file {} to ZIP", fileName); } } return baos.toByteArray(); } private String[] splitBase64(String base64) { StringBuilder part1 = new StringBuilder(); StringBuilder part2 = new StringBuilder(); for (int i = 0; i < base64.length(); i++) { if (i % 2 == 0) { part1.append(base64.charAt(i)); } else { part2.append(base64.charAt(i)); } } return new String[]{part1.toString(), part2.toString()}; } private void saveSplitFiles(Long userId, String part1, String part2) throws IOException { Path userDir = Paths.get("/uploads/passport", String.valueOf(userId)); Files.createDirectories(userDir); Path file1 = userDir.resolve(userId + "___1.txt"); Files.writeString(file1, part1); Path file2 = userDir.resolve(userId + "___2.txt"); Files.writeString(file2, part2); Map cloudKeys = cloudStorageService.saveSplitPartsToColdStorage(userId, file1, file2); Map files = Map.of( FIRST_PART, cloudKeys.get("part1"), SECOND_PART, cloudKeys.get("part2")); moderationFileService.addFiles(userId, files); log.info("Saved split files to cold storage for userId: {}", userId); } private String mergeBase64(String part1, String part2) { StringBuilder merged = new StringBuilder(); int maxLength = Math.max(part1.length(), part2.length()); for (int i = 0; i < maxLength; i++) { if (i < part1.length()) { merged.append(part1.charAt(i)); } if (i < part2.length()) { merged.append(part2.charAt(i)); } } return merged.toString(); } }