2026-04-04 15:31:51 +07:00
|
|
|
package ru.soune.nocopy.service.file;
|
|
|
|
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
2026-04-23 00:38:40 +07:00
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
2026-04-04 15:31:51 +07:00
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import ru.soune.nocopy.entity.file.FileEntity;
|
|
|
|
|
import ru.soune.nocopy.entity.file.moderation.ModerationPassportFile;
|
2026-04-23 00:38:40 +07:00
|
|
|
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
2026-04-04 15:31:51 +07:00
|
|
|
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;
|
|
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
private final CloudStorageService cloudStorageService;
|
|
|
|
|
|
2026-04-04 15:31:51 +07:00
|
|
|
private static final int MAX_SIZE_MB = 20;
|
|
|
|
|
|
|
|
|
|
private static final int FIRST_PART = 1;
|
|
|
|
|
|
|
|
|
|
private static final int SECOND_PART = 2;
|
|
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
@Value("${yandex.cloud.bucket-cold}")
|
|
|
|
|
private String bucketCold;
|
|
|
|
|
|
2026-04-04 15:31:51 +07:00
|
|
|
public void createAndSplitZip(Long userId, List<FileEntity> 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 {
|
2026-04-23 00:38:40 +07:00
|
|
|
List<ModerationPassportFile> activeFiles = moderationFileService.getActiveFilesByUserId(userId);
|
2026-04-04 15:31:51 +07:00
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
String part1Key = null;
|
|
|
|
|
String part2Key = null;
|
2026-04-04 15:31:51 +07:00
|
|
|
|
|
|
|
|
for (ModerationPassportFile file : activeFiles) {
|
|
|
|
|
if (file.getPart() == FIRST_PART) {
|
2026-04-23 00:38:40 +07:00
|
|
|
part1Key = file.getPath();
|
2026-04-04 15:31:51 +07:00
|
|
|
} else if (file.getPart() == SECOND_PART) {
|
2026-04-23 00:38:40 +07:00
|
|
|
part2Key = file.getPath();
|
2026-04-04 15:31:51 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
if (part1Key == null || part2Key == null) {
|
2026-04-04 15:31:51 +07:00
|
|
|
throw new IOException("Missing part 1 or part 2 for user: " + userId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
String part1Content = cloudStorageService.downloadPartFromBucket(bucketCold, part1Key);
|
|
|
|
|
String part2Content = cloudStorageService.downloadPartFromBucket(bucketCold, part2Key);
|
2026-04-04 15:31:51 +07:00
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
String fullBase64 = part1Content + part2Content;
|
|
|
|
|
|
|
|
|
|
return Base64.getDecoder().decode(fullBase64);
|
2026-04-04 15:31:51 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private byte[] createZip(List<FileEntity> 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);
|
2026-04-23 00:38:40 +07:00
|
|
|
|
|
|
|
|
Map<String, String> cloudKeys = cloudStorageService.saveSplitPartsToColdStorage(userId, file1, file2);
|
|
|
|
|
|
|
|
|
|
Map<Integer, String> files = Map.of(
|
|
|
|
|
FIRST_PART, cloudKeys.get("part1"),
|
|
|
|
|
SECOND_PART, cloudKeys.get("part2"));
|
2026-04-04 15:31:51 +07:00
|
|
|
|
|
|
|
|
moderationFileService.addFiles(userId, files);
|
|
|
|
|
|
2026-04-23 00:38:40 +07:00
|
|
|
log.info("Saved split files to cold storage for userId: {}", userId);
|
2026-04-04 15:31:51 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|