package com.screenshot.service;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Сервис для создания ZIP архивов с контролем размера
*/
public class ZipService {
private static final DateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
/**
* Создать ZIP архивы из директории с ограничением размера
* @param sourceDir исходная директория
* @param outputDir директория для архивов
* @param maxSizeBytes максимальный размер архива в байтах
* @return список созданных ZIP файлов
*/
public List<Path> createZipArchives(Path sourceDir, Path outputDir, long maxSizeBytes)
throws IOException {
if (!Files.exists(sourceDir)) {
throw new IOException("Исходная директория не существует: " + sourceDir);
}
Files.createDirectories(outputDir);
// Собираем все файлы
List<FileInfo> allFiles = collectFiles(sourceDir);
if (allFiles.isEmpty()) {
System.out.println("Нет файлов для архивации");
return Collections.emptyList();
}
// Группируем файлы по архивам с учётом размера
List<List<FileInfo>> fileGroups = groupFilesBySize(allFiles, maxSizeBytes);
// Создаём архивы
List<Path> createdArchives = new ArrayList<>();
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
for (int i = 0; i < fileGroups.size(); i++) {
String archiveName = String.format("screenshots_%s_part%d.zip", timestamp, i + 1);
Path archivePath = outputDir.resolve(archiveName);
createZipFile(archivePath, fileGroups.get(i), sourceDir);
createdArchives.add(archivePath);
System.out.println("Создан архив: " + archivePath +
" (" + formatSize(Files.size(archivePath)) + ")");
}
return createdArchives;
}
/**
* Собрать информацию о всех файлах в директории
*/
private List<FileInfo> collectFiles(Path sourceDir) throws IOException {
List<FileInfo> files = new ArrayList<>();
Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (isImageFile(file)) {
files.add(new FileInfo(file, attrs.size()));
}
return FileVisitResult.CONTINUE;
}
});
// Сортируем по дате (новые в конец)
files.sort(Comparator.comparing(f -> f.path.getFileName().toString()));
return files;
}
/**
* Группировать файлы по размеру для архивов
*/
private List<List<FileInfo>> groupFilesBySize(List<FileInfo> files, long maxSizeBytes) {
List<List<FileInfo>> groups = new ArrayList<>();
List<FileInfo> currentGroup = new ArrayList<>();
long currentSize = 0;
// Коэффициент сжатия PNG (примерно 0.9 - PNG уже сжат)
double compressionRatio = 0.95;
for (FileInfo file : files) {
long estimatedCompressedSize = (long) (file.size * compressionRatio);
// Если файл слишком большой для одного архива
if (estimatedCompressedSize > maxSizeBytes) {
if (!currentGroup.isEmpty()) {
groups.add(new ArrayList<>(currentGroup));
currentGroup.clear();
currentSize = 0;
}
// Добавляем большой файл в отдельный архив
groups.add(List.of(file));
continue;
}
// Если текущая группа переполнится
if (currentSize + estimatedCompressedSize > maxSizeBytes && !currentGroup.isEmpty()) {
groups.add(new ArrayList<>(currentGroup));
currentGroup.clear();
currentSize = 0;
}
currentGroup.add(file);
currentSize += estimatedCompressedSize;
}
// Добавляем оставшиеся файлы
if (!currentGroup.isEmpty()) {
groups.add(currentGroup);
}
return groups;
}
/**
* Создать ZIP файл из списка файлов
*/
private void createZipFile(Path zipPath, List<FileInfo> files, Path baseDir)
throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
zos.setLevel(6); // Средний уровень сжатия
for (FileInfo fileInfo : files) {
Path relativePath = baseDir.relativize(fileInfo.path);
ZipEntry entry = new ZipEntry(relativePath.toString().replace("\\", "/"));
entry.setTime(Files.getLastModifiedTime(fileInfo.path).toMillis());
zos.putNextEntry(entry);
Files.copy(fileInfo.path, zos);
zos.closeEntry();
}
}
}
/**
* Проверить, является ли файл изображением
*/
private boolean isImageFile(Path file) {
String name = file.getFileName().toString().toLowerCase();
return name.endsWith(".png") || name.endsWith(".jpg") ||
name.endsWith(".jpeg") || name.endsWith(".bmp");
}
/**
* Форматировать размер файла
*/
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
}
/**
* Информация о файле
*/
private record FileInfo(Path path, long size) {}
/**
* Удалить исходные файлы после успешной отправки
*/
public void deleteSourceFiles(Path sourceDir) throws IOException {
Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
if (isImageFile(file)) {
Files.delete(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
// Удаляем пустые директории
try (var stream = Files.list(dir)) {
if (stream.findAny().isEmpty() && !dir.equals(sourceDir)) {
Files.delete(dir);
}
}
return FileVisitResult.CONTINUE;
}
});
}
/**
* Удалить временные архивы
*/
public void deleteArchives(List<Path> archives) {
for (Path archive : archives) {
try {
Files.deleteIfExists(archive);
} catch (IOException e) {
System.err.println("Не удалось удалить архив: " + archive);
}
}
}
}