Newer
Older
screenshot-app / src / main / java / com / screenshot / service / ZipService.java
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);
            }
        }
    }
}