From 983b5eddda01c1ca2808d02dd45ae1a80bac7dcb Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 26 Feb 2026 00:27:31 +0700 Subject: [PATCH] dev conver files --- .../nocopy/configuration/ImageMigration.java | 44 ++++++++ .../nocopy/controller/FileController.java | 29 ++--- .../soune/nocopy/entity/file/FileEntity.java | 6 + .../repository/FileEntityRepository.java | 2 + .../nocopy/service/FileSimilarityService.java | 3 +- .../service/file/FileEntityService.java | 7 +- .../service/file/ImageResizeService.java | 104 ++++++++++++++++++ .../service/search/SearchImageService.java | 66 ----------- 8 files changed, 172 insertions(+), 89 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/configuration/ImageMigration.java create mode 100644 src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java new file mode 100644 index 0000000..0638556 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.file.ImageResizeService; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@Component +@Slf4j +public class ImageMigration { + + @Autowired + private FileEntityRepository fileRepository; + + @Autowired + private ImageResizeService imageResizeService; + + @EventListener(ApplicationReadyEvent.class) + public void migrate() { + List images = fileRepository.findByThumbnailPathIsNull(); + + for (FileEntity file : images) { + try { + Path path = Paths.get(file.getProtectedFilePath()); + if (Files.exists(path)) { + byte[] data = Files.readAllBytes(path); + imageResizeService.generateSizes(file, data); + fileRepository.save(file); + } + } catch (Exception e) { + log.error("Error migrating {}: {}", file.getId(), e.getMessage()); + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index f814982..2b100bc 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -14,6 +14,7 @@ import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; +import ru.soune.nocopy.service.file.ImageResizeService; import java.io.FileNotFoundException; import java.net.URLEncoder; @@ -41,24 +42,8 @@ public class FileController { @Autowired private UserRepository userRepository; -// @GetMapping("/public/{fileId}") -// public ResponseEntity getPublicFile( -// @PathVariable String fileId) throws IOException { -// -// FileEntity fileEntity = fileRepository.findById(fileId) -// .orElseThrow(() -> new RuntimeException("File not found")); -// -// Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); -// -// ContentDisposition contentDisposition = ContentDisposition.inline() -// .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) -// .build(); -// -// return ResponseEntity.ok() -// .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) -// .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) -// .body(resource); -// } + @Autowired + private ImageResizeService imageResizeService; @GetMapping("/public/{fileId}") public ResponseEntity getPublicFile(@PathVariable String fileId) { @@ -117,14 +102,16 @@ public class FileController { } } - @GetMapping("/protected/{fileId}") + @GetMapping("/protected/{fileId}/{type}") public ResponseEntity getProtectedFile( - @PathVariable String fileId) throws IOException { + @PathVariable String fileId, @PathVariable String type) throws IOException { FileEntity fileEntity = fileRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("File not found")); - Resource resource = fileStorageService.loadFileAsResource(fileEntity.getProtectedFilePath()); + String path = imageResizeService.getPath(fileEntity, type); + + Resource resource = fileStorageService.loadFileAsResource(path); if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED || fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) { diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index a7b74b6..02104c7 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -81,6 +81,12 @@ public class FileEntity { @JsonIgnore private String signature; + @Column(name = "thumbnail_path") + private String thumbnailPath; + + @Column(name = "medium_path") + private String mediumPath; + @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 4398a51..17cf09e 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -61,4 +61,6 @@ public interface FileEntityRepository extends JpaRepository @Query("SELECT MAX(f.supportId) FROM FileEntity f") Integer findMaxSupportId(); + + List findByThumbnailPathIsNull(); } diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index 5cb6d14..da1b0e0 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -211,7 +211,8 @@ public class FileSimilarityService { .supportId(similarImageProjection.getSupportId()) .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) - .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) +// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) + .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .build(); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 007738c..1325f54 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -255,13 +255,14 @@ public class FileEntityService { fileEntityRepository.save(fileEntity); } + private ImageResizeService imageResizeService; + @Transactional public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(id) .orElseThrow(() -> new RuntimeException("File not found: " + id)); String extension = determineFileExtension(fileExt, fileEntity); - log.info("EXTENSION: {}", extension); Path protectedFilePath = prepareProtectedPath(fileEntity, extension); if (Files.exists(protectedFilePath)) { @@ -270,6 +271,10 @@ public class FileEntityService { Files.write(protectedFilePath, data); + if (imageResizeService.isImage(extension)) { + imageResizeService.generateSizes(fileEntity, data); + } + fileEntity.setProtectedFilePath(protectedFilePath.toString()); fileEntity.setProtectedAt(LocalDateTime.now()); fileEntity.setUpdatedAt(LocalDateTime.now()); diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java new file mode 100644 index 0000000..7e9383c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -0,0 +1,104 @@ +package ru.soune.nocopy.service.file; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.file.FileEntity; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@Service +@Slf4j +public class ImageResizeService { + + private static final int THUMBNAIL_SIZE = 150; + private static final int MEDIUM_SIZE = 600; + + @Value("${file.storage.path}") + private String storagePath; + + /** + * Проверяет, является ли файл изображением + */ + public boolean isImage(String extension) { + return extension != null && List.of("jpg", "jpeg", "png", "gif", "webp") + .contains(extension.toLowerCase()); + } + + /** + * Генерирует два размера для изображения + */ + @Transactional + public void generateSizes(FileEntity file, byte[] data) throws IOException { + if (!isImage(file.getFileExtension())) { + return; + } + + try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) { + BufferedImage original = ImageIO.read(bis); + + if (original == null) { + log.warn("Cannot read image: {}", file.getId()); + return; + } + + String baseName = file.getId().replace(".", "_"); + + if (original.getWidth() > THUMBNAIL_SIZE) { + String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE); + file.setThumbnailPath(thumbPath); + } + + if (original.getWidth() > MEDIUM_SIZE) { + String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE); + file.setMediumPath(mediumPath); + } + } + } + + /** + * Ресайзит и сохраняет + */ + private String resizeAndSave(BufferedImage original, String fileName, int targetWidth) throws IOException { + int targetHeight = (int) ((double) original.getHeight() / original.getWidth() * targetWidth); + + BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = resized.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.drawImage(original, 0, 0, targetWidth, targetHeight, null); + g.dispose(); + + Path dirPath = Paths.get(storagePath, "resized"); + Files.createDirectories(dirPath); + + Path filePath = dirPath.resolve(fileName + ".jpg"); + ImageIO.write(resized, "jpg", filePath.toFile()); + + return filePath.toString(); + } + + /** + * Получить путь к нужному размеру + */ + public String getPath(FileEntity file, String size) { + + if ("thumbnail".equals(size) && file.getThumbnailPath() != null) { + return file.getThumbnailPath(); + } + + if ("medium".equals(size) && file.getMediumPath() != null) { + return file.getMediumPath(); + } + + return file.getProtectedFilePath(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index 1a35f71..d25a633 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -127,72 +127,6 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } -// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { -// if (searchApiKey == null || searchApiKey.isBlank()) { -// throw new IllegalStateException("SearchAPI key not configured"); -// } -// -// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient; -// -// HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") -// .newBuilder() -// .addQueryParameter("engine", engine) -// .addQueryParameter("api_key", searchApiKey) -// .addQueryParameter("url", imageUrl) -// .addQueryParameter("search_type", searchType) -// .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) -// .build(); -// -// Request request = new Request.Builder() -// .url(url) -// .header("Accept", "application/json") -// .header("User-Agent", "Mozilla/5.0") -// .build(); -// -// int maxAttempts = 2; -// IOException lastException = null; -// -// for (int attempt = 1; attempt <= maxAttempts; attempt++) { -// long start = System.currentTimeMillis(); -// -// try (Response response = client.newCall(request).execute()) { -// long duration = System.currentTimeMillis() - start; -// log.info("Yandex response code={}, duration={}ms, attempt={}", -// response.code(), duration, attempt); -// -// if (!response.isSuccessful()) { -// throw new IOException("API error: " + response.code()); -// } -// -// ResponseBody body = response.body(); -// if (body == null) { -// throw new IOException("Empty response body"); -// } -// -// return body.string(); -// -// } catch (IOException e) { -// lastException = e; -// log.warn("Attempt {} failed for image: {}, error: {}", -// attempt, imageUrl, e.getMessage()); -// -// if (attempt == maxAttempts) { -// log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl); -// throw lastException; -// } -// -// try { -// Thread.sleep(500L * attempt); -// } catch (InterruptedException ie) { -// Thread.currentThread().interrupt(); -// throw new IOException("Retry interrupted", ie); -// } -// } -// } -// -// throw lastException; -// } - private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { if (searchApiKey == null || searchApiKey.isBlank()) { throw new IllegalStateException("SearchAPI key not configured");