From 647fd6e1a7143d059afb0a8f02cf9696fddf621d Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 22 Dec 2025 14:07:44 +0700 Subject: [PATCH 1/3] NCBACK-3 Add found by yandex image --- build.gradle | 4 + docker-compose.yaml | 2 + .../nocopy/configuration/HandlerConfig.java | 4 +- .../nocopy/configuration/JacksonConfig.java | 2 + .../configuration/file/YandexConfig.java | 14 ++ .../java/ru/soune/nocopy/dto/MessageCode.java | 1 + .../nocopy/dto/file/ImageSearchRequest.java | 10 ++ .../nocopy/dto/file/YandexSearchResponse.java | 36 +++++ .../handler/ImageFoundRequestHandler.java | 36 +++++ .../repository/FileEntityRepository.java | 1 + .../nocopy/service/YandexSearchService.java | 149 ++++++++++++++++++ src/main/resources/application.yaml | 5 + 12 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/ImageSearchRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java create mode 100644 src/main/java/ru/soune/nocopy/service/YandexSearchService.java diff --git a/build.gradle b/build.gradle index d42173a..111da7e 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,10 @@ dependencies { implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'commons-validator:commons-validator:1.7' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0' + implementation 'tools.jackson.core:jackson-core:3.0.3' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' annotationProcessor 'org.projectlombok:lombok' diff --git a/docker-compose.yaml b/docker-compose.yaml index d3f9924..eb4724b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -30,6 +30,8 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_PORT: 5432 POSTGRES_HOST: db + YANDEX_SEARCH_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q + YANDEX_SEARCH_FOLDER_ID: b1gokpdbm6qfpsou8pcd depends_on: - db ports: diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 5b3c744..674b27c 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -16,7 +16,8 @@ public class HandlerConfig { LoginRequestHandler login, FileUploadHandler upload, FileEntityHandler file, - LogoutRequestHandler logoutHandler + LogoutRequestHandler logoutHandler, + ImageFoundRequestHandler imageFoundRequestHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -24,6 +25,7 @@ public class HandlerConfig { map.put(20004, upload); map.put(20005, file); map.put(20006, logoutHandler); + map.put(20007, imageFoundRequestHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/configuration/JacksonConfig.java b/src/main/java/ru/soune/nocopy/configuration/JacksonConfig.java index 99c5c00..db00da1 100644 --- a/src/main/java/ru/soune/nocopy/configuration/JacksonConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/JacksonConfig.java @@ -35,6 +35,8 @@ public class JacksonConfig { mapper.registerModule(javaTimeModule); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.enable(SerializationFeature.INDENT_OUTPUT); return mapper; } diff --git a/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java new file mode 100644 index 0000000..e9f1576 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.configuration.file; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "yandex") +public class YandexConfig { + private String apiKey; + private String folderId; + private String searchUrl; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index a3ac162..ee9d40b 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -8,6 +8,7 @@ public enum MessageCode { INVALID_TOKEN(2, "Invalid token"), INVALID_ACTION(2, "Invalid action"), FILE_UPLOAD_ERROR(2, "File upload error"), + IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INCOMPLETE_UPLOAD(2, "Not load all chunks"), MSG_ID_NOT_FOUND(4, "Message id not found"), diff --git a/src/main/java/ru/soune/nocopy/dto/file/ImageSearchRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ImageSearchRequest.java new file mode 100644 index 0000000..f943989 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/ImageSearchRequest.java @@ -0,0 +1,10 @@ +package ru.soune.nocopy.dto.file; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ImageSearchRequest { + @JsonProperty("file_id") + private String fileId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java new file mode 100644 index 0000000..760440f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java @@ -0,0 +1,36 @@ +package ru.soune.nocopy.dto.file; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class YandexSearchResponse { + + @JsonProperty("images") + private List images; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ImageResult { + @JsonProperty("url") + private String url; + + @JsonProperty("pageUrl") + private String pageUrl; + + @JsonProperty("pageTitle") + private String pageTitle; + + @JsonProperty("width") + private Integer width; + + @JsonProperty("height") + private Integer height; + + @JsonProperty("host") + private String host; + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java new file mode 100644 index 0000000..1b7b0fd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -0,0 +1,36 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.file.ImageSearchRequest; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.service.YandexSearchService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ImageFoundRequestHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final YandexSearchService yandexSearchService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + + ImageSearchRequest imageSearchRequest = objectMapper.convertValue( + request.getMessageBody(), ImageSearchRequest.class); + + String fileId = imageSearchRequest.getFileId(); + + YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), response); + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 3ee41cb..3214e9d 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -14,6 +14,7 @@ import java.util.Optional; public interface FileEntityRepository extends JpaRepository { List findByUserId(Long userId); + Optional findByUserIdAndChecksum(Long userId, String imageHash); List findByUserIdAndStatus(Long userId, FileStatus status); diff --git a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java new file mode 100644 index 0000000..611036c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java @@ -0,0 +1,149 @@ +package ru.soune.nocopy.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.DeserializationFeature; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.repository.FileEntityRepository; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; + +@Slf4j +@Service +@RequiredArgsConstructor +public class YandexSearchService { + + private final FileEntityRepository fileEntityRepository; + private final ObjectMapper objectMapper; + + @Value("${yandex.api-key}") + private String apiKey; + + @Value("${yandex.folder-id}") + private String folderId; + + @Value("${yandex.search-url}") + private String searchUrl; + + @PostConstruct + public void init() { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + } + + public YandexSearchResponse searchByFileEntity(String fileId) throws IOException { + FileEntity fileEntity = fileEntityRepository.findById(fileId) + .orElseThrow(() -> { + log.error("File not found: {}", fileId); + return new RuntimeException("ФFile not found: " + fileId); + }); + + + if (!isImageFile(fileEntity)) { + log.error("File not image: {}", fileEntity.getMimeType()); + throw new RuntimeException("File not image: " + fileId); + } + + byte[] fileBytes = readFileFromDisk(fileEntity); + log.info("Прочитано {} байт с диска", fileBytes.length); + + return callYandexApi(fileBytes); + } + + private boolean isImageFile(FileEntity fileEntity) { + String mimeType = fileEntity.getMimeType(); + return mimeType != null && mimeType.startsWith("image"); + } + + private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException { + Path filePath = Path.of(fileEntity.getFilePath()); + + if (!Files.exists(filePath)) { + throw new IOException("File not found: " + fileEntity.getFilePath()); + } + + if (!Files.isReadable(filePath)) { + throw new IOException("Cannot read file: " + fileEntity.getFilePath()); + } + + return Files.readAllBytes(filePath); + } + + private YandexSearchResponse callYandexApi(byte[] imageBytes) throws IOException { + String imageBase64 = Base64.getEncoder().encodeToString(imageBytes); + String jsonRequest = String.format("{\"folderId\":\"%s\",\"data\":\"%s\",\"page\":0,\"showSimilarImages\":true}", + folderId, imageBase64); + + URL url = new URL(searchUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + try { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Api-Key " + apiKey); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + connection.setDoOutput(true); + + try (OutputStream os = connection.getOutputStream()) { + byte[] input = jsonRequest.getBytes("utf-8"); + os.write(input, 0, input.length); + os.flush(); + } + + int responseCode = connection.getResponseCode(); + + String responseBody; + if (responseCode == HttpURLConnection.HTTP_OK) { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "utf-8"))) { + responseBody = readAll(br); + } + } else { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getErrorStream(), "utf-8"))) { + responseBody = readAll(br); + } + throw new IOException("Ошибка Yandex API: " + responseCode + " - " + responseBody); + } + + return parseJsonResponse(responseBody); + + } finally { + connection.disconnect(); + } + } + + private String readAll(BufferedReader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return sb.toString(); + } + + private YandexSearchResponse parseJsonResponse(String json) { + YandexSearchResponse response = null; + try { + response = objectMapper.readValue(json, YandexSearchResponse.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return response; + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 08326e1..45fcaaa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -38,6 +38,11 @@ security: allowed-headers: "*" allow-credentials: true +yandex: + api-key: ${YANDEX_SEARCH_API_KEY} + folder-id: ${YANDEX_SEARCH_FOLDER_ID} + search-url: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" + server: port: ${SERVER_PORT:8080} -- 2.54.0 From ffb147d716488d696315ec84a067a333ad05d434 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 22 Dec 2025 14:42:27 +0700 Subject: [PATCH 2/3] NCBACK-3 Add found by yandex image --- .../handler/ImageFoundRequestHandler.java | 5 ++--- .../nocopy/service/YandexSearchService.java | 20 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 1b7b0fd..301bb10 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -22,9 +22,8 @@ public class ImageFoundRequestHandler implements RequestHandler { @Override public BaseResponse handle(BaseRequest request) throws Exception { - - ImageSearchRequest imageSearchRequest = objectMapper.convertValue( - request.getMessageBody(), ImageSearchRequest.class); + ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), + ImageSearchRequest.class); String fileId = imageSearchRequest.getFileId(); diff --git a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java index 611036c..3fd8c60 100644 --- a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java @@ -8,8 +8,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.exception.FileEntityNotFoundException; +import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import java.io.*; @@ -18,6 +22,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; +import java.util.Map; @Slf4j @Service @@ -45,18 +50,19 @@ public class YandexSearchService { public YandexSearchResponse searchByFileEntity(String fileId) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> { - log.error("File not found: {}", fileId); - return new RuntimeException("ФFile not found: " + fileId); + throw new NotValidFieldException("File not found", new BaseResponse(20007, + MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), + Map.of("fileId",fileId))); }); - if (!isImageFile(fileEntity)) { log.error("File not image: {}", fileEntity.getMimeType()); - throw new RuntimeException("File not image: " + fileId); + throw new NotValidFieldException("File not image", new BaseResponse(20007, + MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), + Map.of("file_type", fileEntity.getMimeType()))); } byte[] fileBytes = readFileFromDisk(fileEntity); - log.info("Прочитано {} байт с диска", fileBytes.length); return callYandexApi(fileBytes); } @@ -93,7 +99,7 @@ public class YandexSearchService { connection.setRequestProperty("Authorization", "Api-Key " + apiKey); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Accept", "application/json"); - connection.setRequestProperty("User-Agent", "Mozilla/5.0"); +// connection.setRequestProperty("User-Agent", "Mozilla/5.0"); connection.setConnectTimeout(30000); connection.setReadTimeout(30000); connection.setDoOutput(true); @@ -117,7 +123,7 @@ public class YandexSearchService { new InputStreamReader(connection.getErrorStream(), "utf-8"))) { responseBody = readAll(br); } - throw new IOException("Ошибка Yandex API: " + responseCode + " - " + responseBody); + throw new IOException("Error Yandex API: " + responseCode + " - " + responseBody); } return parseJsonResponse(responseBody); -- 2.54.0 From b3a3d57ee3e8442b982600b70a942fa6e808d3f3 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 9 Jan 2026 23:41:19 +0700 Subject: [PATCH 3/3] NCBACK-3 --- .../nocopy/configuration/file/YandexConfig.java | 14 -------------- .../soune/nocopy/service/YandexSearchService.java | 11 +++++++++-- 2 files changed, 9 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java diff --git a/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java deleted file mode 100644 index e9f1576..0000000 --- a/src/main/java/ru/soune/nocopy/configuration/file/YandexConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.soune.nocopy.configuration.file; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Data -@Configuration -@ConfigurationProperties(prefix = "yandex") -public class YandexConfig { - private String apiKey; - private String folderId; - private String searchUrl; -} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java index 3fd8c60..6c3d32f 100644 --- a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/YandexSearchService.java @@ -54,6 +54,7 @@ public class YandexSearchService { MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), Map.of("fileId",fileId))); }); + byte[] fileBytes; if (!isImageFile(fileEntity)) { log.error("File not image: {}", fileEntity.getMimeType()); @@ -62,7 +63,14 @@ public class YandexSearchService { Map.of("file_type", fileEntity.getMimeType()))); } - byte[] fileBytes = readFileFromDisk(fileEntity); + try { + fileBytes = readFileFromDisk(fileEntity); + } catch (IOException e) { + throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007, + MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), + Map.of("fileId", fileId, + "filePath", fileEntity.getFilePath()))); + } return callYandexApi(fileBytes); } @@ -99,7 +107,6 @@ public class YandexSearchService { connection.setRequestProperty("Authorization", "Api-Key " + apiKey); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Accept", "application/json"); -// connection.setRequestProperty("User-Agent", "Mozilla/5.0"); connection.setConnectTimeout(30000); connection.setReadTimeout(30000); connection.setDoOutput(true); -- 2.54.0