Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
38099dcbd7
9 changed files with 258 additions and 178 deletions
41
config.md
Normal file
41
config.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Описание конфигурации параметров проекта
|
||||||
|
|
||||||
|
В micord.env заданы следующие переменные окружения:
|
||||||
|
|
||||||
|
#### Конфигурация и топики Kafka ЛК РП
|
||||||
|
- `AV_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka
|
||||||
|
- `AV_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами
|
||||||
|
- `AV_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений
|
||||||
|
- `AV_KAFKA_USERNAME` - пользователь для подключения к Kafka
|
||||||
|
- `AV_KAFKA_PASSWORD` - пароль для подключения к Kafka
|
||||||
|
- `AV_KAFKA_GROUP_ID` - идентификатор группы потребителей, который отвечает за создание группы для объединения нескольких потребителей
|
||||||
|
- `AV_KAFKA_TOPIC_NAME` - топик для чтения данных по загруженному в личном кабинете файлу
|
||||||
|
- `AV_KAFKA_STATUS_TOPIC_NAME` - топик для отправки статусов проверки файла
|
||||||
|
|
||||||
|
#### Конфигурация и топики Kafka ЕРВУ
|
||||||
|
- `ERVU_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka
|
||||||
|
- `ERVU_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами
|
||||||
|
- `ERVU_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений
|
||||||
|
- `ERVU_KAFKA_USERNAME` - пользователь для подключения к Kafka
|
||||||
|
- `ERVU_KAFKA_PASSWORD` - пароль для подключения к Kafka
|
||||||
|
- `ERVU_KAFKA_GROUP_ID` - идентификатор группы потребителей, который отвечает за создание группы для объединения нескольких потребителей
|
||||||
|
- `ERVU_KAFKA_ERROR_TOPIC_NAME` - топик для отправки данных об ошибках проверки файла или наличии вирусов
|
||||||
|
- `ERVU_KAFKA_SUCCESS_TOPIC_NAME` - топик для отправки данных об успешной проверке файла
|
||||||
|
- `ERVU_KAFKA_RESPONSE_TOPIC_NAME` - топик для чтения статусов файла, полученных от ЕРВУ
|
||||||
|
|
||||||
|
#### Настройки взаимодействия с файлами и антивирусом
|
||||||
|
- `FILE_SAVING_PATH` - путь для сохранения файла на диске
|
||||||
|
- `AV_CHECK_ENABLED` - параметр включения/отключения проверки на вирусы
|
||||||
|
- `AV_REST_ADDRESS` - url для отправки файлов на проверку антивирусом
|
||||||
|
- `AV_FIRST_TIMEOUT_MILLISECONDS` - таймаут ожидания окончания проверки анивирусом
|
||||||
|
- `AV_RETRY_MAX_ATTEMPTS_COUNT` - количество попыток проверки файла
|
||||||
|
- `AV_RETRY_DELAY_MILLISECONDS` - задержка между попытками проверки файла
|
||||||
|
|
||||||
|
#### Конфигурация S3
|
||||||
|
- `S3_ENDPOINT` - url для подключения к S3
|
||||||
|
- `S3_ACCESS_KEY` - публичная часть пары ключей AWS
|
||||||
|
- `S3_SECRET_KEY` - закрытая часть пары ключей AWS
|
||||||
|
- `S3_BUCKET_NAME` - наименование бакета для сохранения проверенного файла
|
||||||
|
- `S3_PATH_STYLE_ACCESS_ENABLED` - параметр включения/отключения стиля, при котором название бакета указывается в части пути до объекта в URI
|
||||||
|
|
||||||
|
|
||||||
19
pom.xml
19
pom.xml
|
|
@ -7,13 +7,13 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.3.0</version>
|
<version>3.3.5</version>
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>ru.micord.ervu.lkrp</groupId>
|
<groupId>ru.micord.ervu.lkrp</groupId>
|
||||||
<artifactId>file-upload</artifactId>
|
<artifactId>file-upload</artifactId>
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
<version>1.10.0-SNAPSHOT</version>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
@ -42,12 +42,6 @@
|
||||||
<version>4.5.14</version>
|
<version>4.5.14</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.postgresql</groupId>
|
|
||||||
<artifactId>postgresql</artifactId>
|
|
||||||
<version>42.7.3</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|
@ -77,11 +71,6 @@
|
||||||
<artifactId>httpmime</artifactId>
|
<artifactId>httpmime</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.postgresql</groupId>
|
|
||||||
<artifactId>postgresql</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|
@ -104,10 +93,6 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-jooq</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
|
||||||
import org.springframework.retry.annotation.EnableRetry;
|
import org.springframework.retry.annotation.EnableRetry;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author r.latypov
|
* @author r.latypov
|
||||||
*/
|
*/
|
||||||
@EnableRetry
|
@EnableRetry
|
||||||
@EnableTransactionManagement
|
|
||||||
@SpringBootApplication(exclude = KafkaAutoConfiguration.class)
|
@SpringBootApplication(exclude = KafkaAutoConfiguration.class)
|
||||||
public class FileUploadApplication {
|
public class FileUploadApplication {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,8 @@ package ru.micord.ervu.av.exception;
|
||||||
* @author r.latypov
|
* @author r.latypov
|
||||||
*/
|
*/
|
||||||
public class RetryableException extends RuntimeException {
|
public class RetryableException extends RuntimeException {
|
||||||
|
|
||||||
|
public RetryableException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,7 @@ public record FileStatus(String code, String status, String description) {
|
||||||
public static final FileStatus FILE_STATUS_04 = new FileStatus("04", "Получен ЕРВУ",
|
public static final FileStatus FILE_STATUS_04 = new FileStatus("04", "Получен ЕРВУ",
|
||||||
"Файл был принят в обработку"
|
"Файл был принят в обработку"
|
||||||
);
|
);
|
||||||
|
public static final FileStatus FILE_STATUS_11 = new FileStatus("11", "Невозможно проверить файл ЛК РП",
|
||||||
|
"Невозможно проверить файл по причине недоступности или ошибки в работе антивируса"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
86
src/main/java/ru/micord/ervu/av/service/FileManager.java
Normal file
86
src/main/java/ru/micord/ervu/av/service/FileManager.java
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package ru.micord.ervu.av.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpDelete;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.conn.HttpHostConnectException;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.Retryable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import ru.micord.ervu.av.exception.FileUploadException;
|
||||||
|
import ru.micord.ervu.av.exception.InvalidHttpFileUrlException;
|
||||||
|
import ru.micord.ervu.av.exception.RetryableException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author gulnaz
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class FileManager {
|
||||||
|
|
||||||
|
@Retryable(retryFor = {InvalidHttpFileUrlException.class},
|
||||||
|
maxAttemptsExpression = "${av.retry.max.attempts.count}",
|
||||||
|
backoff = @Backoff(delayExpression = "${av.retry.delay.milliseconds}"))
|
||||||
|
public void downloadFile(String fileUrl, Path filePath)
|
||||||
|
throws InvalidHttpFileUrlException, FileUploadException {
|
||||||
|
File file = filePath.toFile();
|
||||||
|
HttpGet request = new HttpGet(fileUrl);
|
||||||
|
|
||||||
|
try (CloseableHttpClient client = HttpClients.createDefault();
|
||||||
|
CloseableHttpResponse response = client.execute(request)) {
|
||||||
|
|
||||||
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
|
if (statusCode == HttpStatus.OK.value()) {
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (entity != null) {
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
entity.writeTo(outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// в хранилище не обнаружено файла; сообщение некорректно
|
||||||
|
String message = "http status code " + statusCode + " : " + fileUrl;
|
||||||
|
throw new InvalidHttpFileUrlException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpHostConnectException e) {
|
||||||
|
throw new FileUploadException(e);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// хранилище недоступно; сообщение некорректно
|
||||||
|
String message =
|
||||||
|
(e.getMessage() == null ? e.getCause().getMessage() : e.getMessage()) + " : " + fileUrl;
|
||||||
|
throw new InvalidHttpFileUrlException(message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retryable(retryFor = {RetryableException.class},
|
||||||
|
maxAttemptsExpression = "${av.retry.max.attempts.count}",
|
||||||
|
backoff = @Backoff(delayExpression = "${av.retry.delay.milliseconds}"))
|
||||||
|
public void deleteFile(String fileUrl) throws FileUploadException {
|
||||||
|
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
||||||
|
HttpDelete delete = new HttpDelete(fileUrl);
|
||||||
|
|
||||||
|
try (CloseableHttpResponse response = client.execute(delete)) {
|
||||||
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
|
if (statusCode != HttpStatus.NO_CONTENT.value()) {
|
||||||
|
String message = "http status code " + statusCode + " : " + fileUrl;
|
||||||
|
throw new RetryableException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// непредусмотренная ошибка доступа через http-клиент
|
||||||
|
throw new FileUploadException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package ru.micord.ervu.av.service;
|
package ru.micord.ervu.av.service;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
|
@ -9,22 +7,9 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonSyntaxException;
|
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
|
||||||
import org.apache.http.client.methods.HttpDelete;
|
|
||||||
import org.apache.http.client.methods.HttpGet;
|
|
||||||
import org.apache.http.client.methods.HttpPost;
|
|
||||||
import org.apache.http.conn.HttpHostConnectException;
|
|
||||||
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
|
||||||
import org.apache.http.entity.mime.content.FileBody;
|
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
|
||||||
import org.apache.http.impl.client.HttpClients;
|
|
||||||
import org.apache.http.util.EntityUtils;
|
|
||||||
import org.apache.kafka.clients.admin.NewTopic;
|
import org.apache.kafka.clients.admin.NewTopic;
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -32,7 +17,6 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.kafka.annotation.KafkaListener;
|
import org.springframework.kafka.annotation.KafkaListener;
|
||||||
import org.springframework.kafka.core.KafkaTemplate;
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
import org.springframework.kafka.support.Acknowledgment;
|
import org.springframework.kafka.support.Acknowledgment;
|
||||||
|
|
@ -42,10 +26,10 @@ import org.springframework.messaging.handler.annotation.Header;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import ru.micord.ervu.av.exception.FileUploadException;
|
import ru.micord.ervu.av.exception.FileUploadException;
|
||||||
import ru.micord.ervu.av.exception.InvalidHttpFileUrlException;
|
import ru.micord.ervu.av.exception.InvalidHttpFileUrlException;
|
||||||
|
import ru.micord.ervu.av.exception.RetryableException;
|
||||||
import ru.micord.ervu.av.kafka.dto.DownloadRequest;
|
import ru.micord.ervu.av.kafka.dto.DownloadRequest;
|
||||||
import ru.micord.ervu.av.kafka.dto.DownloadResponse;
|
import ru.micord.ervu.av.kafka.dto.DownloadResponse;
|
||||||
import ru.micord.ervu.av.kafka.dto.FileStatus;
|
import ru.micord.ervu.av.kafka.dto.FileStatus;
|
||||||
import ru.micord.ervu.av.response.AvFileSendResponse;
|
|
||||||
import ru.micord.ervu.av.response.AvResponse;
|
import ru.micord.ervu.av.response.AvResponse;
|
||||||
import ru.micord.ervu.av.s3.S3Service;
|
import ru.micord.ervu.av.s3.S3Service;
|
||||||
|
|
||||||
|
|
@ -57,10 +41,6 @@ public class FileUploadService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(FileUploadService.class);
|
private static final Logger logger = LoggerFactory.getLogger(FileUploadService.class);
|
||||||
@Value("${av.check.enabled}")
|
@Value("${av.check.enabled}")
|
||||||
private boolean avCheckEnabled;
|
private boolean avCheckEnabled;
|
||||||
@Value("${av.rest.address}")
|
|
||||||
private String avRestAddress;
|
|
||||||
@Value("${av.first.timeout.milliseconds}")
|
|
||||||
private Long avFirstTimeoutMilliseconds;
|
|
||||||
@Value("${file.saving.path}")
|
@Value("${file.saving.path}")
|
||||||
private String fileSavingPath;
|
private String fileSavingPath;
|
||||||
private final KafkaTemplate<String, String> kafkaTemplate;
|
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||||
|
|
@ -68,14 +48,18 @@ public class FileUploadService {
|
||||||
private final NewTopic outErrorTopic;
|
private final NewTopic outErrorTopic;
|
||||||
private final NewTopic outSuccessTopic;
|
private final NewTopic outSuccessTopic;
|
||||||
private final NewTopic inStatusTopic;
|
private final NewTopic inStatusTopic;
|
||||||
|
private final FileManager fIleManager;
|
||||||
private final ReceiveScanReportRetryable receiveScanReportRetryable;
|
private final ReceiveScanReportRetryable receiveScanReportRetryable;
|
||||||
private final S3Service s3Service;
|
private final S3Service s3Service;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FileUploadService(@Qualifier("outputKafkaTemplate") KafkaTemplate<String, String> kafkaTemplate,
|
public FileUploadService(
|
||||||
|
@Qualifier("outputKafkaTemplate") KafkaTemplate<String, String> kafkaTemplate,
|
||||||
@Qualifier("inputKafkaTemplate") KafkaTemplate<String, String> inKafkaTemplate,
|
@Qualifier("inputKafkaTemplate") KafkaTemplate<String, String> inKafkaTemplate,
|
||||||
NewTopic outErrorTopic, NewTopic outSuccessTopic, ReceiveScanReportRetryable receiveScanReportRetryable,
|
NewTopic outErrorTopic, NewTopic outSuccessTopic,
|
||||||
S3Service s3Service, NewTopic inStatusTopic) {
|
ReceiveScanReportRetryable receiveScanReportRetryable,
|
||||||
|
S3Service s3Service, NewTopic inStatusTopic,
|
||||||
|
FileManager fIleManager) {
|
||||||
this.kafkaTemplate = kafkaTemplate;
|
this.kafkaTemplate = kafkaTemplate;
|
||||||
this.outErrorTopic = outErrorTopic;
|
this.outErrorTopic = outErrorTopic;
|
||||||
this.outSuccessTopic = outSuccessTopic;
|
this.outSuccessTopic = outSuccessTopic;
|
||||||
|
|
@ -83,6 +67,7 @@ public class FileUploadService {
|
||||||
this.s3Service = s3Service;
|
this.s3Service = s3Service;
|
||||||
this.inKafkaTemplate = inKafkaTemplate;
|
this.inKafkaTemplate = inKafkaTemplate;
|
||||||
this.inStatusTopic = inStatusTopic;
|
this.inStatusTopic = inStatusTopic;
|
||||||
|
this.fIleManager = fIleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@KafkaListener(id = "${spring.kafka.consumer.group.id}", topics = "${kafka.in.topic.name}",
|
@KafkaListener(id = "${spring.kafka.consumer.group.id}", topics = "${kafka.in.topic.name}",
|
||||||
|
|
@ -103,23 +88,33 @@ public class FileUploadService {
|
||||||
logger.info("working in {}", System.getProperty("user.home"));
|
logger.info("working in {}", System.getProperty("user.home"));
|
||||||
Path filePath = Paths.get(fileSavingPath, fileUrl.fileName());
|
Path filePath = Paths.get(fileSavingPath, fileUrl.fileName());
|
||||||
String downloadUrl = fileUrl.fileUrl();
|
String downloadUrl = fileUrl.fileUrl();
|
||||||
downloadFile(downloadUrl, filePath);
|
fIleManager.downloadFile(downloadUrl, filePath);
|
||||||
|
boolean isAvError = false;
|
||||||
boolean clean = true;
|
boolean clean = true;
|
||||||
boolean infected = false;
|
boolean infected = false;
|
||||||
|
|
||||||
if (avCheckEnabled) {
|
if (avCheckEnabled) {
|
||||||
AvResponse avResponse = checkFile(filePath);
|
try {
|
||||||
|
AvResponse avResponse = receiveScanReportRetryable.checkFile(filePath);
|
||||||
clean = Arrays.stream(avResponse.verdicts())
|
clean = Arrays.stream(avResponse.verdicts())
|
||||||
.anyMatch(verdict -> verdict.equalsIgnoreCase(AvResponse.Scan.VERDICT_CLEAN));
|
.anyMatch(verdict -> verdict.equalsIgnoreCase(AvResponse.Scan.VERDICT_CLEAN));
|
||||||
infected = Arrays.stream(avResponse.verdicts())
|
infected = Arrays.stream(avResponse.verdicts())
|
||||||
.anyMatch(verdict -> verdict.equalsIgnoreCase(AvResponse.Scan.VERDICT_INFECTED));
|
.anyMatch(verdict -> verdict.equalsIgnoreCase(AvResponse.Scan.VERDICT_INFECTED));
|
||||||
|
}
|
||||||
|
catch (FileUploadException | RetryableException e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
isAvError = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infected || !clean) {
|
if (isAvError || infected || !clean) {
|
||||||
downloadRequest.fileInfo().setFileUrl(null);
|
downloadRequest.fileInfo().setFileUrl(null);
|
||||||
downloadRequest.fileInfo().setFileStatus(FileStatus.FILE_STATUS_02);
|
FileStatus fileStatus = isAvError ? FileStatus.FILE_STATUS_11 : FileStatus.FILE_STATUS_02;
|
||||||
sendMessage(outErrorTopic.name(), downloadRequest, messageId, kafkaTemplate);
|
downloadRequest.fileInfo().setFileStatus(fileStatus);
|
||||||
|
|
||||||
|
if (!isAvError) {
|
||||||
|
sendMessage(outErrorTopic.name(), downloadRequest, messageId, kafkaTemplate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
String fileRef = s3Service.putFile(filePath, fileUrl.fileName());
|
String fileRef = s3Service.putFile(filePath, fileUrl.fileName());
|
||||||
|
|
@ -130,7 +125,7 @@ public class FileUploadService {
|
||||||
}
|
}
|
||||||
sendMessage(inStatusTopic.name(), downloadRequest, messageId, inKafkaTemplate);
|
sendMessage(inStatusTopic.name(), downloadRequest, messageId, inKafkaTemplate);
|
||||||
|
|
||||||
deleteFile(downloadUrl);
|
fIleManager.deleteFile(downloadUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.delete(filePath);
|
Files.delete(filePath);
|
||||||
|
|
@ -144,10 +139,12 @@ public class FileUploadService {
|
||||||
}
|
}
|
||||||
catch (InvalidHttpFileUrlException e) {
|
catch (InvalidHttpFileUrlException e) {
|
||||||
// считаем, что повторная обработка сообщения не нужна
|
// считаем, что повторная обработка сообщения не нужна
|
||||||
// ошибку логируем, сообщаем об ошибке, помечаем прочтение сообщения
|
// ошибку логируем, отправляем сообщение с новым статусом, помечаем прочтение сообщения
|
||||||
logger.error(e.getMessage() + ": " + kafkaInMessage);
|
logger.error(e.getMessage() + ": " + kafkaInMessage);
|
||||||
|
downloadRequest.fileInfo().setFileUrl(null);
|
||||||
|
downloadRequest.fileInfo().setFileStatus(FileStatus.FILE_STATUS_11);
|
||||||
|
sendMessage(inStatusTopic.name(), downloadRequest, messageId, inKafkaTemplate);
|
||||||
acknowledgment.acknowledge();
|
acknowledgment.acknowledge();
|
||||||
throw new RuntimeException(kafkaInMessage, e);
|
|
||||||
}
|
}
|
||||||
catch (FileUploadException e) {
|
catch (FileUploadException e) {
|
||||||
// считаем, что нужно повторное считывание сообщения
|
// считаем, что нужно повторное считывание сообщения
|
||||||
|
|
@ -187,120 +184,6 @@ public class FileUploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadFile(String fileUrl, Path filePath)
|
|
||||||
throws InvalidHttpFileUrlException, FileUploadException {
|
|
||||||
File file = filePath.toFile();
|
|
||||||
HttpGet request = new HttpGet(fileUrl);
|
|
||||||
|
|
||||||
try (CloseableHttpClient client = HttpClients.createDefault();
|
|
||||||
CloseableHttpResponse response = client.execute(request)) {
|
|
||||||
|
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
if (statusCode == HttpStatus.OK.value()) {
|
|
||||||
HttpEntity entity = response.getEntity();
|
|
||||||
if (entity != null) {
|
|
||||||
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
|
||||||
entity.writeTo(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// в хранилище не обнаружено файла; сообщение некорректно
|
|
||||||
String message = "http status code " + statusCode + " : " + fileUrl;
|
|
||||||
throw new InvalidHttpFileUrlException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpHostConnectException e) {
|
|
||||||
throw new FileUploadException(e);
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
// хранилище недоступно; сообщение некорректно
|
|
||||||
String message =
|
|
||||||
(e.getMessage() == null ? e.getCause().getMessage() : e.getMessage()) + " : " + fileUrl;
|
|
||||||
throw new InvalidHttpFileUrlException(message, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AvResponse checkFile(Path filePath) throws FileUploadException {
|
|
||||||
File file = filePath.toFile();
|
|
||||||
|
|
||||||
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
|
||||||
HttpPost post = new HttpPost(avRestAddress);
|
|
||||||
HttpEntity entity = MultipartEntityBuilder.create()
|
|
||||||
.addPart("file", new FileBody(file))
|
|
||||||
.build();
|
|
||||||
post.setEntity(entity);
|
|
||||||
|
|
||||||
try (CloseableHttpResponse postResponse = client.execute(post)) {
|
|
||||||
int postStatusCode = postResponse.getStatusLine().getStatusCode();
|
|
||||||
String postResponseJson = EntityUtils.toString(postResponse.getEntity());
|
|
||||||
|
|
||||||
AvFileSendResponse avFileSendResponse;
|
|
||||||
try {
|
|
||||||
avFileSendResponse = new Gson().fromJson(postResponseJson, AvFileSendResponse.class);
|
|
||||||
}
|
|
||||||
catch (JsonSyntaxException e) {
|
|
||||||
throw new FileUploadException("error json: " + postResponseJson, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postStatusCode != HttpStatus.CREATED.value()) {
|
|
||||||
StringBuilder stringBuilder = new StringBuilder(
|
|
||||||
"http status code " + postStatusCode + " for " + avRestAddress + " post request.");
|
|
||||||
|
|
||||||
String status = avFileSendResponse.status();
|
|
||||||
if (status != null) {
|
|
||||||
stringBuilder.append(" Status: ").append(status).append(".");
|
|
||||||
}
|
|
||||||
if (avFileSendResponse.error() != null) {
|
|
||||||
stringBuilder.append(" Error code: ")
|
|
||||||
.append(avFileSendResponse.error().code())
|
|
||||||
.append(". Error message: ")
|
|
||||||
.append(avFileSendResponse.error().message())
|
|
||||||
.append(". ");
|
|
||||||
}
|
|
||||||
throw new FileUploadException(stringBuilder.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
String id = avFileSendResponse.id();
|
|
||||||
String reportRequestUri = avRestAddress + "/" + id;
|
|
||||||
HttpGet get = new HttpGet(reportRequestUri);
|
|
||||||
|
|
||||||
// waiting for timeout time before first request
|
|
||||||
try {
|
|
||||||
TimeUnit.MILLISECONDS.sleep(
|
|
||||||
avFirstTimeoutMilliseconds == null ? 1000L : avFirstTimeoutMilliseconds);
|
|
||||||
}
|
|
||||||
catch (InterruptedException e) {
|
|
||||||
throw new FileUploadException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return receiveScanReportRetryable.receiveScanReport(client, get);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
// непредусмотренная ошибка доступа через http-клиент
|
|
||||||
throw new FileUploadException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteFile(String fileUrl) throws FileUploadException {
|
|
||||||
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
|
||||||
HttpDelete delete = new HttpDelete(fileUrl);
|
|
||||||
|
|
||||||
try (CloseableHttpResponse response = client.execute(delete)) {
|
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
if (statusCode != HttpStatus.NO_CONTENT.value()) {
|
|
||||||
String message = "http status code " + statusCode + " : " + fileUrl;
|
|
||||||
throw new RuntimeException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
// непредусмотренная ошибка доступа через http-клиент
|
|
||||||
throw new FileUploadException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessage(@NonNull String topicName, Object object, String messageId,
|
private void sendMessage(@NonNull String topicName, Object object, String messageId,
|
||||||
KafkaTemplate<String, String> template) {
|
KafkaTemplate<String, String> template) {
|
||||||
ProducerRecord<String, String> record = new ProducerRecord<>(topicName,
|
ProducerRecord<String, String> record = new ProducerRecord<>(topicName,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
package ru.micord.ervu.av.service;
|
package ru.micord.ervu.av.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
import org.apache.http.client.ClientProtocolException;
|
import org.apache.http.client.ClientProtocolException;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||||
|
import org.apache.http.entity.mime.content.FileBody;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.retry.annotation.Backoff;
|
import org.springframework.retry.annotation.Backoff;
|
||||||
import org.springframework.retry.annotation.Retryable;
|
import org.springframework.retry.annotation.Retryable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import ru.micord.ervu.av.exception.FileUploadException;
|
import ru.micord.ervu.av.exception.FileUploadException;
|
||||||
import ru.micord.ervu.av.exception.RetryableException;
|
import ru.micord.ervu.av.exception.RetryableException;
|
||||||
|
import ru.micord.ervu.av.response.AvFileSendResponse;
|
||||||
import ru.micord.ervu.av.response.AvResponse;
|
import ru.micord.ervu.av.response.AvResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,19 +32,92 @@ import ru.micord.ervu.av.response.AvResponse;
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ReceiveScanReportRetryable {
|
public class ReceiveScanReportRetryable {
|
||||||
|
|
||||||
|
@Value("${av.rest.address}")
|
||||||
|
private String avRestAddress;
|
||||||
|
@Value("${av.first.timeout.milliseconds:1000}")
|
||||||
|
private Long avFirstTimeoutMilliseconds;
|
||||||
|
|
||||||
@Retryable(retryFor = {RetryableException.class},
|
@Retryable(retryFor = {RetryableException.class},
|
||||||
maxAttemptsExpression = "${av.retry.max.attempts.count}",
|
maxAttemptsExpression = "${av.retry.max.attempts.count}",
|
||||||
backoff = @Backoff(delayExpression = "${av.retry.delay.milliseconds}"))
|
backoff = @Backoff(delayExpression = "${av.retry.delay.milliseconds}"))
|
||||||
public AvResponse receiveScanReport(CloseableHttpClient client, HttpGet get)
|
public AvResponse checkFile(Path filePath) throws RetryableException, FileUploadException {
|
||||||
throws FileUploadException {
|
File file = filePath.toFile();
|
||||||
|
|
||||||
|
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
||||||
|
HttpPost post = new HttpPost(avRestAddress);
|
||||||
|
HttpEntity entity = MultipartEntityBuilder.create()
|
||||||
|
.addPart("file", new FileBody(file))
|
||||||
|
.build();
|
||||||
|
post.setEntity(entity);
|
||||||
|
|
||||||
|
try (CloseableHttpResponse postResponse = client.execute(post)) {
|
||||||
|
int postStatusCode = postResponse.getStatusLine().getStatusCode();
|
||||||
|
String postResponseJson = EntityUtils.toString(postResponse.getEntity());
|
||||||
|
AvFileSendResponse avFileSendResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
avFileSendResponse = new Gson().fromJson(postResponseJson, AvFileSendResponse.class);
|
||||||
|
}
|
||||||
|
catch (JsonSyntaxException e) {
|
||||||
|
throw new FileUploadException("error json: " + postResponseJson, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postStatusCode != HttpStatus.CREATED.value()) {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder(
|
||||||
|
"http status code " + postStatusCode + " for " + avRestAddress + " post request.");
|
||||||
|
String status = avFileSendResponse.status();
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
stringBuilder.append(" Status: ").append(status).append(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avFileSendResponse.error() != null) {
|
||||||
|
stringBuilder.append(" Error code: ")
|
||||||
|
.append(avFileSendResponse.error().code())
|
||||||
|
.append(". Error message: ")
|
||||||
|
.append(avFileSendResponse.error().message())
|
||||||
|
.append(". ");
|
||||||
|
}
|
||||||
|
throw new FileUploadException(stringBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String id = avFileSendResponse.id();
|
||||||
|
String reportRequestUri = avRestAddress + "/" + id;
|
||||||
|
HttpGet get = new HttpGet(reportRequestUri);
|
||||||
|
|
||||||
|
// waiting for timeout time before first request
|
||||||
|
try {
|
||||||
|
TimeUnit.MILLISECONDS.sleep(avFirstTimeoutMilliseconds);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new FileUploadException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiveScanReport(client, get);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ClientProtocolException e) {
|
||||||
|
throw new RetryableException("Failed to check file");
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// непредусмотренная ошибка доступа через http-клиент
|
||||||
|
throw new FileUploadException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvResponse receiveScanReport(CloseableHttpClient client, HttpGet get)
|
||||||
|
throws RetryableException, FileUploadException {
|
||||||
|
|
||||||
try (CloseableHttpResponse getResponse = client.execute(get)) {
|
try (CloseableHttpResponse getResponse = client.execute(get)) {
|
||||||
int getStatusCode = getResponse.getStatusLine().getStatusCode();
|
int getStatusCode = getResponse.getStatusLine().getStatusCode();
|
||||||
|
|
||||||
if (getStatusCode == HttpStatus.OK.value()) {
|
if (getStatusCode == HttpStatus.OK.value()) {
|
||||||
String getResponseJson = EntityUtils.toString(getResponse.getEntity());
|
String getResponseJson = EntityUtils.toString(getResponse.getEntity());
|
||||||
AvResponse avResponse = new Gson().fromJson(getResponseJson, AvResponse.class);
|
AvResponse avResponse = new Gson().fromJson(getResponseJson, AvResponse.class);
|
||||||
|
|
||||||
if (avResponse.completed() == null) {
|
if (avResponse.completed() == null) {
|
||||||
throw new RetryableException();
|
throw new RetryableException("Failed to complete file scan");
|
||||||
}
|
}
|
||||||
return avResponse;
|
return avResponse;
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +128,7 @@ public class ReceiveScanReportRetryable {
|
||||||
}
|
}
|
||||||
catch (ClientProtocolException e) {
|
catch (ClientProtocolException e) {
|
||||||
// непредусмотренная ошибка доступа через http-клиент
|
// непредусмотренная ошибка доступа через http-клиент
|
||||||
throw new RuntimeException(e);
|
throw new RetryableException("Failed to receive scan report");
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
// непредусмотренная ошибка доступа через http-клиент
|
// непредусмотренная ошибка доступа через http-клиент
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,6 @@ s3.out.secret_key=${S3_SECRET_KEY}
|
||||||
s3.out.bucket_name=${S3_OUT_BUCKET_NAME}
|
s3.out.bucket_name=${S3_OUT_BUCKET_NAME}
|
||||||
s3.out.path.style.access.enabled=${S3_OUT_PATH_STYLE_ACCESS_ENABLED}
|
s3.out.path.style.access.enabled=${S3_OUT_PATH_STYLE_ACCESS_ENABLED}
|
||||||
#
|
#
|
||||||
# spring jooq dsl bean properties begin ->
|
|
||||||
spring.jooq.sql-dialect=Postgres
|
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
|
||||||
# spring jooq dsl bean properties <- end
|
|
||||||
|
|
||||||
# endpoints management
|
# endpoints management
|
||||||
management.endpoints.web.exposure.include = health, info, metrics
|
management.endpoints.web.exposure.include = health, info, metrics
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue