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));
}
}
}