Newer
Older
screenshot-server / src / main / java / com / screenshot / server / service / StorageService.java
package com.screenshot.server.service;

import com.screenshot.server.config.StorageConfig;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class StorageService {

    private final StorageConfig config;
    private Path rootLocation;

    public StorageService(StorageConfig config) {
        this.config = config;
    }

    @PostConstruct
    public void init() {
        this.rootLocation = Path.of(config.getUploadDir());
        try {
            Files.createDirectories(rootLocation);
            System.out.println("Директория хранения: " + rootLocation.toAbsolutePath());
        } catch (IOException e) {
            throw new RuntimeException("Не удалось создать директорию хранения", e);
        }
    }

    public record SaveResult(
        boolean success,
        String message,
        String filePath,
        long fileSize
    ) {}

    public SaveResult store(MultipartFile file) {
        try {
            if (file.isEmpty()) {
                return new SaveResult(false, "Файл пустой", null, 0);
            }

            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null || originalFilename.isBlank()) {
                return new SaveResult(false, "Имя файла отсутствует", null, 0);
            }

            String extension = getExtension(originalFilename);
            if (!config.getAllowedExtensions().contains(extension.toLowerCase())) {
                return new SaveResult(false, 
                    "Недопустимое расширение: " + extension, null, 0);
            }

            if (file.getSize() > config.getMaxFileSizeBytes()) {
                return new SaveResult(false, 
                    "Файл слишком большой. Макс: " + config.getMaxFileSizeMb() + "MB", 
                    null, 0);
            }

            String dateFolder = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
            Path targetDir = rootLocation.resolve(dateFolder);
            Files.createDirectories(targetDir);

            String uniqueFilename = generateUniqueFilename(originalFilename);
            Path targetPath = targetDir.resolve(uniqueFilename);

            try (InputStream inputStream = file.getInputStream()) {
                Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
            }

            String relativePath = dateFolder + "/" + uniqueFilename;
            System.out.println("Файл сохранён: " + relativePath + 
                " (" + formatSize(file.getSize()) + ")");

            return new SaveResult(true, "Файл успешно сохранён", 
                relativePath, file.getSize());

        } catch (IOException e) {
            return new SaveResult(false, "Ошибка сохранения: " + e.getMessage(), null, 0);
        }
    }

    /**
     * Получить список доступных дат
     */
    public List<LocalDate> getAvailableDates() {
        try {
            return Files.list(rootLocation)
                .filter(Files::isDirectory)
                .map(p -> p.getFileName().toString())
                .filter(name -> name.matches("\\d{4}-\\d{2}-\\d{2}"))
                .map(LocalDate::parse)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        } catch (IOException e) {
            return Collections.emptyList();
        }
    }

    /**
     * Получить файлы за определённую дату
     */
    public List<Path> getFilesForDate(LocalDate date) {
        Path dateDir = rootLocation.resolve(date.format(DateTimeFormatter.ISO_DATE));
        if (!Files.exists(dateDir)) {
            return Collections.emptyList();
        }

        try {
            List<Path> files = new ArrayList<>();
            Files.walkFileTree(dateDir, new SimpleFileVisitor<>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    files.add(file);
                    return FileVisitResult.CONTINUE;
                }
            });
            return files;
        } catch (IOException e) {
            return Collections.emptyList();
        }
    }

    /**
     * Получить файлы за период времени в определённую дату
     */
    public List<Path> getFilesForDateAndPeriod(LocalDate date, LocalTime startTime, LocalTime endTime) {
        List<Path> allFiles = getFilesForDate(date);

        return allFiles.stream()
            .filter(file -> {
                try {
                    BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
                    LocalDateTime fileTime = LocalDateTime.ofInstant(
                        attrs.lastModifiedTime().toInstant(), 
                        java.time.ZoneId.systemDefault()
                    );
                    LocalTime time = fileTime.toLocalTime();
                    return !time.isBefore(startTime) && !time.isAfter(endTime);
                } catch (IOException e) {
                    return false;
                }
            })
            .collect(Collectors.toList());
    }

    /**
     * Создать ZIP архивы из списка файлов с ограничением размера
     */
    public List<Path> createZipArchives(List<Path> files, long maxSizeBytes) throws IOException {
        if (files.isEmpty()) {
            return Collections.emptyList();
        }

        List<Path> archives = new ArrayList<>();
        Path tempDir = Files.createTempDirectory("telegram_zip_");

        List<List<Path>> groups = groupFilesBySize(files, maxSizeBytes);
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));

        for (int i = 0; i < groups.size(); i++) {
            String archiveName = String.format("screenshots_%s_part%d.zip", timestamp, i + 1);
            Path archivePath = tempDir.resolve(archiveName);

            createZipFile(archivePath, groups.get(i));
            archives.add(archivePath);
        }

        return archives;
    }

    private List<List<Path>> groupFilesBySize(List<Path> files, long maxSizeBytes) {
        List<List<Path>> groups = new ArrayList<>();
        List<Path> currentGroup = new ArrayList<>();
        long currentSize = 0;

        for (Path file : files) {
            try {
                long fileSize = Files.size(file);

                if (currentSize + fileSize > maxSizeBytes && !currentGroup.isEmpty()) {
                    groups.add(new ArrayList<>(currentGroup));
                    currentGroup.clear();
                    currentSize = 0;
                }

                currentGroup.add(file);
                currentSize += fileSize;

            } catch (IOException e) {
                // Skip file
            }
        }

        if (!currentGroup.isEmpty()) {
            groups.add(currentGroup);
        }

        return groups;
    }

    private void createZipFile(Path zipPath, List<Path> files) throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(
                new BufferedOutputStream(Files.newOutputStream(zipPath)))) {

            zos.setLevel(6);

            for (Path file : files) {
                String entryName = file.getFileName().toString();
                // Добавляем родительскую папку для организации
                Path parent = file.getParent();
                if (parent != null) {
                    entryName = parent.getFileName().toString() + "/" + entryName;
                }

                ZipEntry entry = new ZipEntry(entryName);
                entry.setTime(Files.getLastModifiedTime(file).toMillis());

                zos.putNextEntry(entry);
                Files.copy(file, zos);
                zos.closeEntry();
            }
        }
    }

    private String getExtension(String filename) {
        int lastDot = filename.lastIndexOf('.');
        if (lastDot > 0) {
            return filename.substring(lastDot + 1);
        }
        return "";
    }

    private String generateUniqueFilename(String originalFilename) {
        String extension = getExtension(originalFilename);
        String nameWithoutExt = originalFilename.substring(0, 
            originalFilename.length() - extension.length() - 1);

        String uuid = UUID.randomUUID().toString().substring(0, 8);

        return nameWithoutExt + "_" + uuid + "." + extension;
    }

    public String formatSize(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
        return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    }

    public StorageInfo getStorageInfo() {
        try {
            long totalFiles = Files.walk(rootLocation)
                .filter(Files::isRegularFile)
                .count();

            long totalSize = Files.walk(rootLocation)
                .filter(Files::isRegularFile)
                .mapToLong(p -> {
                    try {
                        return Files.size(p);
                    } catch (IOException e) {
                        return 0;
                    }
                })
                .sum();

            return new StorageInfo(rootLocation.toString(), totalFiles, totalSize);

        } catch (IOException e) {
            return new StorageInfo(rootLocation.toString(), 0, 0);
        }
    }

    public record StorageInfo(String path, long fileCount, long totalSizeBytes) {
        public String formattedSize() {
            if (totalSizeBytes < 1024) return totalSizeBytes + " B";
            if (totalSizeBytes < 1024 * 1024) 
                return String.format("%.1f KB", totalSizeBytes / 1024.0);
            if (totalSizeBytes < 1024 * 1024 * 1024) 
                return String.format("%.1f MB", totalSizeBytes / (1024.0 * 1024.0));
            return String.format("%.2f GB", totalSizeBytes / (1024.0 * 1024.0 * 1024.0));
        }
    }
}