Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Халтобин Евгений 2024-11-08 15:01:08 +03:00
commit 38099dcbd7
9 changed files with 258 additions and 178 deletions

41
config.md Normal file
View 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
View file

@ -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>

View file

@ -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 {

View file

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

View file

@ -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", "Невозможно проверить файл ЛК РП",
"Невозможно проверить файл по причине недоступности или ошибки в работе антивируса"
);
} }

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

View file

@ -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,

View file

@ -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-клиент

View file

@ -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